Skip to content

Commit 29f2d0b

Browse files
committed
ENH: Refactor fieldmap-unwarping flows, more homogeneous interface
This PR closes #19, reorganizing the unwarping tools for fieldmap-based solutions: - [x] Separate the displacements field generation from actual unwarping (i.e., applying the nonlinear transform). By doing this, we are not only addressing #19 directly, we are also readying the ground for tackling #21 since now all the unwarping paths (pepolar, fieldmaps/phases/phasediff, or syn) have more consistent interfaces. - [x] Update the root workflow to reflect these changes. - [x] Unloaded ``unwarp.py`` of the workflow to write reports, which has been moved to a new module called ``outputs.py`` following some of the latest *fMRIPrep* non-written standards. - [x] Polished some minor documentation and stylistic issues within the scope of the proposed changes.
1 parent 593b555 commit 29f2d0b

File tree

6 files changed

+270
-211
lines changed

6 files changed

+270
-211
lines changed

sdcflows/workflows/base.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
151151

152152
# FIELDMAP path
153153
elif 'fieldmap' in fmaps or 'phasediff' in fmaps:
154+
from .fmap import init_fmap2field_wf
154155
from .unwarp import init_sdc_unwarp_wf
155156

156157
# SyN works without this metadata
@@ -179,21 +180,28 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
179180
m for m, _ in fmaps['phasediff']['magnitude']]
180181
fmap_wf.inputs.inputnode.phasediff = fmaps['phasediff']['phases']
181182

183+
fmap2field_wf = init_fmap2field_wf(omp_nthreads=omp_nthreads, debug=debug)
184+
fmap2field_wf.inputs.inputnode.metadata = epi_meta
185+
182186
sdc_unwarp_wf = init_sdc_unwarp_wf(
183187
omp_nthreads=omp_nthreads,
184188
debug=debug,
185189
name='sdc_unwarp_wf')
186-
sdc_unwarp_wf.inputs.inputnode.metadata = epi_meta
187190

