Skip to content

Commit be7058e

Browse files
authored
Merge pull request #50 from oesteban/fix/siemens2rads
ENH: Stop using siemens2rads from old nipype workflows
2 parents 85d728a + 82a108a commit be7058e

File tree

2 files changed

+107
-14
lines changed

2 files changed

+107
-14
lines changed

sdcflows/interfaces/fmap.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,16 @@ class _Phasediff2FieldmapOutputSpec(TraitedSpec):
187187

188188

189189
class Phasediff2Fieldmap(SimpleInterface):
190-
"""Convert a phase difference map into a fieldmap in Hz."""
190+
"""
191+
Convert a phase difference map into a fieldmap in Hz.
192+
193+
This interface is equivalent to running the following steps:
194+
#. Convert from rad to rad/s
195+
(``niflow.nipype1.workflows.dmri.fsl.utils.rads2radsec``)
196+
#. FUGUE execution: fsl.FUGUE(save_fmap=True)
197+
#. Conversion from rad/s to Hz (divide by 2pi, ``rsec2hz``).
198+
199+
"""
191200

192201
input_spec = _Phasediff2FieldmapInputSpec
193202
output_spec = _Phasediff2FieldmapOutputSpec
@@ -200,6 +209,27 @@ def _run_interface(self, runtime):
200209
return runtime
201210

202211

212+
class _PhaseMap2radsInputSpec(BaseInterfaceInputSpec):
213+
in_file = File(exists=True, mandatory=True, desc='input (wrapped) phase map')
214+
215+
216+
class _PhaseMap2radsOutputSpec(TraitedSpec):
217+
out_file = File(desc='the phase map in the range 0 - 6.28')
218+
219+
220+
class PhaseMap2rads(SimpleInterface):
221+
"""Convert a phase map in a.u. to radians."""
222+
223+
input_spec = _PhaseMap2radsInputSpec
224+
output_spec = _PhaseMap2radsOutputSpec
225+
226+
def _run_interface(self, runtime):
227+
self._results['out_file'] = au2rads(
228+
self.inputs.in_file,
229+
newpath=runtime.cwd)
230+
return runtime
231+
232+
203233
def _despike2d(data, thres, neigh=None):
204234
"""Despike axial slices, as done in FSL's ``epiunwarp``."""
205235
if neigh is None:
@@ -531,3 +561,32 @@ def _delta_te(in_values, te1=None, te2=None):
531561
'EchoTime2 metadata field not found. Please consult the BIDS specification.')
532562

533563
return abs(float(te2) - float(te1))
564+
565+
566+
def au2rads(in_file, newpath=None):
567+
"""Convert the input phase difference map in arbitrary units (a.u.) to rads."""
568+
from scipy.stats import mode
569+
im = nb.load(in_file)
570+
data = im.get_fdata(dtype='float32')
571+
hdr = im.header.copy()
572+
573+
# First center data around 0.0.
574+
data -= mode(data, axis=None)[0][0]
575+
576+
# Scale lower tail
577+
data[data < 0] = - np.pi * data[data < 0] / data[data < 0].min()
578+
579+
# Scale upper tail
580+
data[data > 0] = np.pi * data[data > 0] / data[data > 0].max()
581+
582+
# Offset to 0 - 2pi
583+
data += np.pi
584+
585+
# Clip
586+
data = np.clip(data, 0.0, 2 * np.pi)
587+
588+
hdr.set_data_dtype(np.float32)
589+
hdr.set_xyzt_units('mm')
590+
out_file = fname_presuffix(in_file, suffix='_rads', newpath=newpath)
591+
nb.Nifti1Image(data, im.affine, hdr).to_filename(out_file)
592+
return out_file

sdcflows/workflows/phdiff.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,31 @@
2020
from nipype.interfaces import ants, fsl, utility as niu
2121
from nipype.pipeline import engine as pe
2222
from niflow.nipype1.workflows.dmri.fsl.utils import (
23-
siemens2rads, demean_image, cleanup_edge_pipeline)
23+
demean_image, cleanup_edge_pipeline)
2424
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
2525
from niworkflows.interfaces.images import IntraModalMerge
2626
from niworkflows.interfaces.masks import BETRPT
2727

28-
from ..interfaces.fmap import Phasediff2Fieldmap
28+
from ..interfaces.fmap import Phasediff2Fieldmap, PhaseMap2rads
2929

3030

3131
def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
32-
"""
32+
r"""
3333
Distortion correction of EPI sequences using phase-difference maps.
3434
3535
Estimates the fieldmap using a phase-difference image and one or more
3636
magnitude images corresponding to two or more :abbr:`GRE (Gradient Echo sequence)`
37-
acquisitions. The `original code was taken from nipype
37+
acquisitions.
38+
The most delicate bit of this workflow is the phase-unwrapping process: phase maps
39+
are clipped in the range :math:`[0 \dotsb 2 \cdot \pi )`.
40+
To find the integer number of offsets that make a region continously smooth with
41+
its neighbour, FSL PRELUDE is run [Jenkinson2003]_.
42+
FSL PRELUDE takes wrapped maps in the range 0 to 6.28, `as per the user guide
43+
<https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FUGUE/Guide#Step_2_-_Getting_.28wrapped.29_phase_in_radians>`__.
44+
For the phase-difference maps, recentering back to :math:`[-\pi \dotsb \pi )` is necessary.
45+
After some massaging and the application of the effective echo spacing parameter,
46+
the phase-difference maps can be converted into a *B0 field map* in Hz units.
47+
The `original code was taken from nipype
3848
<https://github.com/nipy/nipype/blob/0.12.1/nipype/workflows/dmri/fsl/artifacts.py#L514>`_.
3949
4050
Workflow Graph
@@ -68,6 +78,11 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
6878
fmap : pathlike
6979
The estimated fieldmap in Hz
7080
81+
References
82+
----------
83+
.. [Jenkinson2003] Jenkinson, M. (2003) Fast, automated, N-dimensional phase-unwrapping
84+
algorithm. MRM 49(1):193-197. doi:`10.1002/mrm.10354 <10.1002/mrm.10354>`__.
85+
7186
"""
7287
workflow = Workflow(name=name)
7388
workflow.__desc__ = """\
@@ -98,11 +113,15 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
98113
# nan2zeros=True, args='-kernel sphere 5 -dilM'), name='MskDilate')
99114

100115
# phase diff -> radians
101-
pha2rads = pe.Node(niu.Function(function=siemens2rads), name='pha2rads')
116+
phmap2rads = pe.Node(PhaseMap2rads(), name='phmap2rads',
117+
run_without_submitting=True)
102118

103119
# FSL PRELUDE will perform phase-unwrapping
104120
prelude = pe.Node(fsl.PRELUDE(), name='prelude')
105121

122+
recenter = pe.Node(niu.Function(function=_recenter),
123+
name='recenter', run_without_submitting=True)
124+
106125
denoise = pe.Node(fsl.SpatialFilter(operation='median', kernel_shape='sphere',
107126
kernel_size=3), name='denoise')
108127

@@ -112,21 +131,17 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
112131

113132
compfmap = pe.Node(Phasediff2Fieldmap(), name='compfmap')
114133

115-
# The phdiff2fmap interface is equivalent to:
116-
# rad2rsec (using rads2radsec from niflow.nipype1.workflows.dmri.fsl.utils)
117-
# pre_fugue = pe.Node(fsl.FUGUE(save_fmap=True), name='ComputeFieldmapFUGUE')
118-
# rsec2hz (divide by 2pi)
119-
120134
workflow.connect([
121135
(inputnode, compfmap, [('metadata', 'metadata')]),
122136
(inputnode, magmrg, [('magnitude', 'in_files')]),
123137
(magmrg, n4, [('out_avg', 'input_image')]),
124138
(n4, prelude, [('output_image', 'magnitude_file')]),
125139
(n4, bet, [('output_image', 'in_file')]),
126140
(bet, prelude, [('mask_file', 'mask_file')]),
127-
(inputnode, pha2rads, [('phasediff', 'in_file')]),
128-
(pha2rads, prelude, [('out', 'phase_file')]),
129-
(prelude, denoise, [('unwrapped_phase_file', 'in_file')]),
141+
(inputnode, phmap2rads, [('phasediff', 'in_file')]),
142+
(phmap2rads, prelude, [('out_file', 'phase_file')]),
143+
(prelude, recenter, [('unwrapped_phase_file', 'in_file')]),
144+
(recenter, denoise, [('out', 'in_file')]),
130145
(denoise, demean, [('out_file', 'in_file')]),
131146
(demean, cleanup_wf, [('out', 'inputnode.in_file')]),
132147
(bet, cleanup_wf, [('mask_file', 'inputnode.in_mask')]),
@@ -137,3 +152,22 @@ def init_phdiff_wf(omp_nthreads, name='phdiff_wf'):
137152
])
138153

139154
return workflow
155+
156+
157+
def _recenter(in_file):
158+
"""Recenter the phase-map distribution to the -pi..pi range."""
159+
from os import getcwd
160+
import numpy as np
161+
import nibabel as nb
162+
from nipype.utils.filemanip import fname_presuffix
163+
164+
nii = nb.load(in_file)
165+
data = nii.get_fdata(dtype='float32')
166+
msk = data != 0
167+
msk[data == 0] = False
168+
data[msk] -= np.median(data[msk])
169+
170+
out_file = fname_presuffix(in_file, suffix='_recentered',
171+
newpath=getcwd())
172+
nb.Nifti1Image(data, nii.affine, nii.header).to_filename(out_file)
173+
return out_file

0 commit comments

Comments
 (0)