Skip to content

Commit 1bd8274

Browse files
committed
ENH: Refactor the distortion estimation workflow
Following nipreps#40, rolls back the changes most involved with PyBIDS also making the API evolve less abruptly. The PR builds on top of nipreps#52, and closes nipreps#16. This PR does not address #20, nor nipreps#19 and nipreps#21 although it is setting the necessary stepping stones.
1 parent b124ad0 commit 1bd8274

File tree

3 files changed

+163
-175
lines changed

3 files changed

+163
-175
lines changed

sdcflows/workflows/base.py

Lines changed: 112 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
"""SDC workflows coordination."""
4-
from collections import defaultdict
5-
64
from nipype.pipeline import engine as pe
75
from nipype.interfaces import utility as niu
86
from nipype import logging
97

108
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
119

12-
# Fieldmap workflows
13-
from .pepolar import init_pepolar_unwarp_wf
1410

1511
LOGGER = logging.getLogger('nipype.workflow')
1612
FMAP_PRIORITY = {
@@ -23,12 +19,12 @@
2319
DEFAULT_MEMORY_MIN_GB = 0.01
2420

2521

26-
def init_sdc_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=None):
22+
def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=None):
2723
"""
2824
Build a :abbr:`SDC (susceptibility distortion correction)` workflow.
2925
30-
This workflow implements the heuristics to choose a
31-
:abbr:`SDC (susceptibility distortion correction)` strategy.
26+
This workflow implements the heuristics to choose an estimation
27+
methodology for :abbr:`SDC (susceptibility distortion correction)`.
3228
When no field map information is present within the BIDS inputs,
3329
the EXPERIMENTAL "fieldmap-less SyN" can be performed, using
3430
the ``--use-syn`` argument. When ``--force-syn`` is specified,
@@ -41,10 +37,14 @@ def init_sdc_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=None):
4137
4238
Parameters
4339
----------
44-
epi_file : pybids.BIDSFile
45-
A BIDSFile object representing an distorted image acquired
46-
with an :abbr:`EPI (echo-planar imaging)` MR scheme
47-
(i.e., suffix ``bold``, ``sbref`` or ``dwi``).
40+
fmaps : list of pybids dicts
41+
A list of dictionaries with the available fieldmaps
42+
(and their metadata using the key ``'metadata'`` for the
43+
case of :abbr:`PEPOLAR (Phase-Encoding POLARity)` fieldmaps).
44+
epi_meta : dict
45+
BIDS metadata dictionary corresponding to the
46+
:abbr:`EPI (echo-planar imaging)` run (i.e., suffix ``bold``,
47+
``sbref``, or ``dwi``) for which the fieldmap is being estimated.
4848
omp_nthreads : int
4949
Maximum number of threads an individual process may use
5050
debug : bool
@@ -54,30 +54,23 @@ def init_sdc_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=None):
5454
------
5555
epi_file
5656
A reference image calculated at a previous stage
57-
ref_brain
57+
epi_brain
5858
Same as above, but brain-masked
59-
ref_mask
59+
epi_mask
6060
Brain mask for the run
61-
t1_brain
61+
t1w_brain
6262
T1w image, brain-masked, for the fieldmap-less SyN method
6363
std2anat_xfm
64-
List of standard-to-T1w transforms generated during spatial
64+
Standard-to-T1w transform generated during spatial
6565
normalization (only for the fieldmap-less SyN method).
66-
template : str
67-
Name of template from which prior knowledge will be mapped
68-
into the subject's T1w reference
69-
(only for the fieldmap-less SyN method)
70-
templates : str
71-
Name of templates that index the ``std2anat_xfm`` input list
72-
(only for the fieldmap-less SyN method).
7366
7467
Outputs
7568
-------
7669
epi_file
7770
An unwarped BOLD reference
78-
bold_mask
71+
epi_mask
7972
The corresponding new mask after unwarping
80-
bold_ref_brain
73+
epi_brain
8174
Brain-extracted, unwarped BOLD reference
8275
out_warp
8376
The deformation field to unwarp the susceptibility distortions
@@ -95,14 +88,13 @@ def init_sdc_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=None):
9588
# TODO: To be removed (filter out unsupported fieldmaps):
9689
fmaps = [fmap for fmap in fmaps if fmap['suffix'] in FMAP_PRIORITY]
9790

98-
workflow = Workflow(name='sdc_wf' if fmaps else 'sdc_bypass_wf')
91+
workflow = Workflow(name='sdc_estimate_wf' if fmaps else 'sdc_bypass_wf')
9992
inputnode = pe.Node(niu.IdentityInterface(
100-
fields=['epi_file', 'ref_brain', 'ref_mask',
101-
't1_brain', 'std2anat_xfm', 'template', 'templates']),
93+
fields=['epi_file', 'epi_brain', 'epi_mask', 't1w_brain', 'std2anat_xfm']),
10294
name='inputnode')
10395

10496
outputnode = pe.Node(niu.IdentityInterface(
105-
fields=['output_ref', 'ref_mask', 'ref_brain',
97+
fields=['output_ref', 'epi_mask', 'epi_brain',
10698
'out_warp', 'syn_ref', 'method']),
10799
name='outputnode')
108100

@@ -116,8 +108,8 @@ def init_sdc_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=None):
116108
outputnode.inputs.method = 'None'
117109
workflow.connect([
118110
(inputnode, outputnode, [('epi_file', 'output_ref'),
119-
('ref_mask', 'ref_mask'),
120-
('ref_brain', 'ref_brain')]),
111+
('epi_mask', 'epi_mask'),
112+
('epi_brain', 'epi_brain')]),
121113
])
122114
return workflow
123115

@@ -127,109 +119,113 @@ def init_sdc_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=None):
127119
accurate co-registration with the anatomical reference.
128120
"""
129121

