-
Notifications
You must be signed in to change notification settings - Fork 18
ENH: Add transform analysis utils #286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
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❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #286 +/- ##
==========================================
- Coverage 96.74% 0.00% -96.75%
==========================================
Files 16 17 +1
Lines 1994 2079 +85
Branches 267 0 -267
==========================================
- Hits 1929 0 -1929
- Misses 41 2079 +2038
+ Partials 24 0 -24
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>
Co-authored-by: Oscar Esteban <code@oscaresteban.es>
Cite Power et al. in FD computation function docstring.
Fix style errors.
358dcec to
bacdc83
Compare
|
@jhlegarreta please have one more look - I realized that the FD from transform matrices was also badly computed as it did not accept the previous transform. IMHO, we should also provide a "vectorized" version of FD such that you pass the list of transforms and gives you timeseries. Could be left for a future PR though. @effigies added you to have an extra pair of eyes on this. |
| xfm: TransformBase, | ||
| xfm_prev: TransformBase | None = None, | ||
| radius: float = DEFAULT_FD_RADIUS, | ||
| n_vertices: int = 8, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be interesting to make the output match compute_fd_from_motion for n_vertices = 1.
jhlegarreta
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO, we should also provide a "vectorized" version of FD such that you pass the list of transforms and gives you timeseries. Could be left for a future PR though.
Better for a future PR. This is already better than what we have in NiFreeze (thanks for reviewing and the subsequent work), allows us to have a common, single authoritative location, and frees NiFreeze from hosting this.
Some inline comments about documentation and syntax highlighting.
|
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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>
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.