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 all 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 @@ -106,6 +106,11 @@
"affiliation": "University College London",
"orcid": "0000-0002-9699-9052"
},
{
"name": "Cieslak, Matthew",
"affiliation": "Department of Neuropsychiatry, University of Pennsylvania",
"orcid": "0000-0002-1931-4734"
mattcieslak marked this conversation as resolved.
Show resolved Hide resolved
},
{
"name": "Halchenko, Yaroslav O.",
"affiliation": "Dartmouth College: Hanover, NH, United States",
Expand Down
10 changes: 10 additions & 0 deletions fmriprep/data/boilerplate.bib
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,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 @@ -6,7 +6,7 @@
bids, cifti, freesurfer, images, itk, surf, utils)

from .reports import SubjectSummary, FunctionalSummary, AboutSummary
from .fmap import FieldEnhance, FieldToRadS, FieldToHz, Phasediff2Fieldmap
from .fmap import FieldEnhance, FieldToRadS, FieldToHz, Phasediff2Fieldmap, Phases2Fieldmap
from .confounds import GatherConfounds, ICAConfounds, FMRISummary
from .multiecho import T2SMap

Expand Down
93 changes: 91 additions & 2 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 @@ -207,6 +207,34 @@ def _run_interface(self, runtime):
return runtime
mattcieslak marked this conversation as resolved.
Show resolved Hide resolved


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,
newpath=runtime.cwd)
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 Down Expand Up @@ -497,8 +525,69 @@ 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):
if np.any(img < -128):
# This happens sometimes on 7T fieldmaps
LOGGER.info("Found negative values in phase image: rescaling")
imax = img.max()
imin = img.min()
scaled = 2 * ((img - imin) / (imax - imin) - 0.5)
return np.pi * scaled
mask = img > 0
imax = img.max()
imin = img.min()
max_check = imax - 4096
if np.abs(max_check) > 10 or np.abs(imin) > 10:
LOGGER.warning("Phase image may be scaled incorrectly: check results")
return mask * (img / 2048 * np.pi - np.pi)

# 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"""
r"""Read :math:`\Delta_\text{TE}` from BIDS metadata dict"""
if isinstance(in_values, float):
te2 = in_values
te1 = 0.
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 @@ -336,7 +336,11 @@ def init_func_preproc_wf(
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['suffix']])
if fmap['suffix'] == 'phase':
Copy link
Member

Choose a reason for hiding this comment

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

I don't think there exists such a suffix in BIDS. This works because fmap_key gets renamed to phase1 either way (not sure about how then it digests the phase2 suffix, I'll check below).

fmap_key = 'phase1'
else:
fmap_key = fmap['suffix']
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
30 changes: 25 additions & 5 deletions fmriprep/workflows/fieldmap/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
'epi': 0,
'fieldmap': 1,
'phasediff': 2,
'syn': 3
'phase': 3,
'syn': 4
}
DEFAULT_MEMORY_MIN_GB = 0.01

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

# FIELDMAP path
if fmap['suffix'] in ['fieldmap', 'phasediff']:
if fmap['suffix'] in ['fieldmap', 'phasediff', 'phase']:
Copy link
Member

Choose a reason for hiding this comment

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

Along the lines of the previous comment.

Suggested change
if fmap['suffix'] in ['fieldmap', 'phasediff', 'phase']:
if fmap['suffix'] in ['fieldmap', 'phasediff', 'phase1']:

outputnode.inputs.method = 'FMB (%s-based)' % fmap['suffix']
# Import specific workflows here, so we don't break everything with one
# unused workflow.
Expand All @@ -212,11 +213,30 @@ 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['suffix'] == 'phasediff':
if fmap['suffix'] 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['suffix'])
# set inputs
fmap_estimator_wf.inputs.inputnode.phasediff = fmap['phasediff']
if fmap['suffix'] == 'phasediff':
fmap_estimator_wf.inputs.inputnode.phasediff = fmap['phasediff']
elif fmap['suffix'] == 'phase':
# Check that fieldmap is not bipolar
fmap_polarity = fmap['metadata'].get('DiffusionScheme', None)
if fmap_polarity == 'Bipolar':
LOGGER.warning("Bipolar fieldmaps are not supported. Ignoring")
workflow.__postdesc__ = ""
outputnode.inputs.method = 'None'
workflow.connect([
(inputnode, outputnode, [('bold_ref', 'bold_ref'),
('bold_mask', 'bold_mask'),
('bold_ref_brain', 'bold_ref_brain')]),
])
return workflow
if fmap_polarity is None:
LOGGER.warning("Assuming phase images are Monopolar")

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")
Expand Down
52 changes: 37 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 phases and at least one magnitude image

"""

Expand All @@ -27,10 +28,10 @@
from niworkflows.interfaces.images import IntraModalMerge
from niworkflows.interfaces.masks import BETRPT

from ...interfaces import Phasediff2Fieldmap, DerivativesDataSink
from ...interfaces import Phasediff2Fieldmap, Phases2Fieldmap, DerivativesDataSink


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 @@ -69,12 +70,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(bids_validate=False), name='meta', mem_gb=0.01)

# 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,47 @@ def _pick1st(inlist):
# pre_fugue = pe.Node(fsl.FUGUE(save_fmap=True), name='ComputeFieldmapFUGUE')
# rsec2hz (divide by 2pi)

if phasetype == "phasediff":
# Read phasediff echo times
meta = pe.Node(ReadSidecarJSON(bids_validate=False), name='meta', mem_gb=0.01)

# 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_report_fmap_mask, [('phasediff', 'source_file')]),
])

elif phasetype == "phase":
workflow.__desc__ += """\
The phase difference used for unwarping was calculated using two separate phase measurements
[@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_report_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 +151,6 @@ def _pick1st(inlist):
(compfmap, outputnode, [('out_file', 'fmap')]),
(bet, outputnode, [('mask_file', 'fmap_mask'),
('out_file', 'fmap_ref')]),
(inputnode, ds_report_fmap_mask, [('phasediff', 'source_file')]),
(bet, ds_report_fmap_mask, [('out_report', 'in_file')]),
])

Expand Down