|
34 | 34 | import re
|
35 | 35 |
|
36 | 36 | import nibabel as nb
|
| 37 | +import nitransforms as nt |
37 | 38 | import numpy as np
|
38 | 39 | import pandas as pd
|
39 | 40 | from nipype import logging
|
|
50 | 51 | from nipype.utils.filemanip import fname_presuffix
|
51 | 52 | from nireports.reportlets.modality.func import fMRIPlot
|
52 | 53 | from niworkflows.utils.timeseries import _cifti_timeseries, _nifti_timeseries
|
| 54 | +from scipy import ndimage as ndi |
| 55 | +from scipy.spatial import transform as sst |
53 | 56 |
|
54 | 57 | LOGGER = logging.getLogger('nipype.interface')
|
55 | 58 |
|
@@ -87,6 +90,60 @@ def _run_interface(self, runtime):
|
87 | 90 | return runtime
|
88 | 91 |
|
89 | 92 |
|
| 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 | + |
90 | 147 | class _FilterDroppedInputSpec(BaseInterfaceInputSpec):
|
91 | 148 | in_file = File(exists=True, desc='input CompCor metadata')
|
92 | 149 |
|
|
0 commit comments