Skip to content

Commit 8c58b70

Browse files
authored
2 parents f53185e + d1ebb00 commit 8c58b70

File tree

7 files changed

+107
-57
lines changed

7 files changed

+107
-57
lines changed

fmriprep/interfaces/confounds.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,58 @@ def _run_interface(self, runtime):
9090
return runtime
9191

9292

93+
class _FSLRMSDeviationInputSpec(BaseInterfaceInputSpec):
94+
xfm_file = File(exists=True, mandatory=True, desc='Head motion transform file')
95+
boldref_file = File(exists=True, mandatory=True, desc='BOLD reference file')
96+
97+
98+
class _FSLRMSDeviationOutputSpec(TraitedSpec):
99+
out_file = File(desc='Output motion parameters file')
100+
101+
102+
class FSLRMSDeviation(SimpleInterface):
103+
"""Reconstruct FSL root mean square deviation from affine transforms."""
104+
105+
input_spec = _FSLRMSDeviationInputSpec
106+
output_spec = _FSLRMSDeviationOutputSpec
107+
108+
def _run_interface(self, runtime):
109+
self._results['out_file'] = fname_presuffix(
110+
self.inputs.boldref_file, suffix='_motion.tsv', newpath=runtime.cwd
111+
)
112+
113+
boldref = nb.load(self.inputs.boldref_file)
114+
hmc = nt.linear.load(self.inputs.xfm_file)
115+
116+
center = 0.5 * (np.array(boldref.shape[:3]) - 1) * boldref.header.get_zooms()[:3]
117+
118+
# Revert to vox2vox transforms
119+
fsl_hmc = nt.io.fsl.FSLLinearTransformArray.from_ras(
120+
hmc.matrix, reference=boldref, moving=boldref
121+
)
122+
fsl_matrix = np.stack([xfm['parameters'] for xfm in fsl_hmc.xforms])
123+
124+
diff = fsl_matrix[1:] @ np.linalg.inv(fsl_matrix[:-1]) - np.eye(4)
125+
M = diff[:, :3, :3]
126+
t = diff[:, :3, 3] + M @ center
127+
Rmax = 80.0
128+
129+
rmsd = np.concatenate(
130+
[
131+
[np.nan],
132+
np.sqrt(
133+
np.diag(t @ t.T)
134+
+ np.trace(M.transpose(0, 2, 1) @ M, axis1=1, axis2=2) * Rmax**2 / 5
135+
),
136+
]
137+
)
138+
139+
params = pd.DataFrame(data=rmsd, columns=['rmsd'])
140+
params.to_csv(self._results['out_file'], sep='\t', index=False, na_rep='n/a')
141+
142+
return runtime
143+
144+
93145
class _FSLMotionParamsInputSpec(BaseInterfaceInputSpec):
94146
xfm_file = File(exists=True, desc='Head motion transform file')
95147
boldref_file = File(exists=True, desc='BOLD reference file')
@@ -139,7 +191,7 @@ def _run_interface(self, runtime):
139191
columns=['trans_x', 'trans_y', 'trans_z', 'rot_x', 'rot_y', 'rot_z'],
140192
)
141193

142-
params.to_csv(self._results['out_file'], sep='\t', index=False)
194+
params.to_csv(self._results['out_file'], sep='\t', index=False, na_rep='n/a')
143195

144196
return runtime
145197

@@ -172,7 +224,7 @@ def _run_interface(self, runtime):
172224

173225
fd = pd.DataFrame(diff.abs().sum(axis=1, skipna=False), columns=['FramewiseDisplacement'])
174226

175-
fd.to_csv(self._results['out_file'], sep='\t', index=False)
227+
fd.to_csv(self._results['out_file'], sep='\t', index=False, na_rep='n/a')
176228

177229
return runtime
178230

@@ -200,7 +252,9 @@ def _run_interface(self, runtime):
200252
)
201253

202254
metadata = pd.read_csv(self.inputs.in_file, sep='\t')
203-
metadata[metadata.retained].to_csv(self._results['out_file'], sep='\t', index=False)
255+
metadata[metadata.retained].to_csv(
256+
self._results['out_file'], sep='\t', index=False, na_rep='n/a'
257+
)
204258

