Skip to content

Commit de5ee61

Browse files
committed
feat: Add FSLMotionParams interface to recover motion parameters
1 parent e933636 commit de5ee61

File tree

1 file changed

+57
-0
lines changed

1 file changed

+57
-0
lines changed

fmriprep/interfaces/confounds.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import re
3535

3636
import nibabel as nb
37+
import nitransforms as nt
3738
import numpy as np
3839
import pandas as pd
3940
from nipype import logging
@@ -50,6 +51,8 @@
5051
from nipype.utils.filemanip import fname_presuffix
5152
from nireports.reportlets.modality.func import fMRIPlot
5253
from niworkflows.utils.timeseries import _cifti_timeseries, _nifti_timeseries
54+
from scipy import ndimage as ndi
55+
from scipy.spatial import transform as sst
5356

5457
LOGGER = logging.getLogger('nipype.interface')
5558

@@ -87,6 +90,60 @@ def _run_interface(self, runtime):
8790
return runtime
8891

8992

93+
class _FSLMotionParamsInputSpec(BaseInterfaceInputSpec):
94+
xfm_file = File(exists=True, desc='Head motion transform file')
95+
boldref_file = File(exists=True, desc='BOLD reference file')
96+
97+
98+
class _FSLMotionParamsOutputSpec(TraitedSpec):
99+
out_file = File(desc='Output motion parameters file')
100+
101+
102+
class FSLMotionParams(SimpleInterface):
103+
"""Reconstruct FSL motion parameters from affine transforms."""
104+
105+
input_spec = _FSLMotionParamsInputSpec
106+
output_spec = _FSLMotionParamsOutputSpec
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+
# FSL's "center of gravity" is the center of mass scaled by zooms
117+
# No rotation is applied.
118+
center_of_gravity = np.matmul(
119+
np.diag(boldref.header.get_zooms()),
120+
ndi.center_of_mass(np.asanyarray(boldref.dataobj)),
121+
)
122+
123+
# Revert to vox2vox transforms
124+
fsl_hmc = nt.io.fsl.FSLLinearTransformArray.from_ras(
125+
hmc.matrix, reference=boldref, moving=boldref
126+
)
127+
fsl_matrix = np.stack([xfm['parameters'] for xfm in fsl_hmc.xforms])
128+
129+
# FSL uses left-handed rotation conventions, so transpose
130+
mats = fsl_matrix[:, :3, :3].transpose(0, 2, 1)
131+
132+
# Rotations are recovered directly
133+
rot_xyz = sst.Rotation.from_matrix(mats).as_euler('XYZ')
134+
# Translations are recovered by applying the rotation to the center of gravity
135+
trans_xyz = fsl_matrix[:, :3, 3] - mats @ center_of_gravity + center_of_gravity
136+
137+
params = pd.DataFrame(
138+
data=np.hstack((trans_xyz, rot_xyz)),
139+
columns=['trans_x', 'trans_y', 'trans_z', 'rot_x', 'rot_y', 'rot_z'],
140+
)
141+
142+
params.to_csv(self._results['out_file'], sep='\t', index=False)
143+
144+
return runtime
145+
146+
90147
class _FilterDroppedInputSpec(BaseInterfaceInputSpec):
91148
in_file = File(exists=True, desc='input CompCor metadata')
92149

0 commit comments

Comments
 (0)