Skip to content

Commit bb08171

Browse files
committed
ENH: Base implementation for phase1/2 fieldmaps
Putting together the lessons learned in nipreps#30, leveraging nipreps#52 and nipreps#53 (unfolded from nipreps#30 too), and utilizing nipreps#50 and nipreps#51, this workflow adds the phase difference map calculation, considering it one use-case of the general phase-difference fieldmap workflow. On top of this PR, we can continue the discussions held in nipreps#30. Probably, we will want to address nipreps#23 the first - the magnitude segmentation is sometimes really bad (e.g. see the phase1/2 unit test). Another discussion arisen in nipreps#30 is the spatial smoothing of the fieldmap (nipreps#22). Finally, the plan is to revise this implementation and determine whether the subtraction should happen before or after PRELUDE, and whether the arctan2 route is more interesting.
1 parent 714e3c5 commit bb08171

File tree

3 files changed

+130
-6
lines changed

3 files changed

+130
-6
lines changed

sdcflows/interfaces/fmap.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,43 @@
2424
LOGGER = logging.getLogger('nipype.interface')
2525

2626

27+
class _SubtractPhasesInputSpec(BaseInterfaceInputSpec):
28+
in_phases = traits.List(File(exists=True), min=1, max=2,
29+
desc='input phase maps')
30+
in_meta = traits.List(traits.Dict(), min=1, max=2,
31+
desc='metadata corresponding to the inputs')
32+
33+
34+
class _SubtractPhasesOutputSpec(TraitedSpec):
35+
phase_diff = File(exists=True, desc='phase difference map')
36+
metadata = traits.Dict(desc='output metadata')
37+
38+
39+
class SubtractPhases(SimpleInterface):
40+
"""Calculate a phase difference map."""
41+
42+
input_spec = _SubtractPhasesInputSpec
43+
output_spec = _SubtractPhasesOutputSpec
44+
45+
def _run_interface(self, runtime):
46+
if len(self.inputs.in_phases) != len(self.inputs.in_meta):
47+
raise ValueError(
48+
'Length of input phase-difference maps and metadata files '
49+
'should match.')
50+
51+
if len(self.inputs.in_phases) == 1:
52+
self._results['phase_diff'] = self.inputs.in_phases[0]
53+
self._results['metadata'] = self.inputs.in_meta[0]
54+
return runtime
55+
56+
self._results['phase_diff'], self._results['metadata'] = \
57+
_subtract_phases(self.inputs.in_phases,
58+
self.inputs.in_meta,
59+
newpath=runtime.cwd)
60+
61+
return runtime
62+
63+
2764
class _FieldEnhanceInputSpec(BaseInterfaceInputSpec):
2865
in_file = File(exists=True, mandatory=True, desc='input fieldmap')
2966
in_mask = File(exists=True, desc='brain mask')
@@ -647,3 +684,36 @@ def au2rads(in_file, newpath=None):
647684
out_file = fname_presuffix(in_file, suffix='_rads', newpath=newpath)
648685
nb.Nifti1Image(data, im.affine, hdr).to_filename(out_file)
649686
return out_file
687+
688+
689+
def _subtract_phases(in_phases, in_meta, newpath=None):
690+
# Discard traits with copy(), so that pop() works.
691+
in_meta = (in_meta[0].copy(), in_meta[1].copy())
692+
echo_times = tuple([m.pop('EchoTime', None) for m in in_meta])
693+
if not all(echo_times):
694+
raise ValueError(
695+
'One or more missing EchoTime metadata parameter '
696+
'associated to one or more phase map(s).')
697+
698+
if echo_times[0] > echo_times[1]:
699+
in_phases = (in_phases[1], in_phases[0])
700+
in_meta = (in_meta[1], in_meta[0])
701+
echo_times = (echo_times[1], echo_times[0])
702+
703+
in_phases_nii = [nb.load(ph) for ph in in_phases]
704+
sub_data = in_phases_nii[1].get_fdata(dtype='float32') - \
705+
in_phases_nii[0].get_fdata(dtype='float32')
706+
707+
new_meta = in_meta[1].copy()
708+
new_meta.update(in_meta[0])
709+
new_meta['EchoTime1'] = echo_times[0]
710+
new_meta['EchoTime2'] = echo_times[1]
711+
712+
hdr = in_phases_nii[0].header.copy()
713+
hdr.set_data_dtype(np.float32)
714+
hdr.set_xyzt_units('mm')
715+
nii = nb.Nifti1Image(sub_data, in_phases_nii[0].affine, hdr)
716+
out_phdiff = fname_presuffix(in_phases[0], suffix='_phdiff',
717+
newpath=newpath)
718+
nii.to_filename(out_phdiff)
719+
return out_phdiff, new_meta

sdcflows/workflows/phdiff.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from nipype.pipeline import engine as pe
2222
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
2323

