Skip to content
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

[ENH] Add phase1/phase2 SDC support #1359

Closed
wants to merge 44 commits into from
Closed
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b2a937a
first pass at 2phases SDC pipeline
mattcieslak Nov 1, 2018
f2b45d4
workflow.connect syntax typo
mattcieslak Nov 1, 2018
9dec5d8
remove unused variables from fmap.py
mattcieslak Nov 1, 2018
840d167
add back pha2rads after accidental delete
mattcieslak Nov 1, 2018
bb510de
Changed copied code from phasediff functions
mattcieslak Nov 1, 2018
4d78483
resolve flake8 errors
mattcieslak Nov 1, 2018
6e0edda
More flake8 issues
mattcieslak Nov 1, 2018
9a0fcde
added import to interfaces/__init__.py
mattcieslak Nov 1, 2018
e5a6dee
Merge phase1phase2 into a single 4D file and send it to the DataSink
mattcieslak Nov 2, 2018
d89c776
Send derived phasediff image to datasink
mattcieslak Nov 2, 2018
f6d4aa7
Take phasediff from compfmap, not inputnode
mattcieslak Nov 2, 2018
649dafe
Refactored interfaces
mattcieslak Nov 2, 2018
8558aa5
fix typo and node name
mattcieslak Nov 2, 2018
1d12256
Calculate input to prelude correctly
mattcieslak Nov 3, 2018
70b3413
phase1, phase2 worksgit status
mattcieslak Nov 4, 2018
191e155
Update zendodo json
mattcieslak Nov 4, 2018
880d04d
Changes suggested during PR review
mattcieslak Nov 8, 2018
dc5b547
Merge branch 'master' into phase1phase2
mattcieslak Nov 8, 2018
9f9284e
line was too long
mattcieslak Nov 8, 2018
ce36514
Merge branch 'phase1phase2' of github.com:mattcieslak/fmriprep into p…
mattcieslak Nov 8, 2018
0712c5e
Add missing bracket
mattcieslak Nov 8, 2018
4fd6add
Merge branch 'master' of https://github.com/poldracklab/fmriprep into…
mattcieslak Nov 8, 2018
09316a9
Make scaling more general
mattcieslak Nov 8, 2018
f9ca876
Credit original script author
mattcieslak Nov 8, 2018
150b54f
Merge branch 'master' of https://github.com/poldracklab/fmriprep into…
mattcieslak Nov 20, 2018
ada0d9d
Undo formatting changes and add citation
mattcieslak Nov 20, 2018
2c52d49
pylint error
mattcieslak Nov 20, 2018
27c51ae
Fix zenodo
mattcieslak Nov 20, 2018
f57c063
missed a couple formatting changes
mattcieslak Nov 20, 2018
2f6a294
style changes
mattcieslak Nov 24, 2018
a8fc3bd
Merge branch 'master' into phase1phase2
effigies Dec 15, 2018
0f7cdec
FIX: Typo in .zenodo.json
effigies Dec 15, 2018
f2e861d
STY: Revert unneeded style changes
effigies Dec 15, 2018
ff2d7d9
Merge branch 'master' into phase1phase2
effigies Dec 17, 2018
d94346f
Interpret phase image values directly
mattcieslak Dec 18, 2018
5ce44ce
Merge branch 'master' of https://github.com/poldracklab/fmriprep into…
mattcieslak Dec 18, 2018
db42f74
Merge remote-tracking branch 'upstream/master' into phase1phase2
effigies Dec 19, 2018
6c7ec33
Check that encoding is not specified as Bipolar
mattcieslak Dec 20, 2018
ef86939
Merge branch 'master' of https://github.com/poldracklab/fmriprep into…
mattcieslak May 10, 2019
1be1be1
Merge branch 'master' of https://github.com/poldracklab/fmriprep into…
mattcieslak May 11, 2019
c27d839
change mask name to ds_report_fmap_mask
mattcieslak May 11, 2019
4d371a1
replate type with suffix
mattcieslak May 12, 2019
9c5b46b
more changes of type to suffix
mattcieslak May 12, 2019
85fcb4e
Support negative numbers in phase images (happens at 7T)
mattcieslak May 13, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
"affiliation": "Department of Psychology, Columbia University",
"orcid": "0000-0001-7936-9991"
},
{
"name": "Cieslak, Matthew",
"affiliation": "Department of Neuropsychiatry, University of Pennsylvania",
"orcid": "0000-0002-1931-4734"
},
{
"name": "Poldrack, Russell A.",
"affiliation": "Department of Psychology, Stanford University",
Expand Down
10 changes: 10 additions & 0 deletions fmriprep/data/boilerplate.bib
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,16 @@ @article{hcppipelines
year = 2013
}

@article{pncprocessing,
title={Neuroimaging of the Philadelphia neurodevelopmental cohort},
author={Satterthwaite, Theodore D and Elliott, Mark A and Ruparel, Kosha and Loughead, James and Prabhakaran, Karthik and Calkins, Monica E and Hopson, Ryan and Jackson, Chad and Keefe, Jack and Riley, Marisa and others},
journal={Neuroimage},
volume={86},
pages={544--553},
year={2014},
publisher={Elsevier}
}

@article{fs_template,
author = {Reuter, Martin and Rosas, Herminia Diana and Fischl, Bruce},
doi = {10.1016/j.neuroimage.2010.07.020},
Expand Down
2 changes: 1 addition & 1 deletion fmriprep/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .surf import NormalizeSurf, GiftiNameSource, GiftiSetAnatomicalStructure
from .reports import SubjectSummary, FunctionalSummary, AboutSummary
from .utils import TPM2ROI, AddTPMs, AddTSVHeader, ConcatAffines, JoinTSVColumns
from .fmap import FieldEnhance, FieldToRadS, FieldToHz, Phasediff2Fieldmap
from .fmap import FieldEnhance, FieldToRadS, FieldToHz, Phasediff2Fieldmap, Phases2Fieldmap
from .confounds import GatherConfounds, ICAConfounds, FMRISummary
from .itk import MCFLIRT2ITK, MultiApplyTransforms
from .multiecho import FirstEcho
100 changes: 88 additions & 12 deletions fmriprep/interfaces/fmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from nipype.utils.filemanip import fname_presuffix
from nipype.interfaces.base import (
BaseInterfaceInputSpec, TraitedSpec, File, isdefined, traits,
SimpleInterface)
SimpleInterface, InputMultiObject)

LOGGER = logging.getLogger('nipype.interface')

Expand Down Expand Up @@ -116,11 +116,11 @@ def _run_interface(self, runtime):
if nslices > 1:
diffmapmsk = mask[..., errorslice[0]:errorslice[-1]]
diffmapnii = nb.Nifti1Image(
diffmap[..., errorslice[0]:errorslice[-1]] * diffmapmsk,
datanii.affine, datanii.header)
diffmap[..., errorslice[0]:errorslice[-1]] * diffmapmsk, datanii.affine,
datanii.header)

