ENH: Add transform analysis utils - #286
Conversation
|
I did not dare to create a I think the plotting should go to |
7fae796 to
73b27ba
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #286 +/- ##
==========================================
+ Coverage 96.74% 96.87% +0.13%
==========================================
Files 16 17 +1
Lines 1994 2083 +89
Branches 267 240 -27
==========================================
+ Hits 1929 2018 +89
Misses 41 41
Partials 24 24
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
73b27ba to
86fa6af
Compare
|
Pinging @oesteban. |
86fa6af to
711146a
Compare
Yes
No, if you look at it closer, you'll see this is using apply_affine from nibabel.affines, which is not the same (that applies affines to points, our apply in nitransforms and the apply_affines (notice the 's' at the end) in nifreeze apply affines to images (internally to points, but that's not relevant here). |
oesteban
left a comment
There was a problem hiding this comment.
Thanks for the update. I just realized of a little inconsistency. Other than that, I think this is almost ready.
|
Had a closer look at the |
04b0f77 to
8039887
Compare
Yes, nitransforms.resampling.apply does the job. We created the apply_transforms wrapper in nifreeze because the multiple-volume resampling was not very well supported in nitransforms, but that changed. |
b71eac7 to
f3236b7
Compare
66875cb to
8164b14
Compare
8164b14 to
98ec243
Compare
|
This is ready to go. Merging this would allow |
oesteban
left a comment
There was a problem hiding this comment.
We had a pretty hefty bug in the original implementation of nifreeze!
079e6bc to
358dcec
Compare
Add transform analysis utils. Transfer contents from the `NiFreeze` projects so that hey can be reused across projects requiring transform analysis: https://github.com/nipreps/nifreeze/blob/d27ba7552bbd9095c3c13b46443d87a4b5504c4c/src/nifreeze/analysis/motion.py https://github.com/nipreps/nifreeze/blob/d27ba7552bbd9095c3c13b46443d87a4b5504c4c/src/nifreeze/data/utils.py Add a fixture to be able to reuse a random number generator across tests. Co-authored-by: Oscar Esteban <code@oscaresteban.es>
|
Out of curiosity, why are we standardizing on degrees? Apart from AFNI, everybody reports angles in radians. It seems to add unnecessary complication. Nipype also normalizes to SPM style (https://github.com/nipy/nipype/blob/8234ec318489930fa17487cf15d210420214b00a/nipype/utils/misc.py#L246-L271) so consistency within the nipy world seems desirable. |
|
Another conceptual question: Do we want to continue the tradition of accepting an unlabeled Nx6 matrix, or would it be a good opportunity to require the caller to consider the it useful to have a function that accepts an Nx6 matrix, or would it make more sense to accept 2 Nx3 matrices and a radius? The reason I ask is that it would be very tempting to load an Nx6 matrix out of some transform file and pass it directly without carefully considering which are the translations and which are the rotations. I might think of the API as follows: def framewise_displacement(
translations: np.ndarray,
rotations: np.ndarray,
radius: float = DEFAULT_RADIUS
) -> np.ndarray:
"""Calculate framewise displacement of motion parameters. ..."""
def extract_parameters(motion_parameters: np.ndarray, fmt: str | None=None) -> tuple[np.ndarray, np.ndarray]:
"""Extract translation and rotation parameters from a parameter array.
This function normalizes rotation parameters to radians, if the input format is known to use degrees.
The parameters are returned in the order expected by the `framewise_displacement` function.
"""
# I know what my matrix is:
fd = framewise_displacement(motion_parameters[:, :3], motion_parameters[:, 3:])
# I know I got it from AFNI:
afni_fd = framewise_displacement(*extract_parameters(motion_parameters, fmt='afni'))We could even force people to use labels instead and say: @dataclass(kw_only=True)
class MotionParameters:
rotations: np.ndarray
translations: np.ndarray
def extract_parameters(motion_parameters: np.ndarray, fmt: str | None=None) -> MotionParameters: ...
def framewise_displacement(motion_parameters: MotionParameters, radius: float = DEFAULT_RADIUS) -> np.ndarray: ...This would even suggest a straightforward translation between affines and motion parameters: def affineToMotionParams(aff: Affine) -> MotionParameters: ...
def motionParamstoAffine(motion_parameters: MotionParameters) -> LinearTransformsMapping: ...Just a couple thoughts. Will review the PR on the assumption that you'll stick with degrees and a big matrix. |
| def extract_motion_parameters(affine: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: | ||
| """Extract translation (mm) and rotation (degrees) parameters from an affine matrix. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| affine : :obj:`~numpy.ndarray` | ||
| The affine transformation matrix. | ||
|
|
||
| Returns | ||
| ------- | ||
| :obj:`tuple` | ||
| Extracted translation and rotation parameters. | ||
| """ | ||
|
|
||
| translation = affine[:3, 3] | ||
| rotation_rad = np.arctan2( | ||
| [affine[2, 1], affine[0, 2], affine[1, 0]], | ||
| [affine[2, 2], affine[0, 0], affine[1, 1]], | ||
| ) | ||
| rotation_deg = np.rad2deg(rotation_rad) | ||
| return *translation, *rotation_deg |
There was a problem hiding this comment.
This isn't obviously correct to me. There are many ways of extracting angles from a rotation matrix, but I'm surprised that you seem able to get them using only 6 entries of a 3x3 matrix.
At the least, this function needs a citation, but I would be inclined to rely on scipy.spatial.transforms or transforms3d for this functionality.
There was a problem hiding this comment.
Thanks, will investigate.
| numpy.ndarray | ||
| An array of shape ``(n_points, 3)`` whose rows have unit norm. | ||
|
|
||
| Examples |
There was a problem hiding this comment.
Would it be too much to ask for these docs to have plots to show the points on the unit sphere?
There was a problem hiding this comment.
Sounds good, I'll see if it doesn't involve much or defer to a future PR.
| Identifies high-motion frames as timepoint exceeding a given threshold value | ||
| based on z-score normalized framewise displacement (FD) values. |
There was a problem hiding this comment.
Is there a citation for this one? This seems dubious to me, as z-scoring will amplify low-motion subjects' FD and suppress high-motion subjects' FD. For the purposes of censoring, absolute thresholds are often used based on voxel size.
There was a problem hiding this comment.
I'm inclined to drop this function altogether. The purpose is not censoring, but rather identifying the higher trees in the forest.
Co-authored-by: Chris Markiewicz <effigies@gmail.com>
|
Can we revisit this so that we can reduce the fronts across NiFreeze, please? |
Remove spike identification function.
|
I think #286 (comment) can be addressed in a subsequent PR. |
|
I'd squash commits once we are satisfied with the changes. |
a42548b to
3a6fb98
Compare
Use `scipy` to extract rotations from affine matrices.
Make the computation of FD for a single vertex consistent.
Ensure motion array has right dimensionality before computing FD. Take advantage of the commit to: - Make the array use floating point precision. - Prefer using explicit array rather than relying on broadcasting for prepending.
Improve return value docstring in FD computation function from motion array.
Plot unit sphere sampling docstring examples. Take advantage of the commit to: - Define the values used for the docstring test as a named variable for the sake of clearliness. - Use Sphinx markup for math symbols instead of using non-unicode characters.
Use L1 norm to check resulting FD value from transform matrix.
3a6fb98 to
730b14f
Compare
|
Note: we are assuming that the matrices are all valid: scipy 1.17 introduced a check parameter that we are not able to use because we only require scipy >= 1.10. I think I've done all I can on this PR. |
Improve documentation: use appropriate markup for types, document exceptions.
608b796 to
e562414
Compare
Test exceptions.
e562414 to
0732e1d
Compare
Add transform analysis utils.
Transfer contents from the
NiFreezeprojects so that hey can be reused across projects requiring transform analysis:https://github.com/nipreps/nifreeze/blob/d27ba7552bbd9095c3c13b46443d87a4b5504c4c/src/nifreeze/analysis/motion.py https://github.com/nipreps/nifreeze/blob/d27ba7552bbd9095c3c13b46443d87a4b5504c4c/src/nifreeze/data/utils.py
Add a fixture to be able to reuse a random number generator across tests.
Fixes #239.