122+
# In case there are multiple fieldmaps prefer EPI
123+
fmaps.sort(key=lambda fmap: FMAP_PRIORITY[fmap['suffix']])
124+
fmap = fmaps[0]
125+
130126
# PEPOLAR path
131127
if 'epi' in fmaps:
128+
from .pepolar import init_pepolar_unwarp_wf, check_pes
132129
outputnode.inputs.method = 'PEB/PEPOLAR (phase-encoding based / PE-POLARity)'
130+
131+
# Filter out EPI fieldmaps to be used
132+
fmaps_epi = [(epi.path, epi.get_metadata()['PhaseEncodingDirection'])
133+
for epi in fmaps['epi']]
134+
135+
# Find matched PE directions
136+
matched_pe = check_pes(fmaps_epi, epi_meta['PhaseEncodingDirection'])
137+
133138
# Get EPI polarities and their metadata
134139
sdc_unwarp_wf = init_pepolar_unwarp_wf(
135-
bold_meta=epi_file.get_metadata(),
136-
epi_fmaps=[(fmap, fmap.get_metadata()["PhaseEncodingDirection"])
137-
for fmap in fmaps['epi']],
138-
omp_nthreads=omp_nthreads,
139-
name='pepolar_unwarp_wf')
140+
matched_pe=matched_pe,
141+
omp_nthreads=omp_nthreads)
142+
sdc_unwarp_wf.inputs.inputnode.epi_pe_dir = epi_meta['PhaseEncodingDirection']
143+
sdc_unwarp_wf.inputs.inputnode.fmaps_epi = fmaps_epi
140144

141145
workflow.connect([
142146
(inputnode, sdc_unwarp_wf, [
143147
('epi_file', 'inputnode.in_reference'),
144-
('bold_mask', 'inputnode.in_mask'),
145-
('bold_ref_brain', 'inputnode.in_reference_brain')]),
148+
('epi_brain', 'inputnode.in_reference_brain'),
149+
('epi_mask', 'inputnode.in_mask')]),
146150
])
147151