188191
workflow.connect([
192+
(inputnode, fmap2field_wf, [
193+
('epi_file', 'inputnode.in_reference'),
194+
('epi_brain', 'inputnode.in_reference_brain')]),
189195
(inputnode, sdc_unwarp_wf, [
190196
('epi_file', 'inputnode.in_reference'),
191-
('epi_brain', 'inputnode.in_reference_brain'),
192-
('epi_mask', 'inputnode.in_mask')]),
193-
(fmap_wf, sdc_unwarp_wf, [
197+
('epi_brain', 'inputnode.in_reference_brain')]),
198+
(fmap_wf, fmap2field_wf, [
194199
('outputnode.fmap', 'inputnode.fmap'),
195200
('outputnode.fmap_ref', 'inputnode.fmap_ref'),
196201
('outputnode.fmap_mask', 'inputnode.fmap_mask')]),
202+
(fmap2field_wf, sdc_unwarp_wf, [
203+
('outputnode.out_warp', 'inputnode.in_warp')]),
204+
197205
])
198206
elif not only_syn:
199207
raise ValueError('Fieldmaps of types %s are not supported' %

sdcflows/workflows/fmap.py

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,25 @@
88
Direct B0 mapping sequences
99
~~~~~~~~~~~~~~~~~~~~~~~~~~~
1010
When the fieldmap is directly measured with a prescribed sequence (such as
11-
:abbr:`SE (spiral echo)`), we only need to calculate the corresponding B-Spline
12-
coefficients to adapt the fieldmap to the TOPUP tool.
11+
:abbr:`SE (spiral echo)`), we only need to calculate the corresponding
12+
displacements field that accounts for the distortions.
1313
This procedure is described with more detail `here
1414
<https://cni.stanford.edu/wiki/GE_Processing#Fieldmaps>`__.
1515
1616
This corresponds to `this section of the BIDS specification
1717
<https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/01-magnetic-resonance-imaging-data.html#a-real-fieldmap-image>`__.
1818
1919
"""
20+
import pkg_resources as pkgr
2021

2122
from nipype.pipeline import engine as pe
2223
from nipype.interfaces import utility as niu, fsl
2324
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
25+
from niworkflows.interfaces import itk
2426
from niworkflows.interfaces.images import IntraModalMerge
27+
from niworkflows.interfaces.registration import ANTSApplyTransformsRPT, ANTSRegistrationRPT
2528

29+
from ..interfaces.fmap import get_ees as _get_ees, FieldToRadS
2630
from .gre import init_fmap_postproc_wf, init_magnitude_wf
2731

2832

@@ -71,6 +75,10 @@ def init_fmap_wf(omp_nthreads, fmap_bspline, name='fmap_wf'):
7175
7276
"""
7377
workflow = Workflow(name=name)
78+
workflow.__desc__ = """\
79+
A B0-nonuniformity map (or *fieldmap*) was directly measured with an MRI scheme
80+
designed with that purpose (typically, a spiral pulse sequence).
81+
"""
7482
inputnode = pe.Node(niu.IdentityInterface(
7583
fields=['magnitude', 'fieldmap']), name='inputnode')
7684
outputnode = pe.Node(niu.IdentityInterface(fields=['fmap', 'fmap_ref', 'fmap_mask']),
@@ -101,3 +109,138 @@ def init_fmap_wf(omp_nthreads, fmap_bspline, name='fmap_wf'):
101109
(fmap_postproc_wf, outputnode, [('outputnode.out_fmap', 'fmap')]),
102110
])
103111
return workflow
112+
113+
114+
def init_fmap2field_wf(omp_nthreads, debug, name='fmap2field_wf'):
115+
"""
116+
Convert the estimated fieldmap in Hz into a displacements field.
117+
118+
This workflow takes in a fieldmap and calculates the corresponding
119+
displacements field (in other words, an ANTs-compatible warp file).
120+
121+
Workflow Graph
122+
.. workflow ::
123+
:graph2use: orig
124+
:simple_form: yes
125+
126+
from sdcflows.workflows.fmap import init_fmap2field_wf
127+
wf = init_fmap2field_wf(omp_nthreads=8,
128+
debug=False)
129+
130+
Parameters
131+
----------
132+
omp_nthreads : int
133+
Maximum number of threads an individual process may use.
134+
debug : bool
135+
Run fast configurations of registrations.
136+
name : str
137+
Unique name of this workflow.
138+
139+
Inputs
140+
------
141+
in_reference
142+
the reference image
143+
in_reference_brain
144+
the reference image (skull-stripped)
145+
metadata
146+
metadata associated to the ``in_reference`` EPI input
147+
fmap
148+
the fieldmap in Hz
149+
fmap_ref
150+
the reference (anatomical) image corresponding to ``fmap``
151+
fmap_mask
152+
a brain mask corresponding to ``fmap``
153+
154+
155+
Outputs
156+
-------
157+
out_reference
158+
the ``in_reference`` after unwarping
159+
out_reference_brain
160+
the ``in_reference`` after unwarping and skullstripping
161+
out_warp
162+
the corresponding :abbr:`DFM (displacements field map)` compatible with
163+
ANTs
164+
out_jacobian
165+
the jacobian of the field (for drop-out alleviation)
166+
out_mask
167+
mask of the unwarped input file
168+
169+
"""
170+
workflow = Workflow(name=name)
171+
workflow.__desc__ = """\
172+
The *fieldmap* was then co-registered to the target EPI (echo-planar imaging)
173+
reference run and converted to a displacements field map (amenable to registration
174+
tools such as ANTs) with FSL's `fugue` and other *SDCflows* tools.
175+
"""
176+
inputnode = pe.Node(niu.IdentityInterface(
177+
fields=['in_reference', 'in_reference_brain', 'metadata',
178+
'fmap_ref', 'fmap_mask', 'fmap']), name='inputnode')
179+
outputnode = pe.Node(niu.IdentityInterface(
180+
fields=['out_warp']), name='outputnode')
181+
182+
# Register the reference of the fieldmap to the reference
183+
# of the target image (the one that shall be corrected)
184+
ants_settings = pkgr.resource_filename('sdcflows', 'data/fmap-any_registration.json')
185+
if debug:
186+
ants_settings = pkgr.resource_filename(
187+
'sdcflows', 'data/fmap-any_registration_testing.json')
188+
189+
fmap2ref_reg = pe.Node(
190+
ANTSRegistrationRPT(generate_report=True, from_file=ants_settings,
191+
output_inverse_warped_image=True, output_warped_image=True),
192+
name='fmap2ref_reg', n_procs=omp_nthreads)
193+
194+
# Map the VSM into the EPI space
195+
fmap2ref_apply = pe.Node(ANTSApplyTransformsRPT(
196+
generate_report=True, dimension=3, interpolation='BSpline', float=True),
197+
name='fmap2ref_apply')
198+
199+
fmap_mask2ref_apply = pe.Node(ANTSApplyTransformsRPT(
200+
generate_report=False, dimension=3, interpolation='MultiLabel',
201+
float=True),
202+
name='fmap_mask2ref_apply')
203+
204+
# Fieldmap to rads and then to voxels (VSM - voxel shift map)
205+
torads = pe.Node(FieldToRadS(fmap_range=0.5), name='torads')
206+
207+
get_ees = pe.Node(niu.Function(function=_get_ees, output_names=['ees']), name='get_ees')
208+
209+
gen_vsm = pe.Node(fsl.FUGUE(save_unmasked_shift=True), name='gen_vsm')
210+
# Convert the VSM into a DFM (displacements field map)
211+
# or: FUGUE shift to ANTS warping.
212+
vsm2dfm = pe.Node(itk.FUGUEvsm2ANTSwarp(), name='vsm2dfm')
213+
214+
workflow.connect([
215+
(inputnode, fmap2ref_reg, [('fmap_ref', 'moving_image'),
216+
('in_reference_brain', 'fixed_image')]),
217+
(inputnode, fmap2ref_apply, [('fmap', 'input_image'),
218+
('in_reference', 'reference_image')]),
219+
(inputnode, fmap_mask2ref_apply, [('in_reference', 'reference_image'),
220+
('fmap_mask', 'input_image')]),
221+
(inputnode, get_ees, [('in_reference', 'in_file'),
222+
('metadata', 'in_meta')]),
223+
(inputnode, gen_vsm, [(('metadata', _get_pedir_fugue), 'unwarp_direction')]),
224+
(inputnode, vsm2dfm, [(('metadata', _get_pedir_bids), 'pe_dir')]),
225+
(fmap2ref_reg, fmap2ref_apply, [
226+
('composite_transform', 'transforms')]),
227+
(fmap2ref_reg, fmap_mask2ref_apply, [
228+
('composite_transform', 'transforms')]),
229+
(fmap2ref_apply, torads, [('output_image', 'in_file')]),
230+
(fmap_mask2ref_apply, gen_vsm, [('output_image', 'mask_file')]),
231+
(get_ees, gen_vsm, [('ees', 'dwell_time')]),
232+
(gen_vsm, vsm2dfm, [('shift_out_file', 'in_file')]),
233+
(torads, gen_vsm, [('out_file', 'fmap_in_file')]),
234+
(vsm2dfm, outputnode, [('out_file', 'out_warp')]),
235+
])
236+
return workflow
237+
238+
239+
# Helper functions
240+
# ------------------------------------------------------------
241+
def _get_pedir_bids(in_dict):
242+
return in_dict['PhaseEncodingDirection']
243+
244+
245+
def _get_pedir_fugue(in_dict):
246+
return in_dict['PhaseEncodingDirection'].replace('i', 'x').replace('j', 'y').replace('k', 'z')

