Skip to content

Commit 714e3c5

Browse files
authored
Merge pull request #56 from oesteban/fix/19-init-sdc-unwarp-workflow
ENH: Refactor fieldmap-unwarping flows, more homogeneous interface
2 parents 593b555 + ac2daa4 commit 714e3c5

File tree

9 files changed

+436
-272
lines changed

9 files changed

+436
-272
lines changed

sdcflows/interfaces/fmap.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,63 @@ def _run_interface(self, runtime):
230230
return runtime
231231

232232

233+
class _FUGUEvsm2ANTSwarpInputSpec(BaseInterfaceInputSpec):
234+
in_file = File(exists=True, mandatory=True,
235+
desc='input displacements field map')
236+
pe_dir = traits.Enum('i', 'i-', 'j', 'j-', 'k', 'k-',
237+
desc='phase-encoding axis')
238+
239+
240+
class _FUGUEvsm2ANTSwarpOutputSpec(TraitedSpec):
241+
out_file = File(desc='the output warp field')
242+
fieldmap = File(desc='field map in mm')
243+
244+
245+
class FUGUEvsm2ANTSwarp(SimpleInterface):
246+
"""Convert a voxel-shift-map to ants warp."""
247+
248+
_dtype = '<f4'
249+
input_spec = _FUGUEvsm2ANTSwarpInputSpec
250+
output_spec = _FUGUEvsm2ANTSwarpOutputSpec
251+
252+
def _run_interface(self, runtime):
253+
phaseEncDim = {'i': 0, 'j': 1, 'k': 2}[self.inputs.pe_dir[0]]
254+
phaseEncSign = [1.0, -1.0][len(self.inputs.pe_dir) != 2]
255+
256+
# Create new header
257+
nii = nb.load(self.inputs.in_file)
258+
hdr = nii.header.copy()
259+
hdr.set_data_dtype(self._dtype)
260+
261+
# Get data, convert to mm
262+
data = nii.get_fdata(dtype=self._dtype)
263+
aff = np.diag([1.0, 1.0, -1.0])
264+
if np.linalg.det(aff) < 0 and phaseEncDim != 0:
265+
# Reverse direction since ITK is LPS
266+
aff *= -1.0
267+
268+
aff = aff.dot(nii.affine[:3, :3])
269+
data *= phaseEncSign * nii.header.get_zooms()[phaseEncDim]
270+
self._results['fieldmap'] = fname_presuffix(
271+
self.inputs.in_file, suffix='_units-mm_fieldmap', newpath=runtime.cwd)
272+
nb.Nifti1Image(data, nii.affine, hdr).to_filename(self._results['fieldmap'])
273+
274+
# Compose a vector field
275+
zeros = np.zeros_like(data, dtype=self._dtype)
276+
field = [zeros, zeros]
277+
field.insert(phaseEncDim, data)
278+
field = np.stack(field, -1)
279+
280+
hdr.set_intent('vector', (), '')
281+
# Write out
282+
self._results['out_file'] = fname_presuffix(
283+
self.inputs.in_file, suffix='_desc-field_sdcwarp', newpath=runtime.cwd)
284+
nb.Nifti1Image(field[:, :, :, np.newaxis, :], nii.affine, hdr).to_filename(
285+
self._results['out_file'])
286+
287+
return runtime
288+
289+
233290
def _despike2d(data, thres, neigh=None):
234291
"""Despike axial slices, as done in FSL's ``epiunwarp``."""
235292
if neigh is None:

sdcflows/workflows/base.py

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
DEFAULT_MEMORY_MIN_GB = 0.01
2020

2121

22-
def init_sdc_estimate_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):
2323
"""
2424
Build a :abbr:`SDC (susceptibility distortion correction)` workflow.
2525
@@ -66,8 +66,8 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
6666
6767
Outputs
6868
-------
69-
epi_file
70-
An unwarped EPI scan reference
69+
epi_corrected
70+
The EPI scan reference after unwarping.
7171
epi_mask
7272
The corresponding new mask after unwarping
7373
epi_brain
@@ -77,6 +77,8 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
7777
syn_ref
7878
If ``--force-syn``, an unwarped EPI scan reference with this
7979
method (for reporting purposes)
80+
method : str
81+
Short description of the estimation method that was run.
8082
8183
"""
8284
workflow = Workflow(name='sdc_estimate_wf' if fmaps else 'sdc_bypass_wf')
@@ -85,28 +87,25 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
8587
name='inputnode')
8688

