Skip to content

Commit b9576a5

Browse files
authored
Merge pull request #1283 from chrisfilo/enh/3dvolreg
[ENH] Replace FSL ``mcflirt`` with AFNI ``3dVolReg``
2 parents aa56aaa + bc48f64 commit b9576a5

File tree

18 files changed

+272
-137
lines changed

18 files changed

+272
-137
lines changed

.circleci/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ jobs:
537537
-e FMRIPREP_DEV 1 \
538538
--config $PWD/nipype.cfg -w /tmp/ds054/work \
539539
/tmp/data/ds054 /tmp/ds054/derivatives participant \
540-
--fs-no-reconall --sloppy \
540+
--fs-no-reconall --sloppy --hmc-use-mcflirt \
541541
--output-space T1w template \
542542
--template-resampling-grid 2mm \
543543
--mem_mb 4096 --nthreads 2 -vv
@@ -778,7 +778,7 @@ workflows:
778778
- get_regression_data
779779
filters:
780780
branches:
781-
ignore:
781+
ignore:
782782
- /docs?\/.*/
783783
- /ds005\/.*/
784784
- /ds054\/.*/

docs/citing.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ we recommend to include in your paper.
100100

101101
<p style="font-style: italic;">
102102
Functional data was <span class="slicetime_text_true">slice time corrected using
103-
<code>3dTshift</code> from AFNI v16.2.07 [11, RRID:SCR_005927]
104-
and </span>motion corrected using <code>mcflirt</code> (FSL v5.0.9 [9]).
103+
<code>3dTshift</code> (AFNI v16.2.07 [11, RRID:SCR_005927]) and </span>
104+
motion-corrected using <code>3dVolreg</code> (AFNI v16.2.07 [11, RRID:SCR_005927]).
105105
<span class="SDC_text_TOPUP" style="display: none">Distortion correction was performed
106106
using an implementation of the TOPUP technique [10] using <code>3dQwarp</code> (AFNI v16.2.07 [11]).</span>
107107
<span class="SDC_text_FUGUE" style="display: none">Distortion correction was performed using fieldmaps
@@ -257,7 +257,7 @@ Other relevant references
257257
.. [Power2017] Power JD, Plitt M, Kundu P, Bandettini PA, Martin A (2017) Temporal interpolation alters
258258
motion in fMRI scans: Magnitudes and consequences for artifact detection. PLOS ONE 12(9): e0182939.
259259
doi:`10.1371/journal.pone.0182939 <https://doi.org/10.1371/journal.pone.0182939>`_.
260-
261-
.. [Brett2001] Brett M, Leff AP, Rorden C, Ashburner J (2001) Spatial Normalization of Brain Images with
262-
Focal Lesions Using Cost Function Masking. NeuroImage 14(2)
260+
261+
.. [Brett2001] Brett M, Leff AP, Rorden C, Ashburner J (2001) Spatial Normalization of Brain Images with
262+
Focal Lesions Using Cost Function Masking. NeuroImage 14(2)
263263
doi:`10.006/nimg.2001.0845 <https://doi.org/10.1006/nimg.2001.0845>`_.

docs/outputs.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ Derivatives related to EPI files are in the ``func`` subfolder.
7777
Volumetric output spaces include ``T1w`` and ``MNI152NLin2009cAsym`` (default).
7878

7979
- ``*bold_space-<space>_brainmask.nii.gz`` Brain mask for EPI files, calculated by nilearn on the average EPI volume, post-motion correction
80-
- ``*bold_space-<space>_preproc.nii.gz`` Motion-corrected (using MCFLIRT for estimation and ANTs for interpolation) EPI file
81-
- (optional) ``*bold_space-<space>_variant-smoothAROMAnonaggr_preproc.nii.gz`` Motion-corrected (using MCFLIRT for estimation and ANTs for interpolation),
80+
- ``*bold_space-<space>_preproc.nii.gz`` Head-motion corrected EPI file
81+
- (optional) ``*bold_space-<space>_variant-smoothAROMAnonaggr_preproc.nii.gz`` Head-motion corrected,
8282
smoothed (6mm), and non-aggressively denoised (using AROMA) EPI file - currently produced only for the ``MNI152NLin2009cAsym`` space
8383

