Skip to content

Commit c093c19

Browse files
committed
enh: refactor the distortion estimation workflow
Close #16.
1 parent 9c520a6 commit c093c19

File tree

5 files changed

+188
-200
lines changed

5 files changed

+188
-200
lines changed

sdcflows/workflows/base.py

Lines changed: 122 additions & 126 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(distorted_ref, 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,41 +37,40 @@ def init_sdc_wf(distorted_ref, omp_nthreads=1, debug=False, ignore=None):
4137
4238
Parameters
4339
----------
44-
distorted_ref : pybids.BIDSFile
45-
A BIDSFile object with 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.
4648
omp_nthreads : int
4749
Maximum number of threads an individual process may use
4850
debug : bool
4951
Enable debugging outputs
5052
5153
Inputs
5254
------
53-
distorted_ref
55+
epi_file
5456
A reference image calculated at a previous stage
55-
ref_brain
57+
epi_brain
5658
Same as above, but brain-masked
57-
ref_mask
59+
epi_mask
5860
Brain mask for the run
59-
t1_brain
61+
t1w_brain
6062
T1w image, brain-masked, for the fieldmap-less SyN method
6163
std2anat_xfm
62-
List of standard-to-T1w transforms generated during spatial
64+
Standard-to-T1w transform generated during spatial
6365
normalization (only for the fieldmap-less SyN method).
64-
template : str
65-
Name of template from which prior knowledge will be mapped
66-
into the subject's T1w reference
67-
(only for the fieldmap-less SyN method)
68-
templates : str
69-
Name of templates that index the ``std2anat_xfm`` input list
70-
(only for the fieldmap-less SyN method).
7166
7267
Outputs
7368
-------
74-
distorted_ref
69+
epi_file
7570
An unwarped BOLD reference
76-
bold_mask
71+
epi_mask
7772
The corresponding new mask after unwarping
78-
bold_ref_brain
73+
epi_brain
7974
Brain-extracted, unwarped BOLD reference
8075
out_warp
8176
The deformation field to unwarp the susceptibility distortions
@@ -90,19 +85,16 @@ def init_sdc_wf(distorted_ref, omp_nthreads=1, debug=False, ignore=None):
9085
if not isinstance(ignore, (list, tuple)):
9186
ignore = tuple(ignore)
9287

93-
fmaps = defaultdict(list, [])
94-
for associated in distorted_ref.get_associations(kind='InformedBy'):
95-
if associated.suffix in list(FMAP_PRIORITY.keys()):
96-
fmaps[associated.suffix].append(associated)
88+
# TODO: To be removed (filter out unsupported fieldmaps):
89+
fmaps = [fmap for fmap in fmaps if fmap['suffix'] in FMAP_PRIORITY]
9790

98-
workflow = Workflow(name='sdc_wf' if distorted_ref 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=['distorted_ref', '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

@@ -115,121 +107,125 @@ def init_sdc_wf(distorted_ref, omp_nthreads=1, debug=False, ignore=None):
115107
"""
116108
outputnode.inputs.method = 'None'
117109
workflow.connect([
118-
(inputnode, outputnode, [('distorted_ref', 'output_ref'),
119-
('ref_mask', 'ref_mask'),
120-
('ref_brain', 'ref_brain')]),
110+
(inputnode, outputnode, [('epi_file', 'output_ref'),
111+
('epi_mask', 'epi_mask'),
112+
('epi_brain', 'epi_brain')]),
121113
])
122114
return workflow
123115

124116
workflow.__postdesc__ = """\
125-
Based on the estimated susceptibility distortion, an
126-
unwarped BOLD reference was calculated for a more accurate
127-
co-registration with the anatomical reference.
117+
Based on the estimated susceptibility distortion, an unwarped
118+
EPI (echo-planar imaging) reference was calculated for a more
119+
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=distorted_ref.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, [
143-
('distorted_ref', 'inputnode.in_reference'),
144-
('bold_mask', 'inputnode.in_mask'),
145-
('bold_ref_brain', 'inputnode.in_reference_brain')]),
147+
('epi_file', 'inputnode.in_reference'),
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-
# ('distorted_ref', '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-
# ('distorted_ref', 'inputnode.distorted_ref'),
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'),
230-
('outputnode.out_reference', 'distorted_ref'),
231-
('outputnode.out_reference_brain', 'bold_ref_brain'),
232-
('outputnode.out_mask', 'bold_mask')]),
226+
('outputnode.out_reference', 'epi_file'),
227+
('outputnode.out_reference_brain', 'epi_brain'),
228+
('outputnode.out_mask', 'epi_mask')]),
233229
])
234230

235231
return workflow

sdcflows/workflows/pepolar.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ def init_pepolar_unwarp_wf(omp_nthreads=1, matched_pe=False,
7373
fmaps_epi : list of tuple(pathlike, str)
7474
The list of EPI images that will be used in PE-Polar correction, and
7575
their corresponding ``PhaseEncodingDirection`` metadata.
76-
The workflow will use the ``bold_pe_dir`` input to separate out those
76+
The workflow will use the ``epi_pe_dir`` input to separate out those
7777
EPI acquisitions with opposed PE blips and those with matched PE blips
7878
(the latter could be none, and ``in_reference_brain`` would then be
7979
used). The workflow raises a ``ValueError`` when no images with
8080
opposed PE blips are found.
81-
bold_pe_dir : str
81+
epi_pe_dir : str
8282
The baseline PE direction.
8383
in_reference : pathlike
84-
The baseline reference image (must correspond to ``bold_pe_dir``).
84+
The baseline reference image (must correspond to ``epi_pe_dir``).
8585
in_reference_brain : pathlike
8686
The reference image above, but skullstripped.
8787
in_mask : pathlike
@@ -110,7 +110,7 @@ def init_pepolar_unwarp_wf(omp_nthreads=1, matched_pe=False,
110110

111111
inputnode = pe.Node(niu.IdentityInterface(
112112
fields=['fmaps_epi', 'in_reference', 'in_reference_brain',
113-
'in_mask', 'bold_pe_dir']), name='inputnode')
113+
'in_mask', 'epi_pe_dir']), name='inputnode')
114114

115115
outputnode = pe.Node(niu.IdentityInterface(
116116
fields=['out_reference', 'out_reference_brain', 'out_warp', 'out_mask']),
@@ -140,11 +140,11 @@ def init_pepolar_unwarp_wf(omp_nthreads=1, matched_pe=False,
140140
omp_nthreads=omp_nthreads)
141141

142142
workflow.connect([
143-
(inputnode, qwarp, [(('bold_pe_dir', _qwarp_args), 'args')]),
143+
(inputnode, qwarp, [(('epi_pe_dir', _qwarp_args), 'args')]),
144144
(inputnode, cphdr_warp, [('in_reference', 'hdr_file')]),
145145
(inputnode, prepare_epi_wf, [
146146
('fmaps_epi', 'inputnode.maps_pe'),
147-
('bold_pe_dir', 'inputnode.epi_pe'),
147+
('epi_pe_dir', 'inputnode.epi_pe'),
148148
('in_reference_brain', 'inputnode.ref_brain')]),
149149
(prepare_epi_wf, qwarp, [('outputnode.opposed_pe', 'base_file'),
150150
('outputnode.matched_pe', 'in_file')]),

0 commit comments

Comments
 (0)