Skip to content

Commit 68d71d8

Browse files
committed
Merge pull request #525 from oesteban/master
New EPI dewarping workflow, in a similar manner to eddy_current correction
2 parents 2cfa242 + a9a8f19 commit 68d71d8

File tree

3 files changed

+315
-4
lines changed

3 files changed

+315
-4
lines changed

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Next release
33
* ENH: New interfaces: nipy.Trim
44

55
* ENH: Allow control over terminal output for commandline interfaces
6+
* ENH: New workflows: susceptibility correction for EPI imaging based on epidewarp.fsl
7+
updated to support both dMRI and fMRI, and new parameters.
8+
* ENH: Minor improvements to FSL's FUGUE interface
69

710
Release 0.7.0 (Dec 18, 2012)
811
============================

nipype/interfaces/fsl/preprocess.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,12 @@ class FUGUEInputSpec(FSLCommandInputSpec):
10921092
desc='filename of input volume')
10931093
unwarped_file = File(argstr='--unwarp=%s', genfile=True,
10941094
desc='apply unwarping and save as filename', hash_files=False)
1095+
1096+
save_warped = traits.Bool( desc='apply forward warp and save' )
1097+
1098+
warped_file = File(argstr='--warp=%s', genfile=True,
1099+
desc='apply forward warp and save as filename', hash_files=False)
1100+
10951101
phasemap_file = File(exists=True, argstr='--phasemap=%s',
10961102
desc='filename for input phase image')
10971103
dwell_to_asym_ratio = traits.Float(argstr='--dwelltoasym=%.10f',
@@ -1104,8 +1110,12 @@ class FUGUEInputSpec(FSLCommandInputSpec):
11041110
desc='filename for saving fieldmap (rad/s)', hash_files=False)
11051111
fmap_in_file = File(exists=True, argstr='--loadfmap=%s',
11061112
desc='filename for loading fieldmap (rad/s)')
1107-
shift_out_file = File(argstr='--saveshift=%s',
1108-
desc='filename for saving pixel shift volume', hash_files=False)
1113+
1114+
save_shift = traits.Bool( desc='output pixel shift volume' )
1115+
1116+
shift_out_file = traits.File( argstr='--saveshift=%s', genfile=True,
1117+
desc='filename for saving pixel shift volume', hash_files=False)
1118+
11091119
shift_in_file = File(exists=True, argstr='--loadshift=%s',
11101120
desc='filename for reading pixel shift volume')
11111121
median_2dfilter = traits.Bool(argstr='--median',
@@ -1154,6 +1164,8 @@ class FUGUEInputSpec(FSLCommandInputSpec):
11541164

11551165
class FUGUEOutputSpec(TraitedSpec):
11561166
unwarped_file = File(exists=True, desc='unwarped file')
1167+
shift_out_file = File( desc='voxel shift map file' )
1168+
warped_file = File( desc='warped file' )
11571169

11581170

11591171
class FUGUE(FSLCommand):
@@ -1174,18 +1186,52 @@ def __init__(self, **kwargs):
11741186
super(FUGUE, self).__init__(**kwargs)
11751187
warn('This interface has not been fully tested. Please report any failures.')
11761188

1189+
def _parse_inputs(self, skip=None):
1190+
skip = []
1191+
if not isdefined( self.inputs.save_shift ) or not self.inputs.save_shift:
1192+
skip.append('shift_out_file')
1193+
if not isdefined( self.inputs.save_warped ) or not self.inputs.save_warped:
1194+
skip.append('warped_file')
1195+
else:
1196+
skip.append('unwarped_file')
1197+
1198+
return super(FUGUE,self)._parse_inputs(skip=skip)
1199+
11771200
def _list_outputs(self):
11781201
outputs = self._outputs().get()
11791202
out_file = self.inputs.unwarped_file
11801203
if not isdefined(out_file):
11811204
out_file = self._gen_fname(self.inputs.in_file,
11821205
suffix='_unwarped')
11831206
outputs['unwarped_file'] = os.path.abspath(out_file)
1207+
1208+
if isdefined( self.inputs.save_shift ) and self.inputs.save_shift:
1209+
shift_out = self.inputs.shift_out_file
1210+
if not isdefined(shift_out):
1211+
shift_out = self._gen_fname( self.inputs.in_file, suffix='_shift' )
1212+
1213+
outputs['shift_out_file'] = os.path.abspath( shift_out )
1214+
1215+
if isdefined( self.inputs.save_warped ) and self.inputs.save_warped:
1216+
warped_out = self.inputs.warped_file
1217+
if not isdefined(warped_out):
1218+
warped_out = self._gen_fname( self.inputs.in_file, suffix='_fwdwarp' )
1219+
1220+
outputs['warped_file'] = os.path.abspath( warped_out )
1221+
del outputs['unwarped_file']
1222+
11841223
return outputs
11851224

11861225
def _gen_filename(self, name):
11871226
if name == 'unwarped_file':
11881227
return self._list_outputs()['unwarped_file']
1228+
1229+
if name == 'warped_file':
1230+
return self._list_outputs()['warped_file']
1231+
1232+
if name == 'shift_out_file':
1233+
return self._list_outputs()['shift_out_file']
1234+
11891235
return None
11901236

11911237

nipype/workflows/dmri/fsl/dti.py

Lines changed: 264 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
# coding: utf-8
2+
13
import nipype.pipeline.engine as pe
24
import nipype.interfaces.utility as util
35
import nipype.interfaces.fsl as fsl
4-
6+
import os
57

68
def transpose(samples_over_fibres):
79
import numpy as np
@@ -214,4 +216,264 @@ def create_eddy_correct_pipeline(name="eddy_correct"):
214216

215217
pipeline.connect([(merge, outputnode, [("merged_file", "eddy_corrected")])])
216218

217-
return pipeline
219+
return pipeline
220+
221+
def create_epi_correct_pipeline(name="epi_correct", register_to_ref=False ):
222+
""" Replaces the epidewarp.fsl script (http://www.nmr.mgh.harvard.edu/~greve/fbirn/b0/epidewarp.fsl)
223+
for EPI distortion correction with the fieldmap information in
224+
dMRI and fMRI (Jezzard et al., MRM 1995 ) using FSL's FUGUE.
225+
226+
Example
227+
-------
228+
229+
>>> nipype_epicorrect = create_epi_correct_pipeline("nipype_epicorrect")
230+
>>> nipype_epicorrect.inputs.inputnode.in_file = 'epi_data.nii'
231+
>>> nipype_epicorrect.inputs.inputnode.fieldmap_mag = 'magnitude.nii'
232+
>>> nipype_epicorrect.inputs.inputnode.fieldmap_pha = 'phase.nii'
233+
>>> nipype_epicorrect.inputs.inputnode.te_diff = 2.46
234+
>>> nipype_epicorrect.inputs.inputnode.epi_echospacing = 0.51
235+
>>> nipype_epicorrect.inputs.inputnode.epi_rev_encoding = False
236+
>>> nipype_epicorrect.inputs.inputnode.ref_volume = 0
237+
>>> nipype_epicorrect.inputs.inputnode.epi_parallel = True
238+
>>> nipype_epicorrect.run() # doctest: +SKIP
239+
240+
Inputs::
241+
242+
inputnode.in_file - The volume acquired with EPI sequence
243+
inputnode.fieldmap_mag - The magnitude of the fieldmap
244+
inputnode.fieldmap_pha - The phase difference of the fieldmap
245+
inputnode.te_diff - Time difference between TE in ms.
246+
inputnode.epi_echospacing - The echo spacing (aka dwell time) in the EPI sequence
247+
inputnode.epi_ph_encoding_dir - The phase encoding direction in EPI acquisition (default y)
248+
inputnode.epi_rev_encoding - True if it is acquired with reverse encoding
249+
inputnode.epi_parallel - True if EPI was acquired in a parallel imaging scheme
250+
inputnode.vsm_sigma - Sigma value of the gaussian smoothing filter applied to the vsm (voxel shift map)
251+
inputnode.ref_volume - The reference volume (B=0 in dMRI or a central frame in fMRI)
252+
253+
254+
Outputs::
255+
256+
outputnode.epi_corrected
257+
"""
258+
259+
inputnode = pe.Node(interface = util.IdentityInterface(fields=["in_file",
260+
"fieldmap_mag",
261+
"fieldmap_pha",
262+
"te_diff",
263+
"epi_echospacing",
264+
"epi_ph_encoding_dir",
265+
"epi_rev_encoding",
266+
"epi_parallel",
267+
"vsm_sigma",
268+
"ref_volume",
269+
"unwarp_direction"
270+
]), name="inputnode")
271+
272+
pipeline = pe.Workflow(name=name)
273+
274+
275+
matrix_file = os.path.abspath( './flirt.txt' )
276+
277+
# Keep first frame from magnitude
278+
select_mag = pe.Node( interface=fsl.utils.ExtractROI(t_size=1,t_min=0), name="select_magnitude" )
279+
280+
# mask_brain
281+
mask_mag = pe.Node( interface=fsl.BET(mask=True),name='mask_magnitude' )
282+
mask_mag_dil = pe.Node( interface=util.Function( input_names=["in_file"], output_names=["out_file"], function=_dilate_mask), name='mask_dilate' )
283+
284+
# Compute dwell time
285+
dwell_time = pe.Node( interface=util.Function( input_names=["dwell_time","is_parallel","is_reverse_encoding"], output_names=["dwell_time"], function=_compute_dwelltime), name='dwell_time')
286+
287+
# Normalize phase diff to be [-pi, pi)
288+
norm_pha = pe.Node( interface=util.Function( input_names=["in_file"], output_names=["out_file"], function=_prepare_phasediff ), name='normalize_phasediff')
289+
# Execute FSL PRELUDE: prelude -p %s -a %s -o %s -f -v -m %s
290+
prelude = pe.Node( interface=fsl.PRELUDE(process3d=True), name='phase_unwrap' )
291+
fill_phase = pe.Node( interface=util.Function( input_names=["in_file"], output_names=["out_file"], function=_fill_phase ), name='fill_phasediff' )
292+
293+
# to assure that vsm is same dimension as mag. The input only affects the output dimension.
294+
# The content of the input has no effect on the vsm. The de-warped mag volume is
295+
# meaningless and will be thrown away
296+
# fugue -i %s -u %s -p %s --dwell=%s --asym=%s --mask=%s --saveshift=%s % ( mag_name, magdw_name, ph_name, esp, tediff, mask_name, vsmmag_name)
297+
vsm = pe.Node( interface=fsl.FUGUE(save_shift=True), name="generate_vsm" )
298+
vsm_mean = pe.Node( interface=util.Function( input_names=["in_file","mask_file","in_unwarped"], output_names=["out_file"], function=_vsm_remove_mean), name="vsm_mean_shift" )
299+
300+
# fugue_epi
301+
dwi_split = pe.Node( interface=util.Function( input_names=['in_file'], output_names=['out_files'], function=_split_dwi ), name='dwi_split' )
302+
# 'fugue -i %s -u %s --loadshift=%s --mask=%s' % ( vol_name, out_vol_name, vsm_name, mask_name )
303+
dwi_applyxfm = pe.MapNode( interface=fsl.FUGUE(icorr=True,save_shift=False), iterfield=['in_file'], name='dwi_fugue' )
304+
# Merge back all volumes
305+
dwi_merge = pe.Node( interface=fsl.utils.Merge(dimension='t' ), name='dwi_merge')
306+
307+
outputnode = pe.Node(interface = util.IdentityInterface(fields=["epi_corrected"]),
308+
name="outputnode")
309+
310+
311+
pipeline.connect([
312+
(inputnode, dwell_time, [('epi_echospacing','dwell_time'), ('epi_parallel','is_parallel'),('epi_rev_encoding','is_reverse_encoding') ])
313+
,(inputnode, select_mag, [('fieldmap_mag','in_file')] )
314+
,(inputnode, norm_pha, [('fieldmap_pha','in_file')] )
315+
,(select_mag, mask_mag, [('roi_file', 'in_file')] )
316+
,(mask_mag, mask_mag_dil, [('mask_file','in_file')] )
317+
,(select_mag, prelude, [('roi_file', 'magnitude_file') ])
318+
,(norm_pha, prelude, [('out_file', 'phase_file' ) ])
319+
,(mask_mag_dil, prelude, [('out_file', 'mask_file' ) ])
320+
,(prelude, fill_phase, [('unwrapped_phase_file','in_file')] )
321+
,(inputnode, vsm, [('fieldmap_mag', 'in_file')])
322+
,(fill_phase, vsm, [('out_file','phasemap_file')]) #, (('out_file',_genvsmpath),'shift_out_file') ] )
323+
,(inputnode, vsm, [(('te_diff',_ms2sec),'asym_se_time'),('vsm_sigma','smooth2d')])
324+
,(dwell_time, vsm, [(('dwell_time',_ms2sec),'dwell_time') ])
325+
,(mask_mag_dil, vsm, [('out_file','mask_file')] )
326+
,(mask_mag_dil, vsm_mean, [('out_file','mask_file')] )
327+
,(vsm, vsm_mean, [('unwarped_file','in_unwarped'),('shift_out_file','in_file')])
328+
,(inputnode, dwi_split, [('in_file','in_file')])
329+
,(dwi_split, dwi_applyxfm, [('out_files','in_file')])
330+
,(dwi_applyxfm, dwi_merge, [('unwarped_file','in_files')])
331+
,(dwi_merge, outputnode, [('merged_file','epi_corrected')])
332+
])
333+
334+
if register_to_ref:
335+
# register to ref volume
336+
""" Register magfw to example epi. There are some parameters here that may need to be tweaked. Should probably strip the mag
337+
Pre-condition: forward warp the mag in order to reg with func. What does mask do here?
338+
"""
339+
# Select reference volume from EPI (B0 in dMRI and a middle frame in fMRI)
340+
select_epi = pe.Node( interface=fsl.utils.ExtractROI(t_size=1), name="select_epi" )
341+
342+
343+
# fugue -i %s -w %s --loadshift=%s --mask=%s % ( mag_name, magfw_name, vsmmag_name, mask_name ), log ) # Forward Map
344+
vsm_fwd = pe.Node( interface=fsl.FUGUE(save_warped=True), name="vsm_fwd")
345+
vsm_reg = pe.Node( interface=fsl.FLIRT( bins=256, cost='corratio', dof=6, interp='trilinear', searchr_x=[-10,10], searchr_y=[-10,10], searchr_z=[-10,10] ), name="vsm_registration" )
346+
# 'flirt -in %s -ref %s -out %s -init %s -applyxfm' % ( vsmmag_name, ref_epi, vsmmag_name, magfw_mat_out )
347+
vsm_applyxfm = pe.Node( interface=fsl.ApplyXfm(), name='vsm_apply_xfm')
348+
# 'flirt -in %s -ref %s -out %s -init %s -applyxfm' % ( mask_name, ref_epi, mask_name, magfw_mat_out )
349+
msk_applyxfm = pe.Node( interface=fsl.ApplyXfm(), name='msk_apply_xfm')
350+
351+
pipeline.connect([
352+
(inputnode, select_epi, [('in_file','in_file'), ('ref_volume','t_min')] )
353+
,(select_epi, vsm_reg, [('roi_file','reference')])
354+
,(vsm, vsm_fwd, [('shift_out_file','shift_in_file')])
355+
,(mask_mag_dil, vsm_fwd, [('out_file','mask_file')] )
356+
,(inputnode, vsm_fwd, [('fieldmap_mag', 'in_file')])
357+
,(vsm_fwd, vsm_reg, [('warped_file','in_file')])
358+
,(vsm_reg, msk_applyxfm, [('out_matrix_file','in_matrix_file')])
359+
,(select_epi, msk_applyxfm, [('roi_file','reference')])
360+
,(mask_mag_dil, msk_applyxfm, [('out_file','in_file') ])
361+
,(vsm_reg, vsm_applyxfm, [('out_matrix_file','in_matrix_file')])
362+
,(select_epi, vsm_applyxfm, [('roi_file','reference')])
363+
,(vsm_mean, vsm_applyxfm, [('out_file','in_file')])
364+
,(msk_applyxfm, dwi_applyxfm, [('out_file','mask_file')])
365+
,(vsm_applyxfm, dwi_applyxfm, [('out_file','shift_in_file')])
366+
])
367+
else:
368+
pipeline.connect([
369+
(mask_mag_dil, dwi_applyxfm, [('out_file','mask_file')])
370+
,(vsm_mean, dwi_applyxfm, [('out_file','shift_in_file')])
371+
])
372+
373+
374+
return pipeline
375+
376+
def _compute_dwelltime( dwell_time=0.68, is_parallel=False, is_reverse_encoding=False ):
377+
if is_parallel:
378+
dwell_time*=0.5
379+
380+
if is_reverse_encoding:
381+
dwell_time*=-1.0
382+
383+
return dwell_time
384+
385+
def _prepare_phasediff( in_file ):
386+
import nibabel as nib
387+
import os
388+
import numpy as np
389+
img = nib.load( in_file )
390+
max_diff = np.max( img.get_data().reshape(-1) )
391+
min_diff = np.min( img.get_data().reshape(-1) )
392+
A = ( 2.0 * np.pi )/( max_diff-min_diff )
393+
B = np.pi - ( A * max_diff )
394+
diff_norm = img.get_data() * A + B
395+
396+
name,fext = os.path.splitext( os.path.basename(in_file) )
397+
if fext == '.gz': name,_ = os.path.splitext(name)
398+
out_file = os.path.abspath( './%s_2pi.nii.gz' % name )
399+
nib.save( nib.Nifti1Image( diff_norm, img.get_affine(), img.get_header() ), out_file )
400+
return out_file
401+
402+
def _dilate_mask ( in_file, iterations=4 ):
403+
import nibabel as nib
404+
import scipy.ndimage as ndimage
405+
import os
406+
img = nib.load( in_file )
407+
img._data = ndimage.binary_dilation( img.get_data(), iterations=iterations )
408+
409+
name,fext = os.path.splitext( os.path.basename(in_file) )
410+
if fext == '.gz': name,_ = os.path.splitext(name)
411+
out_file = os.path.abspath( './%s_dil.nii.gz' % name )
412+
nib.save( img, out_file )
413+
return out_file
414+
415+
416+
def _fill_phase( in_file ):
417+
import nibabel as nib
418+
import os
419+
import numpy as np
420+
img = nib.load( in_file )
421+
dumb_img = nib.Nifti1Image( np.zeros( img.get_shape() ), img.get_affine(), img.get_header() )
422+
out_nii = nib.funcs.concat_images( ( img, dumb_img ) )
423+
name,fext = os.path.splitext( os.path.basename(in_file) )
424+
if fext == '.gz': name,_ = os.path.splitext(name)
425+
out_file = os.path.abspath( './%s_phase_unwrapped.nii.gz' % name )
426+
nib.save( out_nii, out_file )
427+
return out_file
428+
429+
def _vsm_remove_mean( in_file, mask_file, in_unwarped ):
430+
import nibabel as nib
431+
import os
432+
import numpy as np
433+
import numpy.ma as ma
434+
img = nib.load( in_file )
435+
msk = nib.load( mask_file ).get_data()
436+
img_data = img.get_data()
437+
img_data[ msk==0 ] = 0
438+
vsmmag_masked = ma.masked_values( img_data.reshape(-1), 0.0 )
439+
vsmmag_masked = vsmmag_masked - vsmmag_masked.mean()
440+
img._data = vsmmag_masked.reshape( img.get_shape() )
441+
name,fext = os.path.splitext( os.path.basename(in_file) )
442+
if fext == '.gz': name,_ = os.path.splitext(name)
443+
out_file = os.path.abspath( './%s_demeaned.nii.gz' % name )
444+
nib.save( img, out_file )
445+
return out_file
446+
447+
#def _generate_fwdmap( in_file, mask_file, shift_in_file, in_unwarped):
448+
# import os
449+
# out_file = os.path.abspath( './fwdmap.nii.gz' )
450+
# cmd = 'fugue -i %s -w %s --loadshift=%s --mask=%s' % ( in_file, out_file, shift_in_file, mask_file )
451+
# print " Running:",cmd
452+
# os.system(cmd)
453+
# return out_file
454+
455+
def _genvsmpath( in_file ):
456+
import os
457+
name,fext = os.path.splitext( os.path.basename(in_file) )
458+
if fext == '.gz': name,_ = os.path.splitext(name)
459+
out_file = os.path.abspath( os.path.join( os.path.dirname(in_file), '%s_vsm.nii.gz' % name ))
460+
return out_file
461+
462+
def _ms2sec( val ):
463+
return val*1e-3;
464+
465+
def _yield( in_file, in_block ):
466+
return in_file
467+
468+
def _split_dwi( in_file ):
469+
import nibabel as nib
470+
import os
471+
out_files = []
472+
frames = nib.funcs.four_to_three( nib.load( in_file ) )
473+
name,fext = os.path.splitext( os.path.basename(in_file) )
474+
if fext == '.gz': name,_ = os.path.splitext(name)
475+
for i,frame in enumerate(frames):
476+
out_file = os.path.abspath( './%s_%03d.nii.gz' % (name,i) )
477+
nib.save( frame, out_file )
478+
out_files.append( out_file )
479+
return out_files

0 commit comments

Comments
 (0)