8484
Surface output spaces include ``fsnative`` (full density subject-specific mesh),
@@ -160,7 +160,7 @@ derivative of RMS variance over voxels (or :abbr:`DVARS (D referring to differen
160160
standardized (``stdDVARS``), non-standardized (``non-stdDVARS``), and voxel-wise standardized (``vx-wisestdDVARS``);
161161
the ``FrameDisplacement`` is a quantification of the estimated bulk-head motion; ``X``, ``Y``, ``Z``, ``RotX``,
162162
``RotY``, ``RotZ`` are the actual 6 rigid-body transform parameters estimated by FMRIPREP;
163-
the ``NonSteadyStateOutlierXX`` columns indicate non-steady state volumes with a single ``1`` value and ``0`` elsewhere (there
163+
the ``NonSteadyStateOutlierXX`` columns indicate non-steady state volumes with a single ``1`` value and ``0`` elsewhere (there
164164
is one ``NonSteadyStateOutlierXX`` column per outlier/volume); and finally six noise components ``aCompCorXX`` calculated using
165165
:abbr:`CompCor (Component Based Noise Correction Method)`
166166
and five noise components ``AROMAaggrCompXX`` if

docs/workflows.rst

Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,40 @@ is presented below:
1919
:simple_form: yes
2020

2121
from fmriprep.workflows.base import init_single_subject_wf
22-
wf = init_single_subject_wf(subject_id='test',
23-
name='single_subject_wf',
24-
task_id='',
25-
longitudinal=False,
26-
t2s_coreg=False,
27-
omp_nthreads=1,
28-
freesurfer=True,
29-
reportlets_dir='.',
30-
output_dir='.',
31-
bids_dir='.',
32-
skull_strip_template='OASIS',
33-
skull_strip_fixed_seed=False,
34-
template='MNI152NLin2009cAsym',
35-
output_spaces=['T1w', 'fsnative',
36-
'template', 'fsaverage5'],
37-
medial_surface_nan=False,
38-
cifti_output=False,
39-
ignore=[],
40-
debug=False,
41-
low_mem=False,
42-
anat_only=False,
43-
hires=True,
44-
use_bbr=True,
45-
bold2t1w_dof=9,
46-
fmap_bspline=False,
47-
fmap_demean=True,
48-
use_syn=True,
49-
force_syn=True,
50-
template_out_grid='native',
51-
use_aroma=False,
52-
aroma_melodic_dim=None,
53-
ignore_aroma_err=False)
22+
wf = init_single_subject_wf(
23+
subject_id='test',
24+
name='single_subject_wf',
25+
task_id='',
26+
longitudinal=False,
27+
t2s_coreg=False,
28+
omp_nthreads=1,
29+
freesurfer=True,
30+
reportlets_dir='.',
31+
output_dir='.',
32+
bids_dir='.',
33+
skull_strip_template='OASIS',
34+
skull_strip_fixed_seed=False,
35+
template='MNI152NLin2009cAsym',
36+
output_spaces=['T1w', 'fsnative', 'template', 'fsaverage5'],
37+
medial_surface_nan=False,
38+
cifti_output=False,
39+
ignore=[],
40+
debug=False,
41+
low_mem=False,
42+
anat_only=False,
43+
hires=True,
44+
use_bbr=True,
45+
bold2t1w_dof=9,
46+
fmap_bspline=False,
47+
fmap_demean=True,
48+
use_syn=True,
49+
force_syn=True,
50+
template_out_grid='native',
51+
use_aroma=False,
52+
aroma_melodic_dim=None,
53+
ignore_aroma_err=False,
54+
use_mcflirt=False,
55+
)
5456

5557

5658
T1w/T2w preprocessing
@@ -248,30 +250,32 @@ BOLD preprocessing
248250
:simple_form: yes
249251

250252
from fmriprep.workflows.bold import init_func_preproc_wf
251-
wf = init_func_preproc_wf('/completely/made/up/path/sub-01_task-nback_bold.nii.gz',
252-
omp_nthreads=1,
253-
ignore=[],
254-
freesurfer=True,
255-
reportlets_dir='.',
256-
output_dir='.',
257-
template='MNI152NLin2009cAsym',
258-
output_spaces=['T1w', 'fsnative',
259-
'template', 'fsaverage5'],
260-
medial_surface_nan=False,
261-
cifti_output=False,
262-
debug=False,
263-
low_mem=False,
264-
use_bbr=True,
265-
t2s_coreg=False,
266-
bold2t1w_dof=9,
267-
fmap_bspline=True,
268-
fmap_demean=True,
269-
use_syn=True,
270-
force_syn=True,
271-
template_out_grid='native',
272-
use_aroma=False,
273-
aroma_melodic_dim=None,
274-
ignore_aroma_err=False)
253+
wf = init_func_preproc_wf(
254+
'/completely/made/up/path/sub-01_task-nback_bold.nii.gz',
255+
omp_nthreads=1,
256+
ignore=[],
257+
freesurfer=True,
258+
reportlets_dir='.',
259+
output_dir='.',
260+
template='MNI152NLin2009cAsym',
261+
output_spaces=['T1w', 'fsnative', 'template', 'fsaverage5'],
262+
medial_surface_nan=False,
263+
cifti_output=False,
264+
debug=False,
265+
low_mem=False,
266+
use_bbr=True,
267+
t2s_coreg=False,
268+
bold2t1w_dof=9,
269+
fmap_bspline=True,
270+
fmap_demean=True,
271+
use_syn=True,
272+
force_syn=True,
273+
template_out_grid='native',
274+
use_aroma=False,
275+
aroma_melodic_dim=None,
276+
ignore_aroma_err=False,
277+
use_mcflirt=False,
278+
)
275279

276280
Preprocessing of :abbr:`BOLD (blood-oxygen level-dependent)` files is
277281
split into multiple sub-workflows described below.
@@ -332,11 +336,12 @@ Head-motion estimation
332336

333337
from fmriprep.workflows.bold import init_bold_hmc_wf
334338
wf = init_bold_hmc_wf(
339+
use_mcflirt=False,
335340
mem_gb=1,
336341
omp_nthreads=1)
337342

338343
Using the previously :ref:`estimated reference scan <bold_ref>`,
339-
FSL ``mcflirt`` is used to estimate head-motion.
344+
AFNI's ``3dVolreg`` is used to estimate head-motion.
340345
As a result, one rigid-body transform with respect to
341346
the reference image is written for each :abbr:`BOLD (blood-oxygen level-dependent)`
342347
time-step.
@@ -546,7 +551,7 @@ Confounds estimation
546551
metadata={"RepetitionTime": 2.0,
547552
"SliceTiming": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]})
548553

549-
Given a motion-corrected fMRI, a brain mask, ``mcflirt`` movement parameters and a
554+
Given a motion-corrected fMRI, a brain mask, head-motion parameters and a
550555
segmentation, the `discover_wf` sub-workflow calculates potential
551556
confounds per volume.
552557

fmriprep/__about__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
'scikit-image',
106106
'versioneer',
107107
'pyyaml',
108+
'transforms3d',
108109
]
109110

110111
LINKS_REQUIRES = [

fmriprep/cli/run.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ def get_parser():
160160
'--medial-surface-nan', required=False, action='store_true', default=False,
161161
help='Replace medial wall values with NaNs on functional GIFTI files. Only '
162162
'performed for GIFTI files mapped to a freesurfer subject (fsaverage or fsnative).')
163+
g_conf.add_argument(
164+
'--hmc-use-mcflirt', required=False, action='store_true', default=False,
165+
help='Head-Motion Correction (HMC) - use FSL\'s ``mcflirt`` instead '
166+
'of AFNI\'s ``3dVolreg``.')
163167

164168
# ICA_AROMA options
165169
g_aroma = parser.add_argument_group('Specific options for running ICA_AROMA')
@@ -554,6 +558,7 @@ def build_workflow(opts, retval):
554558
use_aroma=opts.use_aroma,
555559
aroma_melodic_dim=opts.aroma_melodic_dimensionality,
556560
ignore_aroma_err=opts.ignore_aroma_denoising_errors,
561+
use_mcflirt=opts.hmc_use_mcflirt,
557562
)
558563
retval['return_code'] = 0
559564

fmriprep/interfaces/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
from .utils import TPM2ROI, AddTPMs, AddTSVHeader, ConcatAffines, JoinTSVColumns
1717
from .fmap import FieldEnhance, FieldToRadS, FieldToHz, Phasediff2Fieldmap
1818
from .confounds import GatherConfounds, ICAConfounds, FMRISummary
19-
from .itk import MCFLIRT2ITK, MultiApplyTransforms
19+
from .itk import MCFLIRT2ITK, Volreg2ITK, MultiApplyTransforms
2020
from .multiecho import FirstEcho

fmriprep/interfaces/images.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
traits, TraitedSpec, BaseInterfaceInputSpec, SimpleInterface,
2222
File, InputMultiPath, OutputMultiPath)
2323
from nipype.interfaces import fsl
24+
from fmriprep.utils.misc import remove_rotation_and_shear
2425

2526
LOGGER = logging.getLogger('nipype.interface')
2627

@@ -293,6 +294,7 @@ def _run_interface(self, runtime):
293294

294295
class ValidateImageInputSpec(BaseInterfaceInputSpec):
295296
in_file = File(exists=True, mandatory=True, desc='input image')
297+
remove_rotation_and_shear = traits.Bool(False, usedefault=True)
296298

297299

298300
class ValidateImageOutputSpec(TraitedSpec):
@@ -342,6 +344,7 @@ class ValidateImage(SimpleInterface):
342344
output_spec = ValidateImageOutputSpec
343345

344346
def _run_interface(self, runtime):
347+
345348
img = nb.load(self.inputs.in_file)
346349
out_report = os.path.join(runtime.cwd, 'report.html')
347350

@@ -370,7 +373,14 @@ def _run_interface(self, runtime):
370373

371374
# Both match, qform valid (implicit with match), codes okay -> do nothing, empty report
372375
if matching_affines and qform_code > 0 and sform_code > 0:
373-
self._results['out_file'] = self.inputs.in_file
376+
if self.inputs.remove_rotation_and_shear:
377+
out_fname = fname_presuffix(self.inputs.in_file, suffix='_valid',
378+
newpath=runtime.cwd)
379+
img = remove_rotation_and_shear(img)
380+
img.to_filename(out_fname)
381+
self._results['out_file'] = out_fname
382+
else:
383+
self._results['out_file'] = self.inputs.in_file
374384
open(out_report, 'w').close()
375385
self._results['out_report'] = out_report
376386
return runtime
@@ -418,6 +428,9 @@ def _run_interface(self, runtime):
418428
</p>
419429
"""
420430
snippet = '<h3 class="elem-title">%s</h3>\n%s\n' % (warning_txt, description)
431+
432+
if self.inputs.remove_rotation_and_shear:
433+
img = remove_rotation_and_shear(img)
421434
# Store new file and report
422435
img.to_filename(out_fname)
423436
with open(out_report, 'w') as fobj:

fmriprep/interfaces/itk.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
"""
1111
import os
12+
from pathlib import Path
1213
from mimetypes import guess_type
1314
from tempfile import TemporaryDirectory
1415
import numpy as np
@@ -78,6 +79,49 @@ def _run_interface(self, runtime):
7879
return runtime
7980

8081

82+
class Volreg2ITKInputSpec(BaseInterfaceInputSpec):
83+
in_file = File(exists=True, mandatory=True,
84+
desc='mat file generated by AFNI\'s 3dVolreg')
85+
86+
87+
class Volreg2ITKOutputSpec(TraitedSpec):
88+
out_file = File(desc='the output ITKTransform file')
89+
90+
91+
class Volreg2ITK(SimpleInterface):
92+
93+
"""
94+
Convert an AFNI's mat file into an ITK Transform file.
95+
"""
96+
input_spec = Volreg2ITKInputSpec
97+
output_spec = Volreg2ITKOutputSpec
98+
99+
def _run_interface(self, runtime):
100+
# Load AFNI mat entries and reshape appropriately
101+
orig_afni_mat = np.loadtxt(self.inputs.in_file)
102+
afni_affines = [mat.reshape(3, 4, order='C') for mat in orig_afni_mat]
103+
104+
out_file = Path(fname_presuffix(self.inputs.in_file, use_ext=False,
105+
suffix='_mc4d_itk.txt', newpath=runtime.cwd))
106+
107+
fixed_params = 'FixedParameters: 0 0 0' # Center of rotation does not change
108+
lines = ["#Insight Transform File V1.0"]
109+
for i, affine in enumerate(afni_affines):
110+
lines.append("#Transform %d" % i)
111+
lines.append("Transform: AffineTransform_double_3_3")
112+
113+
ants_affine_2d = np.hstack((affine[:3, :3].reshape(1, -1),
114+
affine[:3, 3].reshape(1, -1)))
115+
params = ants_affine_2d.reshape(-1).astype('float64')
116+
params_list = ["%g" % i for i in params.tolist()]
117+
lines.append("Parameters: %s" % ' '.join(params_list))
118+
lines.append(fixed_params)
119+
120+
out_file.write_text('\n'.join(lines))
121+
self._results['out_file'] = str(out_file)
122+
return runtime
123+
124+
81125
class MultiApplyTransformsInputSpec(ApplyTransformsInputSpec):
82126
input_image = InputMultiPath(File(exists=True), mandatory=True,
83127
desc='input time-series as a list of volumes after splitting'

fmriprep/utils/misc.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@
77
"""
88

99

10+
def remove_rotation_and_shear(img):
11+
from transforms3d.affines import decompose, compose
12+
import numpy as np
13+
14+
T, _, Z, _ = decompose(img.affine)
15+
affine = compose(T=T, R=np.diag([1, 1, 1]), Z=Z)
16+
return img.__class__(np.asanyarray(img.dataobj), affine, img.header)
17+
18+
19+
def split_and_rm_rotshear_func(in_file):
20+
import os
21+
import nibabel as nb
22+
from fmriprep.utils.misc import remove_rotation_and_shear
23+
out_files = []
24+
imgs = nb.four_to_three(nb.load(in_file))
25+
for i, img in enumerate(imgs):
26+
out_file = os.path.abspath('vol%04d.nii.gz' % i)
27+
img = remove_rotation_and_shear(
28+
nb.as_closest_canonical(img))
29+
img.to_filename(out_file)
30+
out_files.append(out_file)
31+
return out_files
32+
33+
1034
def fix_multi_T1w_source_name(in_files):
1135
"""
1236
Make up a generic source name when there are multiple T1s

0 commit comments

Comments
 (0)