32
32
from __future__ import annotations
33
33
34
34
from itertools import product
35
+ from typing import Tuple
35
36
36
37
import nibabel as nb
37
38
import nitransforms as nt
38
39
import numpy as np
39
40
41
+ RADIUS = 50.0
42
+ """Typical radius (in mm) of a sphere mimicking the size of a typical human brain."""
43
+
40
44
41
45
def displacements_within_mask (
42
46
mask_img : nb .spatialimages .SpatialImage ,
@@ -79,11 +83,11 @@ def displacements_within_mask(
79
83
return np .linalg .norm (diffs , axis = - 1 )
80
84
81
85
82
- def displacement_framewise (
86
+ def displacement_framewise_transform (
83
87
img : nb .spatialimages .SpatialImage ,
84
88
test_xfm : nt .base .BaseTransform ,
85
- radius : float = 50.0 ,
86
- ):
89
+ radius : float = RADIUS ,
90
+ ) -> float :
87
91
"""
88
92
Compute the framewise displacement (FD) for a given transformation.
89
93
@@ -95,7 +99,6 @@ def displacement_framewise(
95
99
The transformation to test. Applied to coordinates around the image center.
96
100
radius : :obj:`float`, optional
97
101
The radius (in mm) of the spherical neighborhood around the center of the image.
98
- Default is 50.0 mm.
99
102
100
103
Returns
101
104
-------
@@ -112,3 +115,61 @@ def displacement_framewise(
112
115
fd_coords = np .array (list (product (* ((radius , - radius ),) * 3 ))) + center_xyz
113
116
# Compute the average displacement from the test transformation
114
117
return np .mean (np .linalg .norm (test_xfm .map (fd_coords ) - fd_coords , axis = - 1 ))
118
+
119
+
120
+ def displacement_framewise_motion (motion_parameters : np .ndarray , radius : float = RADIUS ) -> np .ndarray :
121
+ """Compute framewise displacement (FD) from motion parameters.
122
+
123
+ Each row in the motion parameters represents one frame, and columns
124
+ represent each coordinate axis ``x``, `y``, and ``z``. Translation
125
+ parameters are followed by rotation parameters column-wise.
126
+
127
+ Parameters
128
+ ----------
129
+ motion_parameters : :obj:`numpy.ndarray`
130
+ Motion parameters.
131
+ radius : :obj:`float`, optional
132
+ Radius (in mm) of a sphere mimicking the size of a typical human brain.
133
+
134
+ Returns
135
+ -------
136
+ :obj:`numpy.ndarray`
137
+ The framewise displacement (FD) as the sum of absolute differences
138
+ between consecutive frames.
139
+ """
140
+
141
+ translations = motion_parameters [:, :3 ]
142
+ rotations_deg = motion_parameters [:, 3 :]
143
+ rotations_rad = np .deg2rad (rotations_deg )
144
+
145
+ # Compute differences between consecutive frames
146
+ d_translations = np .vstack ([np .zeros ((1 , 3 )), np .diff (translations , axis = 0 )])
147
+ d_rotations = np .vstack ([np .zeros ((1 , 3 )), np .diff (rotations_rad , axis = 0 )])
148
+
149
+ # Convert rotations from radians to displacement on a sphere
150
+ rotation_displacement = d_rotations * radius
151
+
152
+ # Compute FD as sum of absolute differences
153
+ return np .sum (np .abs (d_translations ) + np .abs (rotation_displacement ), axis = 1 )
154
+
155
+
156
+ def extract_motion_parameters (affine : np .ndarray ) -> Tuple [np .ndarray , np .ndarray ]:
157
+ """Extract translation (mm) and rotation (degrees) parameters from an affine matrix.
158
+
159
+ Parameters
160
+ ----------
161
+ affine : :obj:`~numpy.ndarray`
162
+ The affine transformation matrix.
163
+
164
+ Returns
165
+ -------
166
+ :obj:`tuple`
167
+ Extracted translation and rotation parameters.
168
+ """
169
+
170
+ translation = affine [:3 , 3 ]
171
+ rotation_rad = np .arctan2 (
172
+ [affine [2 , 1 ], affine [0 , 2 ], affine [1 , 0 ]], [affine [2 , 2 ], affine [0 , 0 ], affine [1 , 1 ]]
173
+ )
174
+ rotation_deg = np .rad2deg (rotation_rad )
175
+ return * translation , * rotation_deg
0 commit comments