148152
# FIELDMAP path
149-
# elif 'fieldmap' in fmaps:
150-
# # Import specific workflows here, so we don't break everything with one
151-
# # unused workflow.
152-
# suffices = {f.suffix for f in fmaps['fieldmap']}
153-
# if 'fieldmap' in suffices:
154-
# from .fmap import init_fmap_wf
155-
# outputnode.inputs.method = 'FMB (fieldmap-based)'
156-
# fmap_estimator_wf = init_fmap_wf(
157-
# omp_nthreads=omp_nthreads,
158-
# fmap_bspline=False)
159-
# # set inputs
160-
# fmap_estimator_wf.inputs.inputnode.fieldmap = fmap['fieldmap']
161-
# fmap_estimator_wf.inputs.inputnode.magnitude = fmap['magnitude']
162-
163-
# if fmap['suffix'] == 'phasediff':
164-
# from .phdiff import init_phdiff_wf
165-
# fmap_estimator_wf = init_phdiff_wf(omp_nthreads=omp_nthreads)
166-
# # set inputs
167-
# fmap_estimator_wf.inputs.inputnode.phasediff = fmap['phasediff']
168-
# fmap_estimator_wf.inputs.inputnode.magnitude = [
169-
# fmap_ for key, fmap_ in sorted(fmap.items())
170-
# if key.startswith("magnitude")
171-
# ]
172-
173-
# sdc_unwarp_wf = init_sdc_unwarp_wf(
174-
# omp_nthreads=omp_nthreads,
175-
# fmap_demean=fmap_demean,
176-
# debug=debug,
177-
# name='sdc_unwarp_wf')
178-
# sdc_unwarp_wf.inputs.inputnode.metadata = bold_meta
179-
180-
# workflow.connect([
181-
# (inputnode, sdc_unwarp_wf, [
182-
# ('epi_file', 'inputnode.in_reference'),
183-
# ('bold_ref_brain', 'inputnode.in_reference_brain'),
184-
# ('bold_mask', 'inputnode.in_mask')]),
185-
# (fmap_estimator_wf, sdc_unwarp_wf, [
186-
# ('outputnode.fmap', 'inputnode.fmap'),
187-
# ('outputnode.fmap_ref', 'inputnode.fmap_ref'),
188-
# ('outputnode.fmap_mask', 'inputnode.fmap_mask')]),
189-
# ])
190-
191-
# # FIELDMAP-less path
192-
# if any(fm['suffix'] == 'syn' for fm in fmaps):
193-
# # Select template
194-
# sdc_select_std = pe.Node(KeySelect(
195-
# fields=['std2anat_xfm']),
196-
# name='sdc_select_std', run_without_submitting=True)
197-
198-
# syn_sdc_wf = init_syn_sdc_wf(
199-
# bold_pe=bold_meta.get('PhaseEncodingDirection', None),
200-
# omp_nthreads=omp_nthreads)
201-
202-
# workflow.connect([
203-
# (inputnode, sdc_select_std, [
204-
# ('template', 'key'),
205-
# ('templates', 'keys'),
206-
# ('std2anat_xfm', 'std2anat_xfm')]),
207-
# (sdc_select_std, syn_sdc_wf, [
208-
# ('std2anat_xfm', 'inputnode.std2anat_xfm')]),
209-
# (inputnode, syn_sdc_wf, [
210-
# ('t1_brain', 'inputnode.t1_brain'),
211-
# ('epi_file', 'inputnode.epi_file'),
212-
# ('bold_ref_brain', 'inputnode.bold_ref_brain'),
213-
# ('template', 'inputnode.template')]),
214-
# ])
215-
216-
# # XXX Eliminate branch when forcing isn't an option
217-
# if fmap['suffix'] == 'syn': # No fieldmaps, but --use-syn
218-
# outputnode.inputs.method = 'FLB ("fieldmap-less", SyN-based)'
219-
# sdc_unwarp_wf = syn_sdc_wf
220-
# else: # --force-syn was called when other fieldmap was present
221-
# sdc_unwarp_wf.__desc__ = None
222-
# workflow.connect([
223-
# (syn_sdc_wf, outputnode, [
224-
# ('outputnode.out_reference', 'syn_bold_ref')]),
225-
# ])
153+
elif 'fieldmap' in fmaps:
154+
from .unwarp import init_sdc_unwarp_wf
155+
# Import specific workflows here, so we don't break everything with one
156+
# unused workflow.
157+
suffices = {f.suffix for f in fmaps['fieldmap']}
158+
if 'fieldmap' in suffices:
159+
from .fmap import init_fmap_wf
160+
outputnode.inputs.method = 'FMB (fieldmap-based)'
161+
fmap_wf = init_fmap_wf(
162+
omp_nthreads=omp_nthreads,
163+
fmap_bspline=False)
164+
# set inputs
165+
fmap_wf.inputs.inputnode.magnitude = fmap['magnitude']
166+
fmap_wf.inputs.inputnode.fieldmap = fmap['fieldmap']
167+
elif 'phasediff' in suffices:
168+
from .phdiff import init_phdiff_wf
169+
fmap_wf = init_phdiff_wf(omp_nthreads=omp_nthreads)
170+
# set inputs
171+
fmap_wf.inputs.inputnode.phasediff = fmap['phasediff']
172+
fmap_wf.inputs.inputnode.magnitude = [
173+
fmap_ for key, fmap_ in sorted(fmap.items())
174+
if key.startswith("magnitude")
175+
]
176+
else:
177+
raise ValueError('Fieldmaps of types %s are not supported' %
178+
', '.join(['"%s"' % f for f in suffices]))
179+
180+
sdc_unwarp_wf = init_sdc_unwarp_wf(
181+
omp_nthreads=omp_nthreads,
182+
debug=debug,
183+
name='sdc_unwarp_wf')
184+
sdc_unwarp_wf.inputs.inputnode.metadata = epi_meta
185+
186+
workflow.connect([
187+
(inputnode, sdc_unwarp_wf, [
188+
('epi_file', 'inputnode.in_reference'),
189+
('epi_brain', 'inputnode.in_reference_brain'),
190+
('epi_mask', 'inputnode.in_mask')]),
191+
(fmap_wf, sdc_unwarp_wf, [
192+
('outputnode.fmap', 'inputnode.fmap'),
193+
('outputnode.fmap_ref', 'inputnode.fmap_ref'),
194+
('outputnode.fmap_mask', 'inputnode.fmap_mask')]),
195+
])
196+
197+
# FIELDMAP-less path
198+
if any(fm['suffix'] == 'syn' for fm in fmaps):
199+
from .syn import init_syn_sdc_wf
200+
syn_sdc_wf = init_syn_sdc_wf(
201+
bold_pe=epi_meta.get('PhaseEncodingDirection', None),
202+
omp_nthreads=omp_nthreads)
203+
204+
workflow.connect([
205+
(inputnode, syn_sdc_wf, [
206+
('t1w_brain', 'inputnode.t1w_brain'),
207+
('epi_file', 'inputnode.epi_file'),
208+
('epi_brain', 'inputnode.epi_brain'),
209+
('std2anat_xfm', 'inputnode.std2anat_xfm')]),
210+
])
211+
212+
# XXX Eliminate branch when forcing isn't an option
213+
if fmap['suffix'] == 'syn': # No fieldmaps, but --use-syn
214+
outputnode.inputs.method = 'FLB ("fieldmap-less", SyN-based)'
215+
sdc_unwarp_wf = syn_sdc_wf
216+
else: # --force-syn was called when other fieldmap was present
217+
sdc_unwarp_wf.__desc__ = None
218+
workflow.connect([
219+
(syn_sdc_wf, outputnode, [
220+
('outputnode.out_reference', 'syn_bold_ref')]),
221+
])
226222