205259
return runtime
206260

@@ -263,13 +317,15 @@ def _run_interface(self, runtime):
263317
final_components = components.rename(columns=dict(zip(c_orig, c_new, strict=False)))
264318
final_components.rename(columns=dict(zip(w_orig, w_new, strict=False)), inplace=True)
265319
final_components.rename(columns=dict(zip(a_orig, a_new, strict=False)), inplace=True)
266-
final_components.to_csv(self._results['components_file'], sep='\t', index=False)
320+
final_components.to_csv(
321+
self._results['components_file'], sep='\t', index=False, na_rep='n/a'
322+
)
267323

268324
metadata.loc[c_comp_cor.index, 'component'] = c_new
269325
metadata.loc[w_comp_cor.index, 'component'] = w_new
270326
metadata.loc[a_comp_cor.index, 'component'] = a_new
271327

272-
metadata.to_csv(self._results['metadata_file'], sep='\t', index=False)
328+
metadata.to_csv(self._results['metadata_file'], sep='\t', index=False, na_rep='n/a')
273329

274330
return runtime
275331

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
trans_x trans_y trans_z rot_x rot_y rot_z framewise_displacement
2-
1.19425e-05 0.0443863 0.00472985 0.000356176 -0.000617445 0.0 n/a
3-
-2.57666e-05 0.0463662 0.0623273 -0.000208795 -0.00012937 0.0 0.1122673591
4-
-2.64055e-05 -0.00438628 0.067513 -2.59508e-05 -0.00012937 0.000173904 0.0737762289
5-
0.0161645 -0.0226134 0.0630764 -2.59508e-05 -0.000199844 0.000279081 0.0476371754999999
6-
0.0161497 -0.0263834 0.0464668 0.000161259 -0.00012937 0.000573335 0.04799129
7-
0.0161482 -0.0226144 0.0345415 6.52323e-05 -7.276439999999998e-05 0.000573335 0.023327415
8-
0.0121946 -0.00426109 0.0671039 -2.59508e-05 -0.00012937 0.000312581 0.075296445
9-
0.0121556 -0.0175135 0.04042 -2.59508e-05 -0.00012937 0.000166363 0.04728621
10-
0.0126526 -0.000813328 0.0778061 -2.59508e-05 -0.00012937 0.000166363 0.054583272
11-
0.012614 0.0250656 0.106248 -0.000320333 0.000149271 0.000166363 0.0830105879999999
12-
0.0126599 -0.0252459 0.0731423999999999 2.99512e-05 -0.000204037 0.000166363 0.11864261
13-
-0.00608005 0.0349207 0.110289 -0.000485119 -0.00012937 8.57718e-05 0.1495695699999999
14-
-0.00607796 -0.00933714 0.0796319999999999 -0.000126125 -0.00012937 0.000166363 0.09689619
15-
0.0124531 0.00996903 0.0986678 -0.000266813 -0.000207949 0.000166363 0.06783638
16-
0.010915 -0.00933714 0.0986667 -0.000126125 -0.000248281 0.000166363 0.02989637
17-
0.0119349 0.00531637 0.0808524 -0.000209587 -0.0 0.000226933 0.0531033599999999
1+
trans_x trans_y trans_z rot_x rot_y rot_z framewise_displacement rmsd
2+
1.19425e-05 0.0443863 0.00472985 0.000356176 -0.000617445 0.0 n/a n/a
3+
-2.57666e-05 0.0463662 0.0623273 -0.000208795 -0.00012937 0.0 0.1122673591 0.0725718
4+
-2.64055e-05 -0.00438628 0.067513 -2.59508e-05 -0.00012937 0.000173904 0.0737762289 0.0527961
5+
0.0161645 -0.0226134 0.0630764 -2.59508e-05 -0.000199844 0.000279081 0.0476371754999999 0.0260938
6+
0.0161497 -0.0263834 0.0464668 0.000161259 -0.00012937 0.000573335 0.04799129 0.0257898
7+
0.0161482 -0.0226144 0.0345415 6.52323e-05 -7.276439999999998e-05 0.000573335 0.023327415 0.0131254
8+
0.0121946 -0.00426109 0.0671039 -2.59508e-05 -0.00012937 0.000312581 0.075296445 0.0410418
9+
0.0121556 -0.0175135 0.04042 -2.59508e-05 -0.00012937 0.000166363 0.04728621 0.0306652
10+
0.0126526 -0.000813328 0.0778061 -2.59508e-05 -0.00012937 0.000166363 0.054583272 0.0409495
11+
0.012614 0.0250656 0.106248 -0.000320333 0.000149271 0.000166363 0.0830105879999999 0.0452599
12+
0.0126599 -0.0252459 0.0731423999999999 2.99512e-05 -0.000204037 0.000166363 0.11864261 0.0669517
13+
-0.00608005 0.0349207 0.110289 -0.000485119 -0.00012937 8.57718e-05 0.1495695699999999 0.0801947999999999
14+
-0.00607796 -0.00933714 0.0796319999999999 -0.000126125 -0.00012937 0.000166363 0.09689619 0.0586617
15+
0.0124531 0.00996903 0.0986678 -0.000266813 -0.000207949 0.000166363 0.06783638 0.0343961
16+
0.010915 -0.00933714 0.0986667 -0.000126125 -0.000248281 0.000166363 0.02989637 0.0208872
17+
0.0119349 0.00531637 0.0808524 -0.000209587 -0.0 0.000226933 0.0531033599999999 0.0263331

