nibabel icon indicating copy to clipboard operation
nibabel copied to clipboard

BF - Fix io_orientation to process input axes by strength

Open leoyala opened this issue 2 weeks ago • 3 comments

This PR introduces changes aiming to make the axis assignment based on image affines more consistent. Instead of iterating over the axis in order range(p), the axis are now iterated over based on the strongest axis according to the rotation-shear matrix (RS).

Alternative implementation: Add a parameter to io_orientation called sort_by_strength which would activate this new sorting method, while preserving the old behavior. This parameter should then be propagated to as_closest_canonical and other places that might need it. In my opinion this might introduce more complexity and make it harder to maintain.

Notes: In principle the sorting could be done based on the rotation matrix R instead of RS. However this causes test_io_orientation to fail due to the check when additional columns are added to the affine.

Closes #1449

leoyala avatar Dec 09 '25 20:12 leoyala

After testing the new implementation on a few hundred images that have non-standard orientations (e.g. oblique planes). The previous bug seems to be resolved for most cases, however I still found one image for which loading the image assigns what should be an Axial plane to the Sagittal one. The affine of this image is:

[
[ 1.94222772e+00, -1.87633261e-01, -2.54019409e-01, 1.09614174e+02],
[ 1.56332731e+00,  3.34649354e-01, -7.97063559e-02, -1.08926643e+02],
[ 2.16188765e+00, -7.34265447e-02,  2.85847723e-01, -8.57995453e+01],
[ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, 1.00000000e+00]
]

Even more, the old implementation (using range(p)) seems to load this image correctly. Interestingly, running sflhd on this particular image returns the following, which seems to be the same as the new implementation.

sto_xyz:1	1.942228 -0.187633 -0.254019 109.614174 
sto_xyz:2	1.563327 0.334649 -0.079706 -108.926643 
sto_xyz:3	2.161888 -0.073427 0.285848 -85.799545 
sto_xyz:4	0.000000 0.000000 0.000000 1.000000 
sform_xorient	Left-to-Right
sform_yorient	Posterior-to-Anterior
sform_zorient	Inferior-to-Superior

Loading this image with another tool, like MITK, does seem to load the image in the correct orientation, assigning the axial plane to what the new implementation here assigns the sagittal one. Any suggestions for improvement are welcomed :)

leoyala avatar Dec 09 '25 21:12 leoyala

I think I may be thinking about this differently. The goal of an affine is to accurately describe the orientation of an image as a vector space, and the axis codes are just human conveniences. When your data is highly oblique, there just isn't an axial plane in the data, there's a phase-encoding, frequency-encoding and slice axis. We can assign a "nearest" world axis to each, but the only way to be wrong here is to use a procedure that (a) gives unintuitive results when each axis is dominantly aligned with a different world axis or (b) assigns different labels when the axes are reordered.

Looking at the affine you show, it is currently SAL by the naive implementation, but attempting to rotate to RAS will produce IAR.

Here's the rotation matrix:

array([[ 0.58855388, -0.48034116, -0.65028971],
       [ 0.47373557,  0.85670235, -0.20404827],
       [ 0.6551175 , -0.18797196,  0.73177018]])

What happens if you give MITK the as_closest_canonical'd version (old method)? My guess is that it will be inconsistent.

effigies avatar Dec 09 '25 22:12 effigies

Codecov Report

:x: Patch coverage is 96.77419% with 1 line in your changes missing coverage. Please review. :white_check_mark: Project coverage is 95.42%. Comparing base (0f61bcb) to head (a9caf8f).

Files with missing lines Patch % Lines
nibabel/tests/test_orientations.py 96.55% 0 Missing and 1 partial :warning:
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1450   +/-   ##
=======================================
  Coverage   95.42%   95.42%           
=======================================
  Files         209      209           
  Lines       29814    29844   +30     
  Branches     4483     4485    +2     
=======================================
+ Hits        28451    28480   +29     
  Misses        930      930           
- Partials      433      434    +1     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

codecov[bot] avatar Dec 10 '25 15:12 codecov[bot]