227223
workflow.connect([
228224
(sdc_unwarp_wf, outputnode, [
229225
('outputnode.out_warp', 'out_warp'),
230226
('outputnode.out_reference', 'epi_file'),
231-
('outputnode.out_reference_brain', 'bold_ref_brain'),
232-
('outputnode.out_mask', 'bold_mask')]),
227+
('outputnode.out_reference_brain', 'epi_brain'),
228+
('outputnode.out_mask', 'epi_mask')]),
233229
])
234230

235231
return workflow

sdcflows/workflows/syn.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,23 +70,23 @@ def init_syn_sdc_wf(omp_nthreads, bold_pe=None,
7070
7171
Inputs
7272
------
73-
bold_ref
73+
in_reference
7474
reference image
75-
bold_ref_brain
75+
in_reference_brain
7676
skull-stripped reference image
7777
template : str
7878
Name of template targeted by ``template`` output space
79-
t1_brain
79+
t1w_brain
8080
skull-stripped, bias-corrected structural image
8181
std2anat_xfm
8282
inverse registration transform of T1w image to MNI template
8383
8484
Outputs
8585
-------
8686
out_reference
87-
the ``bold_ref`` image after unwarping
87+
the ``in_reference`` image after unwarping
8888
out_reference_brain
89-
the ``bold_ref_brain`` image after unwarping
89+
the ``in_reference_brain`` image after unwarping
9090
out_warp
9191
the corresponding :abbr:`DFM (displacements field map)` compatible with
9292
ANTs
@@ -127,8 +127,8 @@ def init_syn_sdc_wf(omp_nthreads, bold_pe=None,
127127
template [@fieldmapless3].
128128
""".format(ants_ver=Registration().version or '<ver>')
129129
inputnode = pe.Node(
130-
niu.IdentityInterface(['bold_ref', 'bold_ref_brain', 'template',
131-
't1_brain', 'std2anat_xfm']),
130+
niu.IdentityInterface(['in_reference', 'in_reference_brain', 'template',
131+
't1w_brain', 'std2anat_xfm']),
132132
name='inputnode')
133133
outputnode = pe.Node(
134134
niu.IdentityInterface(['out_reference', 'out_reference_brain',
@@ -184,28 +184,28 @@ def init_syn_sdc_wf(omp_nthreads, bold_pe=None,
184184
skullstrip_bold_wf = init_skullstrip_bold_wf()
185185

186186
workflow.connect([
187-
(inputnode, invert_t1w, [('t1_brain', 'in_file'),
188-
('bold_ref', 'ref_file')]),
189-
(inputnode, ref_2_t1, [('bold_ref_brain', 'moving_image')]),
187+
(inputnode, invert_t1w, [('t1w_brain', 'in_file'),
188+
('in_reference', 'ref_file')]),
189+
(inputnode, ref_2_t1, [('in_reference_brain', 'moving_image')]),
190190
(invert_t1w, ref_2_t1, [('out_file', 'fixed_image')]),
191-
(inputnode, t1_2_ref, [('bold_ref', 'reference_image')]),
191+
(inputnode, t1_2_ref, [('in_reference', 'reference_image')]),
192192
(invert_t1w, t1_2_ref, [('out_file', 'input_image')]),
193193
(ref_2_t1, t1_2_ref, [('forward_transforms', 'transforms')]),
194194
(ref_2_t1, transform_list, [('forward_transforms', 'in1')]),
195195
(inputnode, transform_list, [
196196
('std2anat_xfm', 'in2'),
197197
(('template', _prior_path), 'in3')]),
198-
(inputnode, atlas_2_ref, [('bold_ref', 'reference_image')]),
198+
(inputnode, atlas_2_ref, [('in_reference', 'reference_image')]),
199199
(transform_list, atlas_2_ref, [('out', 'transforms')]),
200200
(atlas_2_ref, threshold_atlas, [('output_image', 'in_file')]),
201201
(threshold_atlas, fixed_image_masks, [('out_file', 'in2')]),
202-
(inputnode, syn, [('bold_ref_brain', 'moving_image')]),
202+
(inputnode, syn, [('in_reference_brain', 'moving_image')]),
203203
(t1_2_ref, syn, [('output_image', 'fixed_image')]),
204204
(fixed_image_masks, syn, [('out', 'fixed_image_masks')]),
205205
(syn, outputnode, [('forward_transforms', 'out_warp')]),
206206
(syn, unwarp_ref, [('forward_transforms', 'transforms')]),
207-
(inputnode, unwarp_ref, [('bold_ref', 'reference_image'),
208-
('bold_ref', 'input_image')]),
207+
(inputnode, unwarp_ref, [('in_reference', 'reference_image'),
208+
('in_reference', 'input_image')]),
209209
(unwarp_ref, skullstrip_bold_wf, [
210210
('output_image', 'inputnode.in_file')]),
211211
(unwarp_ref, outputnode, [('output_image', 'out_reference')]),

0 commit comments

Comments
 (0)