sdcflows/workflows/outputs.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Writing out outputs."""
4+
from nipype.pipeline import engine as pe
5+
from nipype.interfaces import utility as niu
6+
from niworkflows.interfaces.bids import DerivativesDataSink
7+
8+
9+
def init_sdc_unwarp_report_wf(name='sdc_unwarp_report_wf', forcedsyn=False):
10+
"""
11+
Save a reportlet showing how SDC unwarping performed.
12+
13+
This workflow generates and saves a reportlet showing the effect of fieldmap
14+
unwarping a BOLD image.
15+
16+
.. workflow::
17+
:graph2use: orig
18+
:simple_form: yes
19+
20+
from sdcflows.workflows.outputs import init_sdc_unwarp_report_wf
21+
wf = init_sdc_unwarp_report_wf()
22+
23+
Parameters
24+
----------
25+
name : str, optional
26+
Workflow name (default: ``sdc_unwarp_report_wf``)
27+
forcedsyn : bool, optional
28+
Whether SyN-SDC was forced.
29+
30+
Inputs
31+
------
32+
in_pre
33+
Reference image, before unwarping
34+
in_post
35+
Reference image, after unwarping
36+
in_seg
37+
Segmentation of preprocessed structural image, including
38+
gray-matter (GM), white-matter (WM) and cerebrospinal fluid (CSF)
39+
in_xfm
40+
Affine transform from T1 space to BOLD space (ITK format)
41+
42+
"""
43+
from niworkflows.interfaces import SimpleBeforeAfter
44+
from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms
45+
from niworkflows.interfaces.images import extract_wm
46+
47+
DEFAULT_MEMORY_MIN_GB = 0.01
48+
49+
workflow = pe.Workflow(name=name)
50+
51+
inputnode = pe.Node(niu.IdentityInterface(
52+
fields=['in_pre', 'in_post', 'in_seg', 'in_xfm']), name='inputnode')
53+
54+
map_seg = pe.Node(ApplyTransforms(
55+
dimension=3, float=True, interpolation='MultiLabel'),
56+
name='map_seg', mem_gb=0.3)
57+
58+
sel_wm = pe.Node(niu.Function(function=extract_wm), name='sel_wm',
59+
mem_gb=DEFAULT_MEMORY_MIN_GB)
60+
61+
bold_rpt = pe.Node(SimpleBeforeAfter(), name='bold_rpt',
62+
mem_gb=0.1)
63+
ds_report_sdc = pe.Node(
64+
DerivativesDataSink(desc='sdc' if not forcedsyn else 'forcedsyn',
65+
suffix='bold'), name='ds_report_sdc',
66+
mem_gb=DEFAULT_MEMORY_MIN_GB, run_without_submitting=True
67+
)
68+
69+
workflow.connect([
70+
(inputnode, bold_rpt, [('in_post', 'after'),
71+
('in_pre', 'before')]),
72+
(bold_rpt, ds_report_sdc, [('out_report', 'in_file')]),
73+
(inputnode, map_seg, [('in_post', 'reference_image'),
74+
('in_seg', 'input_image'),
75+
('in_xfm', 'transforms')]),
76+
(map_seg, sel_wm, [('output_image', 'in_seg')]),
77+
(sel_wm, bold_rpt, [('out', 'wm_seg')]),
78+
])
79+
80+
return workflow

sdcflows/workflows/pepolar.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ def init_pepolar_unwarp_wf(omp_nthreads=1, matched_pe=False,
103103
"""
104104
workflow = Workflow(name=name)
105105
workflow.__desc__ = """\
106-
A deformation field to correct for susceptibility distortions was estimated
107-
based on two echo-planar imaging (EPI) references with opposing phase-encoding
108-
directions, using `3dQwarp` @afni (AFNI {afni_ver}).
106+
A B0-nonuniformity map (or *fieldmap*) was estimated based on two (or more)
107+
echo-planar imaging (EPI) references with opposing phase-encoding
108+
directions, with `3dQwarp` @afni (AFNI {afni_ver}).
109109
""".format(afni_ver=''.join(['%02d' % v for v in afni.Info().version() or []]))
110110

111111
inputnode = pe.Node(niu.IdentityInterface(

sdcflows/workflows/phdiff.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
3333
magnitude images corresponding to two or more :abbr:`GRE (Gradient Echo sequence)`
3434
acquisitions.
3535
The most delicate bit of this workflow is the phase-unwrapping process: phase maps
36-
are clipped in the range :math:`[0 \dotsb 2 \cdot \pi )`.
36+
are clipped in the range :math:`[0 \dotsb 2\pi )`.
3737
To find the integer number of offsets that make a region continously smooth with
3838
its neighbour, FSL PRELUDE is run [Jenkinson2003]_.
3939
FSL PRELUDE takes wrapped maps in the range 0 to 6.28, `as per the user guide
@@ -83,11 +83,11 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
8383
"""
8484
workflow = Workflow(name=name)
8585
workflow.__desc__ = """\
86-
A deformation field to correct for susceptibility distortions was estimated
87-
based on a field map that was co-registered to the EPI (echo-planar imaging) reference
88-
run, using a custom workflow of *SDCFlows* derived from D. Greve's `epidewarp.fsl`
89-
[script](http://www.nmr.mgh.harvard.edu/~greve/fbirn/b0/epidewarp.fsl) and
90-
further improvements of HCP Pipelines [@hcppipelines].
86+
A B0-nonuniformity map (or *fieldmap*) was estimated based on a phase-difference map
87+
calculated with a dual-echo GRE (gradient-recall echo) sequence, processed with a
88+
custom workflow of *SDCFlows* inspired by the
89+
[`epidewarp.fsl` script](http://www.nmr.mgh.harvard.edu/~greve/fbirn/b0/epidewarp.fsl)
90+
and further improvements in HCP Pipelines [@hcppipelines].
9191
"""
9292

9393
inputnode = pe.Node(niu.IdentityInterface(fields=['magnitude', 'phasediff']),

0 commit comments

Comments
 (0)