fmriprep/interfaces/tests/test_confounds.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ def test_FilterDropped(tmp_path, data_dir):
3333
assert filtered_meta == target_meta
3434

3535

36+
def test_FSLRMSDeviation(tmp_path, data_dir):
37+
base = 'sub-01_task-mixedgamblestask_run-01'
38+
xfms = data_dir / f'{base}_from-orig_to-boldref_mode-image_desc-hmc_xfm.txt'
39+
boldref = data_dir / f'{base}_desc-hmc_boldref.nii.gz'
40+
timeseries = data_dir / f'{base}_desc-motion_timeseries.tsv'
41+
42+
rmsd = pe.Node(
43+
confounds.FSLRMSDeviation(xfm_file=str(xfms), boldref_file=str(boldref)),
44+
name='rmsd',
45+
base_dir=str(tmp_path),
46+
)
47+
res = rmsd.run()
48+
49+
orig = pd.read_csv(timeseries, sep='\t')['rmsd']
50+
derived = pd.read_csv(res.outputs.out_file, sep='\t')['rmsd']
51+
52+
# RMSD is nominally in mm, so 0.1um is an acceptable deviation
53+
assert np.allclose(orig.values, derived.values, equal_nan=True, atol=1e-4)
54+
55+
3656
def test_FSLMotionParams(tmp_path, data_dir):
3757
base = 'sub-01_task-mixedgamblestask_run-01'
3858
xfms = data_dir / f'{base}_from-orig_to-boldref_mode-image_desc-hmc_xfm.txt'

fmriprep/workflows/bold/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,6 @@ def init_bold_wf(
675675
('outputnode.bold_mask', 'inputnode.bold_mask'),
676676
('outputnode.hmc_boldref', 'inputnode.hmc_boldref'),
677677
('outputnode.motion_xfm', 'inputnode.motion_xfm'),
678-
('outputnode.rmsd_file', 'inputnode.rmsd_file'),
679678
('outputnode.boldref2anat_xfm', 'inputnode.boldref2anat_xfm'),
680679
('outputnode.dummy_scans', 'inputnode.skip_vols'),
681680
]),

