Skip to content

[ENH/WIP] Add SPM Fieldmap Tool wrapper #1905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 20, 2018

Conversation

jguillon
Copy link
Contributor

Added:

  • VDM Calculation pre-substracted phase case (SIEMENS machines)

Todo:

  • Support VDM Application
  • Support different magnitude and phase file types

Added:
- VDM Calculation pre-substracted phase case (SIEMENS machines)
Todo:
- Support VDM Application
- Support different magnitude and phase file types
Copy link
Contributor

@oesteban oesteban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My two cents, however I'm not very familiar with SPM. Someone more knowledgeable should look at this

class FieldMap(SPMCommand):
"""Use spm to calculate fieldmap vdm.

http://www.fil.ion.ucl.ac.uk/spm/doc/manual.pdf#page=19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix link, this leads to page 16 (Slice timing correction)



class FieldMap(SPMCommand):
"""Use spm to calculate fieldmap vdm.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what "vdm" means? (should be in the documentation, so please expand the acronym in this docstring)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VDM stands for Voxel Displacement Map. Docstring will be modified in the next commit.

field='subj.defaults.defaultsval.uflags.ws',
desc='weighted smoothing');
# Brain mask defaults parameters
template = traits.File(copyfile=False, exists=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use File from nipype.interfaces.base, not from traits.

field='subj.defaults.defaultsval.mflags.reg',
desc='regularization value used in the segmentation');
# EPI unwarping for quality check
epi = traits.File(copyfile=False, exists=True, mandatory=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File from base

matchvdm = traits.Bool(True, usedefault=True,
field='subj.matchvdm',
desc='match VDM to EPI');
sessname = traits.String('_run-', usedefault=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, modify the top from ..base import ... to add Str.

Then use it here directly: sessname = Str(...)

writeunwarped = traits.Bool(False, usedefault=True,
field='subj.writeunwarped',
desc='write unwarped EPI');
anat = traits.File(copyfile=False, exists=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File from base

magnitude = File(mandatory=True, exists=True, copyfile=False,
field='subj.data.presubphasemag.magnitude',
desc='presubstracted magnitude file')
et = traits.List(traits.Float(), minlen=2, maxlen=2, mandatory=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe:

et = traits.Tuple(traits.Float, traits.Float, mandatory=True, ...)

?

def _format_arg(self, opt, spec, val):
"""Convert input to appropriate format for spm
"""
if opt == 'phase' or opt == 'magnitude' or opt == 'anat':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if opt in ['phase', 'magnitude', 'anat']:

"""
if opt == 'phase' or opt == 'magnitude' or opt == 'anat':
return scans_for_fname(filename_to_list(val))
if opt == 'epi' or opt == 'magnitude':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if opt in ['epi', 'magnitude']:

However, I don't see any difference with including these two options with the previous condition, finally, it always falls into scans_for_fname(filename_to_list(val)) anyways

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must have done this for a reason that I can't remember anymore... I will merge these if statements in the next commit.

@codecov-io
Copy link

codecov-io commented Feb 7, 2018

Codecov Report

Merging #1905 into master will increase coverage by <.01%.
The diff coverage is 80%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #1905      +/-   ##
=========================================
+ Coverage   67.49%   67.5%   +<.01%     
=========================================
  Files        1225    1226       +1     
  Lines       60313   60375      +62     
  Branches     8649    8656       +7     
=========================================
+ Hits        40709   40756      +47     
- Misses      18488   18502      +14     
- Partials     1116    1117       +1
Flag Coverage Δ
#smoketests 51.32% <78.43%> (+0.05%) ⬆️
#unittests 65.47% <78.46%> (ø) ⬆️
Impacted Files Coverage Δ
nipype/interfaces/tests/test_auto_DataGrabber.py 78.57% <ø> (ø) ⬆️
...ipype/interfaces/tests/test_auto_SSHDataGrabber.py 78.57% <ø> (ø) ⬆️
nipype/interfaces/spm/__init__.py 100% <100%> (ø) ⬆️
nipype/interfaces/spm/base.py 83.83% <50%> (-0.23%) ⬇️
nipype/interfaces/spm/preprocess.py 57.27% <79.16%> (+1.21%) ⬆️
nipype/interfaces/spm/tests/test_auto_FieldMap.py 85.71% <85.71%> (ø)
nipype/utils/profiler.py 43.1% <0%> (-1.15%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 628bdd5...a4994ef. Read the comment docs.

@oesteban oesteban dismissed their stale review February 7, 2018 18:47

All the requested changes have been included, I'll try to review again but I don't want my potential review to block this PR

>>> from nipype.interfaces.spm import FieldMap
>>> fm = FieldMap()
>>> fm.inputs.phase = 'phasediff.nii'
>>> fm.inputs.magnitude = 'magnitude1.nii'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phasediff.nii and magnitude1.nii don't exist in nipype/testing/data. Either add them or use existing files (such as phase.nii and magnitude.nii).

Copy link
Member

@effigies effigies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments. One to get your doctest fixed, and a number because I felt that your inputspec might benefit from clearer names.

I marked a few with specific suggestions, but in general you may want to consider using more verbose names, rather than copying the field names.

>>> fm = FieldMap()
>>> fm.inputs.phase = 'phase.nii'
>>> fm.inputs.magnitude = 'magnitude.nii'
>>> fm.inputs.et = [5.19, 7.65]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a tuple, not a list.

outputs['vdm'] = []
for phase in filename_to_list(self.inputs.phase):
outputs['vdm'].append(fname_presuffix(phase, prefix='vdm5_sc'))
outputs['vdm'] = list_to_filename(outputs['vdm'])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by this. self.inputs.phase is a single file, so this could just be:

if self.inputs.jobtype == 'calculatevdm':
    outputs['vdm'] = fname_presuffix(self.inputs.phase, prefix='vdm5_sc')

Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not, I just adapted another more complicated wrapper's piece of code without thinking about making it clearer. Thanks for noticing!

"""
einputs = super(FieldMap, self)._parse_inputs()
jobtype = self.inputs.jobtype
return [{'%s' % (jobtype): einputs[0]}]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the advantage of this over the following?

return [{self.inputs.jobtype: einputs[0]}]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like better your style, but I originally used the syntax of another wrapper (typically Coregister).

magnitude = File(mandatory=True, exists=True, copyfile=False,
field='subj.data.presubphasemag.magnitude',
desc='presubstracted magnitude file')
et = traits.Tuple(traits.Float, traits.Float, mandatory=True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is matches the final part of the field name, so it may be that SPM users will find this intuitive, but you may want to consider echo_times or similar for clarity.

class FieldMapInputSpec(SPMCommandInputSpec):
jobtype = traits.Enum('calculatevdm', 'applyvdm', usedefault=True,
desc='one of: calculatevdm, applyvdm')
phase = File(mandatory=True, exists=True, copyfile=False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We often use the suffix _file to indicate inputs that should be files. So consider making this phase_file. Similarly below with magnitude_file.

Not a big deal, if you think that would be disorienting to SPM users.

epifm = traits.Bool(False, usedefault=True,
field='subj.defaults.defaultsval.epifm',
desc='epi-based field map');
ajm = traits.Bool(False, usedefault=True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, again, I would consider jacobian_modulation or similar.

template = File(copyfile=False, exists=True,
field='subj.defaults.defaultsval.mflags.template',
desc='template image for brain masking');
fwhm = traits.Range(low=0, value=5, usedefault=True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got fwhm twice. Perhaps this one should be mask_fwhm, and the other unwarp_fwhm?

@effigies
Copy link
Member

Also, please merge master again, to update the tests.

>>> fm.inputs.echo_times = (5.19, 7.65)
>>> fm.inputs.blip_direction = 1
>>> fm.inputs.total_readout_time = 15.6
>>> fm.inputs.epi_file = 'bold.nii'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for another round, but, this needs to be 'epi.nii', or some other file in nipype/testing/data

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries! It's on me! Btw, I went through the CONTRIBUTING.md file, but did not see any paragraph explaining how to run tests before submitting a PR. Maybe it would have save you some reviews of my code and others'.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that should probably be more clear. I just run

PYTHONPATH="." py.test --doctest-modules nipype

That tests all of nipype. You can specify a specific file you want to test, as well:

PYTHONPATH="." py.test --doctest-modules nipype/interfaces/spm/preprocess.py

@satra satra mentioned this pull request Feb 16, 2018
Copy link
Member

@effigies effigies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@effigies effigies added this to the 1.0.1 milestone Feb 20, 2018
@effigies effigies merged commit 6e07273 into nipy:master Feb 20, 2018
@fabioboh
Copy link
Contributor

I am working on implementing the apply vdm feature.

@fabioboh
Copy link
Contributor

I will soon make a pull request so the apply VDM also work on multiple frames (e.g. fMRI images). This is a tiny change of my previous pull request and I am actually sorry it was not already included in the previous one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants