Skip to content

Commit f45ce74

Browse files
committed
enh:implementing new fmb workflow
1 parent 14dd4b3 commit f45ce74

File tree

3 files changed

+122
-143
lines changed

3 files changed

+122
-143
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from epi import motion_correct
1+
from epi import hmc_pipeline, ecc_pipeline, sdc_fmb

nipype/workflows/dmri/preprocess/epi.py

Lines changed: 120 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,40 @@
33
import nipype.pipeline.engine as pe
44
import nipype.interfaces.utility as niu
55
import nipype.interfaces.fsl as fsl
6+
import nipype.interfaces.ants as ants
67
import os
78

8-
def motion_correct(name='motion_correct'):
9-
"""Creates a pipeline that corrects for head motion artifacts in dMRI sequences.
9+
from .utils import *
10+
11+
def hmc_pipeline(name='motion_correct'):
12+
"""
13+
HMC stands for head-motion correction.
14+
15+
Creates a pipeline that corrects for head motion artifacts in dMRI sequences.
1016
It takes a series of diffusion weighted images and rigidly co-registers
11-
them to one reference image. Finally, the `b`-matrix is rotated accordingly [1]_
17+
them to one reference image. Finally, the `b`-matrix is rotated accordingly [Leemans09]_
1218
making use of the rotation matrix obtained by FLIRT.
1319
14-
Search angles have been limited to 3.5 degrees, based on results in [2]_.
20+
Search angles have been limited to 3.5 degrees, based on results in [Yendiki13]_.
1521
1622
A list of rigid transformation matrices is provided, so that transforms can be
1723
chained. This is useful to correct for artifacts with only one interpolation process (as
1824
previously discussed `here <https://github.com/nipy/nipype/pull/530#issuecomment-14505042>`_),
19-
and also to compute nuisance regressors as proposed by [2]_.
25+
and also to compute nuisance regressors as proposed by [Yendiki13]_.
2026
2127
.. warning:: This workflow rotates the `b`-vectors, so please be advised
2228
that not all the dicom converters ensure the consistency between the resulting
2329
nifti orientation and the gradients table (e.g. dcm2nii checks it).
2430
2531
.. admonition:: References
2632
27-
.. [1] Leemans A, and Jones DK, Magn Reson Med. 2009 Jun;61(6):1336-49.
28-
doi: 10.1002/mrm.21890.
33+
.. [Leemans09] Leemans A, and Jones DK, `The B-matrix must be rotated when correcting
34+
for subject motion in DTI data <http://dx.doi.org/10.1002/mrm.21890>`_,
35+
Magn Reson Med. 61(6):1336-49. 2009. doi: 10.1002/mrm.21890.
2936
30-
.. [2] Yendiki A et al., Spurious group differences due to head motion in a
31-
diffusion MRI study. Neuroimage. 2013 Nov 21;88C:79-90.
32-
doi: 10.1016/j.neuroimage.2013.11.027
37+
.. [Yendiki13] Yendiki A et al., `Spurious group differences due to head motion in
38+
a diffusion MRI study <http://dx.doi.org/10.1016/j.neuroimage.2013.11.027>`_.
39+
Neuroimage. 21(88C):79-90. 2013. doi: 10.1016/j.neuroimage.2013.11.027
3340
3441
Example
3542
-------
@@ -91,14 +98,17 @@ def motion_correct(name='motion_correct'):
9198
])
9299
return wf
93100