fmriprep/workflows/bold/confounds.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
FMRISummary,
4141
FramewiseDisplacement,
4242
FSLMotionParams,
43+
FSLRMSDeviation,
4344
GatherConfounds,
4445
RenameACompCor,
4546
)
@@ -124,8 +125,6 @@ def init_bold_confs_wf(
124125
BOLD series mask
125126
motion_xfm
126127
ITK-formatted head motion transforms
127-
rmsd_file
128-
Root mean squared deviation as measured by ``fsl_motion_outliers`` [Jenkinson2002]_.
129128
skip_vols
130129
number of non steady state volumes
131130
t1w_mask
@@ -225,7 +224,6 @@ def init_bold_confs_wf(
225224
'bold_mask',
226225
'hmc_boldref',
227226
'motion_xfm',
228-
'rmsd_file',
229227
'skip_vols',
230228
't1w_mask',
231229
't1w_tpms',
@@ -269,7 +267,8 @@ def init_bold_confs_wf(
269267
motion_params = pe.Node(FSLMotionParams(), name='motion_params')
270268

271269
# Frame displacement
272-
fdisp = pe.Node(FramewiseDisplacement(), name='fdisp', mem_gb=mem_gb)
270+
fdisp = pe.Node(FramewiseDisplacement(), name='fdisp')
271+
rmsd = pe.Node(FSLRMSDeviation(), name='rmsd')
273272

274273
# Generate aCompCor probseg maps
275274
acc_masks = pe.Node(aCompCorMasks(is_aseg=freesurfer), name='acc_masks')
@@ -373,12 +372,6 @@ def init_bold_confs_wf(
373372
mem_gb=0.01,
374373
run_without_submitting=True,
375374
)
376-
add_rmsd_header = pe.Node(
377-
AddTSVHeader(columns=['rmsd']),
378-
name='add_rmsd_header',
379-
mem_gb=0.01,
380-
run_without_submitting=True,
381-
)
382375
concat = pe.Node(GatherConfounds(), name='concat', mem_gb=0.01, run_without_submitting=True)
383376

384377
# CompCor metadata
@@ -524,6 +517,8 @@ def _select_cols(table):
524517
('bold_mask', 'in_mask')]),
525518
(inputnode, motion_params, [('motion_xfm', 'xfm_file'),
526519
('hmc_boldref', 'boldref_file')]),
520+
(inputnode, rmsd, [('motion_xfm', 'xfm_file'),
521+
('hmc_boldref', 'boldref_file')]),
527522
(motion_params, fdisp, [('out_file', 'in_file')]),
528523
# Brain mask
529524
(inputnode, t1w_mask_tfm, [('t1w_mask', 'input_image'),
@@ -567,7 +562,6 @@ def _select_cols(table):
567562
(merge_rois, signals, [('out', 'label_files')]),
568563

569564
# Collate computed confounds together
570-
(inputnode, add_rmsd_header, [('rmsd_file', 'in_file')]),
571565
(dvars, add_dvars_header, [('out_nstd', 'in_file')]),
572566
(dvars, add_std_dvars_header, [('out_std', 'in_file')]),
573567
(signals, concat, [('out_file', 'signals')]),
@@ -577,7 +571,7 @@ def _select_cols(table):
577571
(rename_acompcor, concat, [('components_file', 'acompcor')]),
578572
(crowncompcor, concat, [('components_file', 'crowncompcor')]),
579573
(motion_params, concat, [('out_file', 'motion')]),
580-
(add_rmsd_header, concat, [('out_file', 'rmsd')]),
574+
(rmsd, concat, [('out_file', 'rmsd')]),
581575
(add_dvars_header, concat, [('out_file', 'dvars')]),
582576
(add_std_dvars_header, concat, [('out_file', 'std_dvars')]),
583577

fmriprep/workflows/bold/fit.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,6 @@ def init_bold_fit_wf(
178178
boldref2fmap_xfm
179179
Affine transform mapping from BOLD reference space to the fieldmap
180180
space, if applicable.
181-
rmsd_file
182-
Root mean squared deviation as measured by ``fsl_motion_outliers`` [Jenkinson2002]_.
183181
dummy_scans
184182
The number of dummy scans declared or detected at the beginning of the series.
185183
@@ -285,7 +283,6 @@ def init_bold_fit_wf(
285283
'motion_xfm',
286284
'boldref2anat_xfm',
287285
'boldref2fmap_xfm',
288-
'rmsd_file',
289286
],
290287
),
291288
name='outputnode',
@@ -299,9 +296,7 @@ def init_bold_fit_wf(
299296
name='hmcref_buffer',
300297
)
301298
fmapref_buffer = pe.Node(niu.Function(function=_select_ref), name='fmapref_buffer')
302-
hmc_buffer = pe.Node(
303-
niu.IdentityInterface(fields=['hmc_xforms', 'rmsd_file']), name='hmc_buffer'
304-
)
299+
hmc_buffer = pe.Node(niu.IdentityInterface(fields=['hmc_xforms']), name='hmc_buffer')
305300
fmapreg_buffer = pe.Node(
306301
niu.IdentityInterface(fields=['boldref2fmap_xfm']), name='fmapreg_buffer'
307302
)
@@ -354,7 +349,6 @@ def init_bold_fit_wf(
354349
(fmapreg_buffer, outputnode, [('boldref2fmap_xfm', 'boldref2fmap_xfm')]),
355350
(hmc_buffer, outputnode, [
356351
('hmc_xforms', 'motion_xfm'),
357-
('rmsd_file', 'rmsd_file'),
358352
]),
359353
(inputnode, func_fit_reports_wf, [
360354
('bold_file', 'inputnode.source_file'),
@@ -444,9 +438,6 @@ def init_bold_fit_wf(
444438
('bold_file', 'inputnode.bold_file'),
445439
]),
446440
(bold_hmc_wf, ds_hmc_wf, [('outputnode.xforms', 'inputnode.xforms')]),
447-
(bold_hmc_wf, hmc_buffer, [
448-
('outputnode.rmsd_file', 'rmsd_file'),
449-
]),
450441
(ds_hmc_wf, hmc_buffer, [('outputnode.xforms', 'hmc_xforms')]),
451442
]) # fmt:skip
452443
else:

fmriprep/workflows/bold/hmc.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ def init_bold_hmc_wf(mem_gb: float, omp_nthreads: int, name: str = 'bold_hmc_wf'
7171
-------
7272
xforms
7373
ITKTransform file aligning each volume to ``ref_image``
74-
rmsd_file
75-
Root mean squared deviation as measured by ``fsl_motion_outliers`` [Jenkinson2002]_.
7674
7775
"""
7876
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
@@ -89,27 +87,19 @@ def init_bold_hmc_wf(mem_gb: float, omp_nthreads: int, name: str = 'bold_hmc_wf'
8987
inputnode = pe.Node(
9088
niu.IdentityInterface(fields=['bold_file', 'raw_ref_image']), name='inputnode'
9189
)
92-
outputnode = pe.Node(niu.IdentityInterface(fields=['xforms', 'rmsd_file']), name='outputnode')
90+
outputnode = pe.Node(niu.IdentityInterface(fields=['xforms']), name='outputnode')
9391

9492
# Head motion correction (hmc)
95-
mcflirt = pe.Node(
96-
fsl.MCFLIRT(save_mats=True, save_plots=True, save_rms=True),
97-
name='mcflirt',
98-
mem_gb=mem_gb * 3,
99-
)
93+
mcflirt = pe.Node(fsl.MCFLIRT(save_mats=True), name='mcflirt', mem_gb=mem_gb * 3)
10094

10195
fsl2itk = pe.Node(MCFLIRT2ITK(), name='fsl2itk', mem_gb=0.05, n_procs=omp_nthreads)
10296

103-
def _pick_rel(rms_files):
104-
return rms_files[-1]
105-
10697
workflow.connect([
10798
(inputnode, mcflirt, [('raw_ref_image', 'ref_file'),
10899
('bold_file', 'in_file')]),
109100
(inputnode, fsl2itk, [('raw_ref_image', 'in_source'),
110101
('raw_ref_image', 'in_reference')]),
111102
(mcflirt, fsl2itk, [('mat_file', 'in_files')]),
112-
(mcflirt, outputnode, [(('rms_files', _pick_rel), 'rmsd_file')]),
113103
(fsl2itk, outputnode, [('out_file', 'xforms')]),
114104
]) # fmt:skip
115105

0 commit comments

Comments
 (0)