bspobj2 = fbsp.BSplineFieldmap(diffmapnii, knots_zooms=[24., 24., 4.],
njobs=self.inputs.num_threads)
bspobj2 = fbsp.BSplineFieldmap(
diffmapnii, knots_zooms=[24., 24., 4.], njobs=self.inputs.num_threads)
bspobj2.fit()
smoothed2 = bspobj2.get_smoothed().get_data()

Expand All @@ -129,8 +129,8 @@ def _run_interface(self, runtime):
else:
final = smoothed1.get_data()

nb.Nifti1Image(final, datanii.affine, datanii.header).to_filename(
self._results['out_file'])
nb.Nifti1Image(final, datanii.affine,
datanii.header).to_filename(self._results['out_file'])

return runtime

Expand Down Expand Up @@ -201,12 +201,38 @@ class Phasediff2Fieldmap(SimpleInterface):

def _run_interface(self, runtime):
self._results['out_file'] = phdiff2fmap(
self.inputs.in_file,
_delta_te(self.inputs.metadata),
self.inputs.in_file, _delta_te(self.inputs.metadata),
newpath=runtime.cwd)
return runtime


class Phases2FieldmapInputSpec(BaseInterfaceInputSpec):
phase_files = InputMultiObject(
File(exists=True), mandatory=True, desc='list of phase1, phase2 files')
metadatas = traits.List(
traits.Dict, mandatory=True, desc='list of phase1, phase2 metadata dicts')


class Phases2FieldmapOutputSpec(TraitedSpec):
out_file = File(desc='the output fieldmap')
phasediff_metadata = traits.Dict(desc='the phasediff metadata')


class Phases2Fieldmap(SimpleInterface):
"""
Convert a phase1, phase2 into a difference map
"""
input_spec = Phases2FieldmapInputSpec
output_spec = Phases2FieldmapOutputSpec

def _run_interface(self, runtime):
# Get the echo times
fmap_file, merged_metadata = phases2fmap(self.inputs.phase_files, self.inputs.metadatas)
self._results['phasediff_metadata'] = merged_metadata
self._results['out_file'] = fmap_file
return runtime


def _despike2d(data, thres, neigh=None):
"""
despiking as done in FSL fugue
Expand All @@ -233,8 +259,7 @@ def _despike2d(data, thres, neigh=None):
patch_range = vals.max() - vals.min()
patch_med = np.median(vals)

if (patch_range > 1e-6 and
(abs(thisval - patch_med) / patch_range) > thres):
if (patch_range > 1e-6 and (abs(thisval - patch_med) / patch_range) > thres):
data[i, j, k] = patch_med
return data

Expand Down Expand Up @@ -471,7 +496,7 @@ def phdiff2fmap(in_file, delta_te, newpath=None):

.. math::

\Delta B_0 (\text{T}^{-1}) = \frac{\Delta \Theta}{2\pi\gamma \Delta\text{TE}}
\Delta B_0 (\text{T}^{-1}) = \frac{\Delta \Theta}{\2\pi\gamma \Delta\text{TE}}


In this case, we do not take into account the gyromagnetic ratio of the
Expand All @@ -497,6 +522,57 @@ def phdiff2fmap(in_file, delta_te, newpath=None):
return out_file


def phases2fmap(phase_files, metadatas, newpath=None):
"""Calculates a phasediff from two phase images. Assumes monopolar
readout. """
import numpy as np
import nibabel as nb
from nipype.utils.filemanip import fname_presuffix
from copy import deepcopy

phasediff_file = fname_presuffix(phase_files[0], suffix='_phasediff', newpath=newpath)
echo_times = [meta.get("EchoTime") for meta in metadatas]
if None in echo_times or echo_times[0] == echo_times[1]:
raise RuntimeError()
# Determine the order of subtraction
short_echo_index = echo_times.index(min(echo_times))
long_echo_index = echo_times.index(max(echo_times))

short_phase_image = phase_files[short_echo_index]
long_phase_image = phase_files[long_echo_index]

image0 = nb.load(short_phase_image)
phase0 = image0.get_fdata()
image1 = nb.load(long_phase_image)
phase1 = image1.get_fdata()

def rescale_image(img):
imax = img.max()
imin = img.min()
scaled = 2 * ((img - imin) / (imax - imin) - 0.5)
return np.pi * scaled

# Calculate fieldmaps
rad0 = rescale_image(phase0)
rad1 = rescale_image(phase1)
a = np.cos(rad0)
b = np.sin(rad0)
c = np.cos(rad1)
d = np.sin(rad1)
fmap = -np.arctan2(b * c - a * d, a * c + b * d)

phasediff_nii = nb.Nifti1Image(fmap, image0.affine)
phasediff_nii.set_data_dtype(np.float32)
phasediff_nii.to_filename(phasediff_file)

merged_metadata = deepcopy(metadatas[0])
del merged_metadata['EchoTime']
merged_metadata['EchoTime1'] = float(echo_times[short_echo_index])
merged_metadata['EchoTime2'] = float(echo_times[long_echo_index])

return phasediff_file, merged_metadata


def _delta_te(in_values, te1=None, te2=None):
"""Read :math:`\Delta_\text{TE}` from BIDS metadata dict"""
if isinstance(in_values, float):
Expand Down
6 changes: 5 additions & 1 deletion fmriprep/workflows/bold/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,11 @@ def init_func_preproc_wf(bold_file, ignore, freesurfer,
if 'fieldmaps' not in ignore:
fmaps = layout.get_fieldmap(ref_file, return_list=True)
for fmap in fmaps:
fmap['metadata'] = layout.get_metadata(fmap[fmap['type']])
if fmap['type'] == 'phase':
fmap_key = 'phase1'
else:
fmap_key = fmap['type']
fmap['metadata'] = layout.get_metadata(fmap[fmap_key])

# Run SyN if forced or in the absence of fieldmap correction
if force_syn or (use_syn and not fmaps):
Expand Down
19 changes: 11 additions & 8 deletions fmriprep/workflows/fieldmap/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
'epi': 0,
'fieldmap': 1,
'phasediff': 2,
'syn': 3
'phase': 3,
'syn': 4
}
DEFAULT_MEMORY_MIN_GB = 0.01

Expand Down Expand Up @@ -193,7 +194,7 @@ def init_sdc_wf(fmaps, bold_meta, omp_nthreads=1,
])

# FIELDMAP path
if fmap['type'] in ['fieldmap', 'phasediff']:
if fmap['type'] in ['fieldmap', 'phasediff', 'phase']:
outputnode.inputs.method = 'FMB (%s-based)' % fmap['type']
# Import specific workflows here, so we don't break everything with one
# unused workflow.
Expand All @@ -206,15 +207,17 @@ def init_sdc_wf(fmaps, bold_meta, omp_nthreads=1,
fmap_estimator_wf.inputs.inputnode.fieldmap = fmap['fieldmap']
fmap_estimator_wf.inputs.inputnode.magnitude = fmap['magnitude']

if fmap['type'] == 'phasediff':
if fmap['type'] in ('phasediff', 'phase'):
from .phdiff import init_phdiff_wf
fmap_estimator_wf = init_phdiff_wf(omp_nthreads=omp_nthreads)
fmap_estimator_wf = init_phdiff_wf(omp_nthreads=omp_nthreads,
phasetype=fmap['type'])
# set inputs
fmap_estimator_wf.inputs.inputnode.phasediff = fmap['phasediff']
if fmap['type'] == 'phasediff':
fmap_estimator_wf.inputs.inputnode.phasediff = fmap['phasediff']
elif fmap['type'] == 'phase':
fmap_estimator_wf.inputs.inputnode.phasediff = [fmap['phase1'], fmap['phase2']]
fmap_estimator_wf.inputs.inputnode.magnitude = [
fmap_ for key, fmap_ in sorted(fmap.items())
if key.startswith("magnitude")
]
fmap_ for key, fmap_ in sorted(fmap.items()) if key.startswith("magnitude")]

sdc_unwarp_wf = init_sdc_unwarp_wf(
omp_nthreads=omp_nthreads,
Expand Down
49 changes: 34 additions & 15 deletions fmriprep/workflows/fieldmap/phdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

Fieldmap preprocessing workflow for fieldmap data structure
8.9.1 in BIDS 1.0.0: one phase diff and at least one magnitude image
8.9.2 in BIDS 1.0.0: two phase images and two magnitude images

"""

Expand All @@ -27,11 +28,11 @@
from ...engine import Workflow
from ...interfaces import (
ReadSidecarJSON, IntraModalMerge, DerivativesDataSink,
Phasediff2Fieldmap
Phasediff2Fieldmap, Phases2Fieldmap
)


def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
def init_phdiff_wf(omp_nthreads, phasetype="phasediff", name='phdiff_wf'):
"""
Estimates the fieldmap using a phase-difference image and one or more
magnitude images corresponding to two or more :abbr:`GRE (Gradient Echo sequence)`
Expand Down Expand Up @@ -70,12 +71,6 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
outputnode = pe.Node(niu.IdentityInterface(
fields=['fmap', 'fmap_ref', 'fmap_mask']), name='outputnode')

def _pick1st(inlist):
return inlist[0]

# Read phasediff echo times
meta = pe.Node(ReadSidecarJSON(), name='meta', mem_gb=0.01, run_without_submitting=True)

# Merge input magnitude images
magmrg = pe.Node(IntraModalMerge(), name='magmrg')

Expand All @@ -90,9 +85,6 @@ def _pick1st(inlist):
# dilate = pe.Node(fsl.maths.MathsCommand(
# nan2zeros=True, args='-kernel sphere 5 -dilM'), name='MskDilate')

# phase diff -> radians
pha2rads = pe.Node(niu.Function(function=siemens2rads), name='pha2rads')

# FSL PRELUDE will perform phase-unwrapping
prelude = pe.Node(fsl.PRELUDE(), name='prelude')

Expand All @@ -110,16 +102,44 @@ def _pick1st(inlist):
# pre_fugue = pe.Node(fsl.FUGUE(save_fmap=True), name='ComputeFieldmapFUGUE')
# rsec2hz (divide by 2pi)

if phasetype == "phasediff":
# phase diff -> radians
pha2rads = pe.Node(niu.Function(function=siemens2rads),
name='pha2rads')
# Read phasediff echo times
meta = pe.Node(ReadSidecarJSON(), name='meta', mem_gb=0.01,
run_without_submitting=True)
workflow.connect([
(meta, compfmap, [('out_dict', 'metadata')]),
(inputnode, pha2rads, [('phasediff', 'in_file')]),
(pha2rads, prelude, [('out', 'phase_file')]),
(inputnode, ds_fmap_mask, [('phasediff', 'source_file')]),
])

elif phasetype == "phase":
workflow.__desc__ += """\
The phase difference used for unwarping was calculated using a method developed
Copy link
Member

Choose a reason for hiding this comment

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

If I'm understanding this PR correctly, this implements a method described by Jezzard & Balaban in 1995 (https://www.ncbi.nlm.nih.gov/pubmed/7674900). Modern scanners generally provide the phase difference map, and for that reason, this was missing in fMRIPrep.

Code contributions are credited via the .zenodo.json file. Please update this workflow description to cover both "phasediff" and "phase" corrections, with the reference to Jezzard & Balaban (or please remove this modification to the boilerplate).

by Mark Elliott [@pncprocessing].
"""
# Special case for phase1, phase2 images
meta = pe.MapNode(ReadSidecarJSON(), name='meta', mem_gb=0.01,
run_without_submitting=True, iterfield=['in_file'])
phases2fmap = pe.Node(Phases2Fieldmap(), name='phases2fmap')
workflow.connect([
(meta, phases2fmap, [('out_dict', 'metadatas')]),
(inputnode, phases2fmap, [('phasediff', 'phase_files')]),
(phases2fmap, prelude, [('out_file', 'phase_file')]),
Copy link
Member

Choose a reason for hiding this comment

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

the difference map needs to be phase unwrapped? aren't the phase maps unwrapped individually?

Copy link
Member

Choose a reason for hiding this comment

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

Consider:

p1 = pi - eps
p2 = -pi + eps

p1 - p2 = 2pi - 2eps = -2eps

Phase differences should be between -pi and pi, as well.

Copy link
Member

Choose a reason for hiding this comment

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

That means you are clipping it again, correct?

Copy link
Member

Choose a reason for hiding this comment

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

Oh sorry, I think I misunderstood the context here. You're saying that prelude just does the clipping between -pi and pi, and we've already done that in phases2fmap?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I understood before that each phase map needs to be unwrapped (which makes sense to me). If they are phase-unwrapped, there's no point on running PRELUDE (Phase Region Expanding Labeller for Unwrapping Discrete Estimates).

Also, I am very curious as to how the individual phase maps are unwrapped, if not done with PRELUDE. I know there are better methods out there (e.g., using graph cuts) but they are rarely used in general.

Copy link
Member

Choose a reason for hiding this comment

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

Okay, I have confirmed this point: as per the FUGUE guide, each phase map should be unwrapped with PRELUDE separately (step 3) of the guide. Then they can be subtracted as they are not wrapped within [-pi, pi) anymore.

After that point, the pipeline should be exactly the same as for the phasediff type of fieldmap.

(phases2fmap, compfmap, [('phasediff_metadata', 'metadata')]),
(phases2fmap, ds_fmap_mask, [('out_file', 'source_file')])
])

workflow.connect([
(inputnode, meta, [('phasediff', 'in_file')]),
(inputnode, magmrg, [('magnitude', 'in_files')]),
(magmrg, n4, [('out_avg', 'input_image')]),
(n4, prelude, [('output_image', 'magnitude_file')]),
(n4, bet, [('output_image', 'in_file')]),
(bet, prelude, [('mask_file', 'mask_file')]),
(inputnode, pha2rads, [('phasediff', 'in_file')]),
(pha2rads, prelude, [('out', 'phase_file')]),
(meta, compfmap, [('out_dict', 'metadata')]),
(prelude, denoise, [('unwrapped_phase_file', 'in_file')]),
(denoise, demean, [('out_file', 'in_file')]),
(demean, cleanup_wf, [('out', 'inputnode.in_file')]),
Expand All @@ -128,7 +148,6 @@ def _pick1st(inlist):
(compfmap, outputnode, [('out_file', 'fmap')]),
(bet, outputnode, [('mask_file', 'fmap_mask'),
('out_file', 'fmap_ref')]),
(inputnode, ds_fmap_mask, [('phasediff', 'source_file')]),
(bet, ds_fmap_mask, [('out_report', 'in_file')]),
])

Expand Down