94-
def eddy_correct(name='eddy_correct'):
95-
"""Creates a pipeline that corrects for artifacts induced by Eddy currents in dMRI
101+
def ecc_pipeline(name='eddy_correct'):
102+
"""
103+
ECC stands for Eddy currents correction.
104+
105+
Creates a pipeline that corrects for artifacts induced by Eddy currents in dMRI
96106
sequences.
97107
It takes a series of diffusion weighted images and linearly co-registers
98108
them to one reference image (the average of all b0s in the dataset).
99109
100-
DWIs are also modulated by the determinant of the Jacobian as indicated by [3]_ and
101-
[4]_.
110+
DWIs are also modulated by the determinant of the Jacobian as indicated by [Jones10]_ and
111+
[Rohde04]_.
102112
103113
A list of rigid transformation matrices can be provided, sourcing from a
104114
:func:`.motion_correct` workflow, to initialize registrations in a *motion free*
@@ -110,12 +120,12 @@ def eddy_correct(name='eddy_correct'):
110120
111121
.. admonition:: References
112122
113-
.. [3] Jones DK, `The signal intensity must be modulated by the determinant of \
114-
the Jacobian when correcting for eddy currents in diffusion MRI \
123+
.. [Jones10] Jones DK, `The signal intensity must be modulated by the determinant of
124+
the Jacobian when correcting for eddy currents in diffusion MRI
115125
<http://cds.ismrm.org/protected/10MProceedings/files/1644_129.pdf>`_,
116126
Proc. ISMRM 18th Annual Meeting, (2010).
117127
118-
.. [4] Rohde et al., `Comprehensive Approach for Correction of Motion and Distortion \
128+
.. [Rohde04] Rohde et al., `Comprehensive Approach for Correction of Motion and Distortion \
119129
in Diffusion-Weighted MRI \
120130
<http://stbb.nichd.nih.gov/pdf/com_app_cor_mri04.pdf>`_, MRM 51:103-114 (2004).
121131
@@ -196,6 +206,99 @@ def eddy_correct(name='eddy_correct'):
196206
])
197207
return wf
198208

209+
def sdc_fmb(name='fmb_correction',
210+
fugue_params=dict(smooth3d=2.0, despike_2dfilter=True),
211+
bmap_params=dict(delta_te=2.46e-3),
212+
epi_params=dict()):
213+
"""
214+
SDC stands for susceptibility distortion correction. FMB stands for fieldmap-based.
215+
216+
The fieldmap based method (FMB) implements SDC by using a mapping of the B0 field
217+
as proposed by [Jezzard95]_. This workflow uses the implementation of FSL
218+
(`FUGUE <http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FUGUE>`_). Phase unwrapping is performed
219+
using `PRELUDE <http://fsl.fmrib.ox.ac.uk/fsl/fsl-4.1.9/fugue/prelude.html>`_
220+
[Jenkinson03]_.
221+
222+
223+
.. admonition:: References
224+
225+
.. [Jezzard95] Jezzard P, and Balaban RS, `Correction for geometric distortion in echo
226+
planar images from B0 field variations <http://dx.doi.org/10.1002/mrm.1910340111>`_,
227+
MRM 34(1):65-73. (1995). doi: 10.1002/mrm.1910340111.
228+
229+
.. [Jenkinson03] Jenkinson M., `Fast, automated, N-dimensional phase-unwrapping algorithm
230+
<http://dx.doi.org/10.1002/mrm.10354>`_, MRM 49(1):193-197, 2003, doi: 10.1002/mrm.10354.
231+
232+
"""
233+
inputnode = pe.Node(niu.IdentityInterface(fields=['in_file', 'in_bval', 'in_mask',
234+
'bmap_pha', 'bmap_mag']),
235+
name='inputnode')
236+
237+
238+
firstmag = pe.Node(fsl.ExtractROI(t_min=0, t_size=1), name='GetFirst')
239+
n4 = pe.Node(ants.N4BiasFieldCorrection(dimension=3), name='Bias')
240+
bet = pe.Node(fsl.BET(frac=0.4, mask=True), name='BrainExtraction')
241+
dilate = pe.Node(fsl.maths.MathsCommand(nan2zeros=True,
242+
args='-kernel sphere 5 -dilM'), name='MskDilate')
243+
pha2rads = pe.Node(niu.Function(input_names=['in_file'], output_names=['out_file'],
244+
function=siemens2rads), name='PreparePhase')
245+
prelude = pe.Node(fsl.PRELUDE(process3d=True), name='PhaseUnwrap')
246+
rad2rsec = pe.Node(niu.Function(input_names=['in_file', 'delta_te'],
247+
output_names=['out_file'], function=rads2radsec), name='ToRadSec')
248+
rad2rsec.inputs.delta_te = bmap_params['delta_te']
249+
250+
avg_b0 = pe.Node(niu.Function(input_names=['in_dwi', 'in_bval'],
251+
output_names=['out_file'], function=b0_average), name='b0_avg')
252+
253+
flirt = pe.Node(fsl.FLIRT(interp='spline', cost='normmi', cost_func='normmi',
254+
dof=6, bins=64, save_log=True, padding_size=10,
255+
searchr_x=[-4,4], searchr_y=[-4,4], searchr_z=[-4,4],
256+
fine_search=1, coarse_search=10),
257+
name='BmapMag2B0')
258+
applyxfm = pe.Node(fsl.ApplyXfm(interp='spline', padding_size=10, apply_xfm=True),
259+
name='BmapPha2B0')
260+
261+
pre_fugue = pe.Node(fsl.FUGUE(save_fmap=True), name='PreliminaryFugue')
262+
demean = pe.Node(niu.Function(input_names=['in_file', 'in_mask'],
263+
output_names=['out_file'], function=demean_image),
264+
name='DemeanFmap')
265+
266+
cleanup = cleanup_edge_pipeline()
267+
268+
wf = pe.Workflow(name=name)
269+
wf.connect([
270+
(inputnode, pha2rads, [('bmap_pha', 'in_file')])
271+
,(inputnode, firstmag, [('bmap_mag', 'in_file')])
272+
,(inputnode, avg_b0, [('in_file', 'in_dwi'),
273+
('in_bval', 'in_bval')])
274+
,(firstmag, n4, [('roi_file', 'input_image')])
275+
,(n4, bet, [('output_image', 'in_file')])
276+
,(bet, dilate, [('mask_file', 'in_file')])
277+
,(pha2rads, prelude, [('out_file', 'phase_file')])
278+
,(n4, prelude, [('output_image', 'magnitude_file')])
279+
,(dilate, prelude, [('out_file', 'mask_file')])
280+
,(prelude, rad2rsec, [('unwrapped_phase_file', 'in_file')])
281+
282+
,(avg_b0, flirt, [('out_file', 'reference')])
283+
,(inputnode, flirt, [('in_mask', 'ref_weight')])
284+
,(n4, flirt, [('output_image', 'in_file')])
285+
,(dilate, flirt, [('out_file', 'in_weight')])
286+
287+
,(avg_b0, applyxfm, [('out_file', 'reference')])
288+
,(rad2rsec, applyxfm, [('out_file', 'in_file')])
289+
,(flirt, applyxfm, [('out_matrix_file', 'in_matrix_file')])
290+
291+
,(applyxfm, pre_fugue, [('out_file', 'fmap_in_file')])
292+
,(inputnode, pre_fugue, [('in_mask', 'mask_file')])
293+
294+
,(pre_fugue, demean, [('fmap_out_file', 'in_file')])
295+
,(inputnode, demean, [('in_mask', 'in_mask')])
296+
297+
,(demean, cleanup, [('out_file', 'inputnode.in_file')])
298+
,(inputnode, cleanup, [('in_mask', 'inputnode.in_mask')])
299+
])
300+
return wf
301+
199302