24-
from ..interfaces.fmap import Phasediff2Fieldmap, PhaseMap2rads
24+
from ..interfaces.fmap import Phasediff2Fieldmap, PhaseMap2rads, SubtractPhases
2525
from .gre import init_fmap_postproc_wf, init_magnitude_wf
2626

2727

@@ -107,6 +107,12 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
107107
# FSL PRELUDE will perform phase-unwrapping
108108
prelude = pe.MapNode(fsl.PRELUDE(), iterfield=['phase_file'], name='prelude')
109109

110+
calc_phdiff = pe.Node(SubtractPhases(), name='calc_phdiff',
111+
run_without_submitting=True)
112+
113+
workflow.connect([
114+
])
115+
110116
fmap_postproc_wf = init_fmap_postproc_wf(omp_nthreads=omp_nthreads,
111117
fmap_bspline=False)
112118
compfmap = pe.Node(Phasediff2Fieldmap(), name='compfmap')
@@ -118,8 +124,11 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
118124
('outputnode.fmap_mask', 'mask_file')]),
119125
(split, phmap2rads, [('map_file', 'in_file')]),
120126
(phmap2rads, prelude, [('out_file', 'phase_file')]),
121-
(split, fmap_postproc_wf, [('meta', 'inputnode.metadata')]),
122-
(prelude, fmap_postproc_wf, [('unwrapped_phase_file', 'inputnode.fmap')]),
127+
(prelude, calc_phdiff, [('unwrapped_phase_file', 'in_phases')]),
128+
(split, calc_phdiff, [('meta', 'in_meta')]),
129+
(calc_phdiff, fmap_postproc_wf, [
130+
('phase_diff', 'inputnode.fmap'),
131+
('metadata', 'inputnode.metadata')]),
123132
(magnitude_wf, fmap_postproc_wf, [
124133
('outputnode.fmap_mask', 'inputnode.fmap_mask'),
125134
('outputnode.fmap_ref', 'inputnode.fmap_ref')]),

sdcflows/workflows/tests/test_phdiff.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
'ds001600',
1111
'testdata',
1212
])
13-
def test_workflow(bids_layouts, tmpdir, output_path, dataset, workdir):
13+
def test_phdiff(bids_layouts, tmpdir, output_path, dataset, workdir):
1414
"""Test creation of the workflow."""
1515
tmpdir.chdir()
1616

@@ -19,11 +19,56 @@ def test_workflow(bids_layouts, tmpdir, output_path, dataset, workdir):
1919
phdiff_wf = init_phdiff_wf(omp_nthreads=2)
2020
phdiff_wf.inputs.inputnode.magnitude = data.get(
2121
suffix=['magnitude1', 'magnitude2'],
22-
acq='v4',
22+
acquisition='v4',
2323
return_type='file',
2424
extension=['.nii', '.nii.gz'])
2525

26-
phdiff_files = data.get(suffix='phasediff', acq='v4',
26+
phdiff_files = data.get(suffix='phasediff', acquisition='v4',
27+
extension=['.nii', '.nii.gz'])
28+
29+
phdiff_wf.inputs.inputnode.phasediff = [
30+
(ph.path, ph.get_metadata()) for ph in phdiff_files]
31+
32+
if output_path:
33+
from ...interfaces.reportlets import FieldmapReportlet
34+
rep = pe.Node(FieldmapReportlet(reference_label='Magnitude'), 'simple_report')
35+
rep.interface._always_run = True
36+
37+
dsink = pe.Node(DerivativesDataSink(
38+
base_directory=str(output_path), keep_dtype=True), name='dsink')
39+
dsink.interface.out_path_base = 'sdcflows'
40+
dsink.inputs.source_file = phdiff_files[0].path
41+
42+
wf.connect([
43+
(phdiff_wf, rep, [
44+
('outputnode.fmap', 'fieldmap'),
45+
('outputnode.fmap_ref', 'reference'),
46+
('outputnode.fmap_mask', 'mask')]),
47+
(rep, dsink, [('out_report', 'in_file')]),
48+
])
49+
else:
50+
wf.add_nodes([phdiff_wf])
51+
52+
if workdir:
53+
wf.base_dir = str(workdir)
54+
55+
wf.run()
56+
57+
58+
def test_phases(bids_layouts, tmpdir, output_path, workdir):
59+
"""Test creation of the workflow."""
60+
tmpdir.chdir()
61+
62+
data = bids_layouts['ds001600']
63+
wf = Workflow(name='phases_ds001600')
64+
phdiff_wf = init_phdiff_wf(omp_nthreads=2)
65+
phdiff_wf.inputs.inputnode.magnitude = data.get(
66+
suffix=['magnitude1', 'magnitude2'],
67+
acquisition='v2',
68+
return_type='file',
69+
extension=['.nii', '.nii.gz'])
70+
71+
phdiff_files = data.get(suffix=['phase1', 'phase2'], acquisition='v2',
2772
extension=['.nii', '.nii.gz'])
2873

2974
phdiff_wf.inputs.inputnode.phasediff = [

0 commit comments

Comments
 (0)