8789
outputnode = pe.Node(niu.IdentityInterface(
88-
fields=['output_ref', 'epi_mask', 'epi_brain',
90+
fields=['epi_corrected', 'epi_mask', 'epi_brain',
8991
'out_warp', 'syn_ref', 'method']),
9092
name='outputnode')
9193

9294
# No fieldmaps - forward inputs to outputs
93-
ignored = False if ignore is None else 'fieldmaps' in ignore
94-
if not fmaps or ignored:
95+
if not fmaps:
9596
workflow.__postdesc__ = """\
96-
Susceptibility distortion correction (SDC) has been skipped because the
97-
dataset does not contain extra field map acquisitions correctly described
98-
with metadata, and the experimental SDC-SyN method was not explicitly selected.
97+
Susceptibility distortion correction (SDC) was omitted.
9998
"""
10099
outputnode.inputs.method = 'None'
101100
workflow.connect([
102-
(inputnode, outputnode, [('epi_file', 'output_ref'),
101+
(inputnode, outputnode, [('epi_file', 'epi_corrected'),
103102
('epi_mask', 'epi_mask'),
104103
('epi_brain', 'epi_brain')]),
105104
])
106105
return workflow
107106

108107
workflow.__postdesc__ = """\
109-
Based on the estimated susceptibility distortion, an unwarped
108+
Based on the estimated susceptibility distortion, a corrected
110109
EPI (echo-planar imaging) reference was calculated for a more
111110
accurate co-registration with the anatomical reference.
112111
"""
@@ -151,6 +150,7 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
151150

152151
# FIELDMAP path
153152
elif 'fieldmap' in fmaps or 'phasediff' in fmaps:
153+
from .fmap import init_fmap2field_wf
154154
from .unwarp import init_sdc_unwarp_wf
155155

156156
# SyN works without this metadata
@@ -161,39 +161,60 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
161161

162162
if 'fieldmap' in fmaps:
163163
from .fmap import init_fmap_wf
164+
try:
165+
fmap, = fmaps['fieldmap']
166+
except ValueError:
167+
LOGGER.warning('Several B0 fieldmaps found for the given target, using '
168+
'the first one.')
169+
fmap = fmaps['fieldmap'][0]
170+
164171
outputnode.inputs.method = 'FMB (fieldmap-based) - directly measured B0 map'
165172
fmap_wf = init_fmap_wf(
166173
omp_nthreads=omp_nthreads,
167174
fmap_bspline=False)
168175
# set inputs
169176
fmap_wf.inputs.inputnode.magnitude = [
170-
m for m, _ in fmaps['fieldmap']['magnitude']]
177+
m for m, _ in fmap['magnitude']]
171178
fmap_wf.inputs.inputnode.fieldmap = [
172-
m for m, _ in fmaps['fieldmap']['fieldmap']]
179+
m for m, _ in fmap['fieldmap']]
173180
elif 'phasediff' in fmaps:
174181
from .phdiff import init_phdiff_wf
182+
try:
183+
fmap, = fmaps['phasediff']
184+
except ValueError:
185+
LOGGER.warning('Several phase-difference maps found for the given target, using '
186+
'the first one.')
187+
fmap = fmaps['phasediff'][0]
188+
175189
outputnode.inputs.method = 'FMB (fieldmap-based) - phase-difference map'
176190
fmap_wf = init_phdiff_wf(omp_nthreads=omp_nthreads)
177191
# set inputs
178192
fmap_wf.inputs.inputnode.magnitude = [
179-
m for m, _ in fmaps['phasediff']['magnitude']]
180-
fmap_wf.inputs.inputnode.phasediff = fmaps['phasediff']['phases']
193+
m for m, _ in fmap['magnitude']]
194+
fmap_wf.inputs.inputnode.phasediff = fmap['phases']
195+
196+
fmap2field_wf = init_fmap2field_wf(omp_nthreads=omp_nthreads, debug=debug)
197+
fmap2field_wf.inputs.inputnode.metadata = epi_meta
181198

182199
sdc_unwarp_wf = init_sdc_unwarp_wf(
183200
omp_nthreads=omp_nthreads,
184201
debug=debug,
185202
name='sdc_unwarp_wf')
186-
sdc_unwarp_wf.inputs.inputnode.metadata = epi_meta
187203

188204
workflow.connect([
205+
(inputnode, fmap2field_wf, [
206+
('epi_file', 'inputnode.in_reference'),
207+
('epi_brain', 'inputnode.in_reference_brain')]),
189208
(inputnode, sdc_unwarp_wf, [
190209
('epi_file', 'inputnode.in_reference'),
191-
('epi_brain', 'inputnode.in_reference_brain'),
192-
('epi_mask', 'inputnode.in_mask')]),
193-
(fmap_wf, sdc_unwarp_wf, [
210+
('epi_mask', 'inputnode.in_reference_mask')]),
211+
(fmap_wf, fmap2field_wf, [
194212
('outputnode.fmap', 'inputnode.fmap'),
195213
('outputnode.fmap_ref', 'inputnode.fmap_ref'),
196214
('outputnode.fmap_mask', 'inputnode.fmap_mask')]),
215+
(fmap2field_wf, sdc_unwarp_wf, [
216+
('outputnode.out_warp', 'inputnode.in_warp')]),
217+
197218
])
198219
elif not only_syn:
199220
raise ValueError('Fieldmaps of types %s are not supported' %
@@ -228,9 +249,44 @@ def init_sdc_estimate_wf(fmaps, epi_meta, omp_nthreads=1, debug=False, ignore=No
228249
workflow.connect([
229250
(sdc_unwarp_wf, outputnode, [
230251
('outputnode.out_warp', 'out_warp'),
231-
('outputnode.out_reference', 'epi_file'),
252+
('outputnode.out_reference', 'epi_corrected'),
232253
('outputnode.out_reference_brain', 'epi_brain'),
233254
('outputnode.out_mask', 'epi_mask')]),
234255
])
235256

236257
return workflow
258+
259+
260+
def fieldmap_wrangler(layout, target_image, use_syn=False, force_syn=False):
261+
"""Query the BIDSLayout for fieldmaps, and arrange them for the orchestration workflow."""
262+
from collections import defaultdict
263+
fmap_bids = layout.get_fieldmap(target_image, return_list=True)
264+
fieldmaps = defaultdict(list)
265+
for fmap in fmap_bids:
266+
if fmap['suffix'] == 'epi':
267+
fieldmaps['epi'].append((fmap['epi'], layout.get_metadata(fmap['epi'])))
268+
269+
if fmap['suffix'] == 'fieldmap':
270+
fieldmaps['fieldmap'].append({
271+
'magnitude': [(fmap['magnitude'], layout.get_metadata(fmap['magnitude']))],
272+
'fieldmap': [(fmap['fieldmap'], layout.get_metadata(fmap['fieldmap']))],
273+
})
274+
275+
if fmap['suffix'] == 'phasediff':
276+
fieldmaps['phasediff'].append({
277+
'magnitude': [(fmap[k], layout.get_metadata(fmap[k]))
278+
for k in sorted(fmap.keys()) if k.startswith('magnitude')],
279+
'phases': [(fmap['phasediff'], layout.get_metadata(fmap['phasediff']))],
280+
})
281+
282+
if fmap['suffix'] == 'phase':
283+
fieldmaps['phasediff'].append({
284+
'magnitude': [(fmap[k], layout.get_metadata(fmap[k]))
285+
for k in sorted(fmap.keys()) if k.startswith('magnitude')],
286+
'phases': [(fmap[k], layout.get_metadata(fmap[k]))
287+
for k in sorted(fmap.keys()) if k.startswith('phase')],
288+
})
289+
290+
if force_syn is True or (not fieldmaps and use_syn is True):
291+
fieldmaps['syn'] = force_syn
292+
return fieldmaps

0 commit comments

Comments
 (0)