200303
def _checkrnum(ref_num):
201304
from nipype.interfaces.base import isdefined
@@ -226,132 +329,7 @@ def _nonb0(in_bval):
226329
bvals = np.loadtxt(in_bval)
227330
return np.where(bvals!=0)[0].tolist()
228331

229-
def recompose_dwi(in_dwi, in_bval, in_corrected, out_file=None):
230-
"""
231-
Recompose back the dMRI data accordingly the b-values table after EC correction
232-
"""
233-
import numpy as np
234-
import nibabel as nb
235-
import os.path as op
236-
237-
if out_file is None:
238-
fname,ext = op.splitext(op.basename(in_dwi))
239-
if ext == ".gz":
240-
fname,ext2 = op.splitext(fname)
241-
ext = ext2 + ext
242-
out_file = op.abspath("%s_eccorrect%s" % (fname, ext))
243-
244-
im = nb.load(in_dwi)
245-
dwidata = im.get_data()
246-
bvals = np.loadtxt(in_bval)
247-
non_b0 = np.where(bvals!=0)[0].tolist()
248-
249-
if len(non_b0)!=len(in_corrected):
250-
raise RuntimeError('Length of DWIs in b-values table and after correction should match')
251-
252-
for bindex, dwi in zip(non_b0, in_corrected):
253-
dwidata[...,bindex] = nb.load(dwi).get_data()
254-
255-
nb.Nifti1Image(dwidata, im.get_affine(), im.get_header()).to_filename(out_file)
256-
return out_file
257-
258-
def recompose_xfm(in_bval, in_xfms):
259-
"""
260-
Insert identity transformation matrices in b0 volumes to build up a list
261-
"""
262-
import numpy as np
263-
import os.path as op
264-
265-
bvals = np.loadtxt(in_bval)
266-
out_matrix = np.array([np.eye(4)] * len(bvals))
267-
xfms = iter([np.loadtxt(xfm) for xfm in in_xfms])
268-
out_files = []
269-
270-
for i, b in enumerate(bvals):
271-
if b == 0.0:
272-
mat = np.eye(4)
273-
else:
274-
mat = xfms.next()
275-
276-
out_name = 'eccor_%04d.mat' % i
277-
out_files.append(out_name)
278-
np.savetxt(out_name, mat)
279-
280-
return out_files
281-
282-
283332
def _xfm_jacobian(in_xfm):
284333
import numpy as np
285334
from math import fabs
286335
return [fabs(np.linalg.det(np.loadtxt(xfm))) for xfm in in_xfm]
287-
288-
289-
def b0_average(in_dwi, in_bval, out_file=None):
290-
"""
291-
A function that averages the *b0* volumes from a DWI dataset.
292-
293-
.. warning:: *b0* should be already registered (head motion artifact should
294-
be corrected).
295-
296-
"""
297-
import numpy as np
298-
import nibabel as nb
299-
import os.path as op
300-
301-
if out_file is None:
302-
fname,ext = op.splitext(op.basename(in_dwi))
303-
if ext == ".gz":
304-
fname,ext2 = op.splitext(fname)
305-
ext = ext2 + ext
306-
out_file = op.abspath("%s_avg_b0%s" % (fname, ext))
307-
308-
imgs = nb.four_to_three(nb.load(in_dwi))
309-
bval = np.loadtxt(in_bval)
310-
311-
b0s = []
312-
313-
for bval, img in zip(bval, imgs):
314-
if bval==0:
315-
b0s.append(img.get_data())
316-
317-
b0 = np.average(np.array(b0s), axis=0)
318-
319-
hdr = imgs[0].get_header().copy()
320-
nii = nb.Nifti1Image(b0, imgs[0].get_affine(), hdr)
321-
322-
nb.save(nii, out_file)
323-
return out_file
324-
325-
326-
def rotate_bvecs(in_bvec, in_matrix):
327-
"""
328-
Rotates the input bvec file accordingly with a list of matrices.
329-
330-
.. note:: the input affine matrix transforms points in the destination image to their \
331-
corresponding coordinates in the original image. Therefore, this matrix should be inverted \
332-
first, as we want to know the target position of :math:`\\vec{r}`.
333-
334-
"""
335-
import os
336-
import numpy as np
337-
338-
name, fext = os.path.splitext(os.path.basename(in_bvec))
339-
if fext == '.gz':
340-
name, _ = os.path.splitext(name)
341-
out_file = os.path.abspath('./%s_rotated.bvec' % name)
342-
bvecs = np.loadtxt(in_bvec).T
343-
new_bvecs = []
344-
345-
if len(bvecs) != len(in_matrix):
346-
raise RuntimeError('Number of b-vectors and rotation matrices should match.')
347-
348-
for bvec, mat in zip(bvecs, in_matrix):
349-
if np.all(bvec==0.0):
350-
new_bvecs.append(bvec)
351-
else:
352-
invrot = np.linalg.inv(np.loadtxt(mat))[:3,:3]
353-
newbvec = invrot.dot(bvec)
354-
new_bvecs.append((newbvec/np.linalg.norm(newbvec)))
355-
356-
np.savetxt(out_file, np.array(new_bvecs).T, fmt='%0.15f')
357-
return out_file

nipype/workflows/dmri/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def configuration(parent_package='',top_path=None):
99
config.add_subpackage('mrtrix')
1010
config.add_subpackage('fsl')
1111
config.add_subpackage('connectivity')
12+
config.add_subpackage('preprocess')
1213

1314
return config
1415

0 commit comments

Comments
 (0)