Skip to content

Commit b3b6e5b

Browse files
committed
REF: Refactor motion-related data computation functions
Refactor motion-related data computation functions into reusable code from the PET notebook. Rename the existing FD function to distinguish it from the refactored function.
1 parent 804b522 commit b3b6e5b

File tree

3 files changed

+72
-35
lines changed

3 files changed

+72
-35
lines changed

docs/notebooks/PET_example.ipynb

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2445,33 +2445,7 @@
24452445
"import numpy as np\n",
24462446
"import pandas as pd\n",
24472447
"\n",
2448-
"def extract_motion_parameters(affine):\n",
2449-
" \"\"\"Extract translation (mm) and rotation (degrees) parameters from an affine matrix.\"\"\"\n",
2450-
" translation = affine[:3, 3]\n",
2451-
" rotation_rad = np.arctan2(\n",
2452-
" [affine[2, 1], affine[0, 2], affine[1, 0]],\n",
2453-
" [affine[2, 2], affine[0, 0], affine[1, 1]]\n",
2454-
" )\n",
2455-
" rotation_deg = np.rad2deg(rotation_rad)\n",
2456-
" return (*translation, *rotation_deg)\n",
2457-
"\n",
2458-
"def compute_fd(motion_parameters):\n",
2459-
" \"\"\"Compute Framewise Displacement from motion parameters.\"\"\"\n",
2460-
" translations = motion_parameters[:, :3]\n",
2461-
" rotations_deg = motion_parameters[:, 3:]\n",
2462-
" rotations_rad = np.deg2rad(rotations_deg)\n",
2463-
" \n",
2464-
" # Compute differences between consecutive frames\n",
2465-
" d_translations = np.vstack([np.zeros((1, 3)), np.diff(translations, axis=0)])\n",
2466-
" d_rotations = np.vstack([np.zeros((1, 3)), np.diff(rotations_rad, axis=0)])\n",
2467-
" \n",
2468-
" # Convert rotations from radians to displacement on a sphere (radius 50 mm)\n",
2469-
" radius = 50 # typical head radius in mm\n",
2470-
" rotation_displacement = d_rotations * radius\n",
2471-
" \n",
2472-
" # Compute FD as sum of absolute differences\n",
2473-
" fd = np.sum(np.abs(d_translations) + np.abs(rotation_displacement), axis=1)\n",
2474-
" return fd\n",
2448+
"from nifreeze.registration.utils import displacement_framewise_motion, extract_motion_parameters\n",
24752449
"\n",
24762450
"# Assume 'affines' is the list of affine matrices you computed earlier\n",
24772451
"motion_parameters = []\n",
@@ -2481,7 +2455,7 @@
24812455
" motion_parameters.append([tx, ty, tz, rx, ry, rz])\n",
24822456
"\n",
24832457
"motion_parameters = np.array(motion_parameters)\n",
2484-
"fd = compute_fd(motion_parameters)\n",
2458+
"fd = displacement_framewise_motion(motion_parameters)\n",
24852459
"\n",
24862460
"# Creating a DataFrame for better visualization\n",
24872461
"df_motion = pd.DataFrame({\n",

docs/notebooks/bold_realignment.ipynb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@
478478
"source": [
479479
"from nitransforms.resampling import apply\n",
480480
"\n",
481-
"from nifreeze.registration.utils import displacement_framewise\n",
481+
"from nifreeze.registration.utils import displacement_framewise_transformation\n",
482482
"\n",
483483
"afni_fd = {}\n",
484484
"nitransforms_fd = {}\n",
@@ -501,7 +501,9 @@
501501
" ]\n",
502502
"\n",
503503
" nii = nb.load(DATA_PATH / bold_run)\n",
504-
" nitransforms_fd[str(bold_run)] = np.array([displacement_framewise(nii, xfm) for xfm in xfms])\n",
504+
" nitransforms_fd[str(bold_run)] = np.array(\n",
505+
" [displacement_framewise_transformation(nii, xfm) for xfm in xfms]\n",
506+
" )\n",
505507
"\n",
506508
" hmc_xfm = nt.linear.LinearTransformsMapping(xfms)\n",
507509
" out_nitransforms = (\n",
@@ -520,7 +522,7 @@
520522
" OUTPUT_DIR / bold_run.parent / f\"{bold_run.name.rsplit('_', 1)[0]}_desc-hmc_xfm.txt\"\n",
521523
" )\n",
522524
" afni_fd[str(bold_run)] = np.array(\n",
523-
" [displacement_framewise(nii, afni_xfms[i]) for i in range(len(afni_xfms))]\n",
525+
" [displacement_framewise_transformation(nii, afni_xfms[i]) for i in range(len(afni_xfms))]\n",
524526
" )\n",
525527
"\n",
526528
" out_afni = (\n",

src/nifreeze/registration/utils.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@
3232
from __future__ import annotations
3333

3434
from itertools import product
35+
from typing import Tuple
3536

3637
import nibabel as nb
3738
import nitransforms as nt
3839
import numpy as np
3940

41+
RADIUS = 50.0
42+
"""Typical radius (in mm) of a sphere mimicking the size of a typical human brain."""
43+
4044

4145
def displacements_within_mask(
4246
mask_img: nb.spatialimages.SpatialImage,
@@ -79,11 +83,11 @@ def displacements_within_mask(
7983
return np.linalg.norm(diffs, axis=-1)
8084

8185

82-
def displacement_framewise(
86+
def displacement_framewise_transform(
8387
img: nb.spatialimages.SpatialImage,
8488
test_xfm: nt.base.BaseTransform,
85-
radius: float = 50.0,
86-
):
89+
radius: float = RADIUS,
90+
) -> float:
8791
"""
8892
Compute the framewise displacement (FD) for a given transformation.
8993
@@ -95,7 +99,6 @@ def displacement_framewise(
9599
The transformation to test. Applied to coordinates around the image center.
96100
radius : :obj:`float`, optional
97101
The radius (in mm) of the spherical neighborhood around the center of the image.
98-
Default is 50.0 mm.
99102
100103
Returns
101104
-------
@@ -112,3 +115,61 @@ def displacement_framewise(
112115
fd_coords = np.array(list(product(*((radius, -radius),) * 3))) + center_xyz
113116
# Compute the average displacement from the test transformation
114117
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

Comments
 (0)