Skip to content

BUG: Invert scaled and skewed versor transforms without throwing - #7056

Open
hjmjohnson wants to merge 2 commits into
InsightSoftwareConsortium:mainfrom
hjmjohnson:bug-scaleversor-inverse
Open

hjmjohnson wants to merge 2 commits into
InsightSoftwareConsortium:mainfrom
hjmjohnson:bug-scaleversor-inverse

Conversation

@hjmjohnson

@hjmjohnson hjmjohnson commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

RESTORE ITK v5.4 compatibility. A seldom-used optional tool built into BRAINSTools identified this regression. I don't think any other tools in my ITK forest are affected. This set of fixes restores the previous behavior.

ScaleVersor3DTransform, ScaleSkewVersor3DTransform, and ComposeScaleSkewVersor3DTransform throw "matrix does not represent rotation" from GetInverseTransform() in main. They inherit the same-type override that b025291 (#6686) added to VersorRigid3DTransform, so the inverse of a scaled or skewed rotation is built as a pure rotation.

  • BUG: all three invert to a MatrixOffsetTransformBase, as the base class does.
  • ENH: ComposeScaleSkewVersor3DTransform then inverts to its own type, falling back to a MatrixOffsetTransformBase when its rotation cannot be recovered (for example, a skew of 50).

Downstream impact: BRAINSTools inverts transforms when writing them, and 111 of its 416 tests fail against ITK main (its CI pins release-5.4, which predates the change).

Root cause and why the types differ

Transform::InvertTransform(const TTransform &) creates the inverse as TTransform, the static type at the call site. VersorRigid3DTransform::GetInverseTransform() calls it with *this as a VersorRigid3DTransform, so a ScaleVersor3DTransform gets a VersorRigid3DTransform inverse. GetInverse() then hands it the scaled matrix, and its ComputeMatrixParameters() calls Versor::Set(matrix), which rejects a matrix that is not a rotation.

  • ScaleVersor3DTransform adds a diagonal scale to the rotation matrix (R + diag(s − 1)); the inverse of such a matrix is generally not one, and its ComputeMatrixParameters() throws.
  • ScaleSkewVersor3DTransform adds scale and six skew terms; it can represent any matrix, but with no unique decomposition, and its ComputeMatrixParameters() throws. A same-type inverse would need a decomposition convention (for example the polar rotation plus the remainder as scale and skew) and is left for a separate change.
  • ComposeScaleSkewVersor3DTransform composes rotation, scale, and skew (R·S·K), and its ComputeMatrixParameters() decomposes a matrix that way, so it can keep its type. Its Gram–Schmidt decomposition loses orthogonality for large skew or extreme scale, and Versor::Set() (tolerance 1e-10) then throws; the second commit returns the base inverse in that case.

These three are the only subclasses of the classes b025291 changed that lacked an override of their own (CenteredEuler3DTransform and Similarity3DTransform have theirs).

Testing

macOS arm64, conda clang, Release, Module_ITKTransform=ON; each commit builds with 0 warnings and passes all 61 ITKTransform tests, and pre-commit run --all-files passes.

itkTestTransformGetInverse now, for each non-identity case:

  • catches an exception from GetInverseTransform() and reports it for that class, so one failure no longer hides the others;
  • checks that inverse(forward(p)) and forward(inverse(p)) both return p at three points;
  • rebuilds the inverse from its fixed and regular parameters, as a transform file stores it, and checks that it maps the points back too.

New cases: a rotated, anisotropically scaled, and (where applicable) skewed instance of each class, plus a ComposeScaleSkewVersor3DTransform with a skew of 50. Without the first commit's header change all three classes fail with the exception BRAINSTools hits; without the fallback, the skew-50 case fails the same way.

Found while testing #7041 in the downstream build forest: BRAINSTools' BRAINSTransformConvertMakeTestFiles aborts on this exception, and most of the 111 failures are tests that read the files it never writes.

AI assistance (Claude Code) was used to find the defect and draft the change; it was reviewed and tested locally.

@github-actions github-actions Bot added type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct area:Core Issues affecting the Core module labels Oct 31, 2026
@hjmjohnson
hjmjohnson force-pushed the bug-scaleversor-inverse branch from f68707c to 41e5c39 Compare October 31, 2026 17:29
@hjmjohnson hjmjohnson changed the title BUG: Invert scaled and skewed versor transforms to a general affine BUG: Invert scaled and skewed versor transforms without throwing Oct 31, 2026
@hjmjohnson

Copy link
Copy Markdown
Member Author

Force-push (content only, same base): ComposeScaleSkewVersor3DTransform now inverts to its own type (its matrix decomposition supports it); the other two keep the general affine inverse, with the reason stated per class.

@hjmjohnson
hjmjohnson force-pushed the bug-scaleversor-inverse branch from 41e5c39 to 5a9e4b1 Compare October 31, 2026 17:43
@hjmjohnson

Copy link
Copy Markdown
Member Author

Force-push (content only, same base): the round-trip check now also verifies forward(inverse(p)) == p, for every case that uses it.

@hjmjohnson

Copy link
Copy Markdown
Member Author

Force-push (content only, same base): split into a BUG: commit (all three invert to MatrixOffsetTransformBase) and an ENH: commit (ComposeScaleSkewVersor3DTransform inverts to its own type, falling back to the base inverse when its rotation cannot be recovered). The test now catches each case, and rebuilds every inverse from its parameters.

@hjmjohnson

Copy link
Copy Markdown
Member Author

Downstream check: this PR restores 121 BRAINSTools tests that pass on ITK release-5.4, fail on ITK main, and pass again with this change.

ITK under test BRAINSTools tests The 121 tests that fail on main
release-5.4 (070c1132bd0, 2026-09-16) 412/413 all pass
main (59629a74d9, 2026-10-28) 295/416 all fail
main + this PR (+ #7041) 458/458 all pass
Details

Run in the downstream build forest (ITK built with every default module and Python wrapping, then BRAINSTools and ANTs built against it with USE_SYSTEM_ITK), on macOS arm64. ANTs passes 179/179 in the same run.

  • Of the 121 failures on main, 16 abort directly on "matrix does not represent rotation" from GetInverseTransform() of a ScaleVersor3DTransform or ScaleSkewVersor3DTransform when BRAINSTools writes a transform; most of the rest read files that BRAINSTransformConvertMakeTestFiles never wrote because it aborted the same way.
  • The single release-5.4 failure is a timeout of BCDTest_writeBranded2DImage, which is not among the 121.
  • Test totals differ between rows because the BRAINSTools checkout differs between forests; the comparison above is by test name.
  • The main + PR row was measured at this PR's previous head, 48f079f4ef9; the current head changes only ComposeScaleSkewVersor3DTransform, which BRAINSTools does not use, and a confirming run on it is in progress.
  • The main + PR run used ITK with this PR and BUG: Stop iterating images that have no pixel buffer #7041 merged locally, and with Module_IsotropicWavelets off because of BUG: AnisotropicDiffusionLBR and IsotropicWavelets both define itk::StructureTensorImageFilter #7057 (unrelated to either PR). BRAINSTools' own CI pins ITK release-5.4, which is why it did not see this.

@hjmjohnson
hjmjohnson marked this pull request as ready for review October 31, 2026 19:39
@greptile-apps

This comment was marked as resolved.

@hjmjohnson

Copy link
Copy Markdown
Member Author

Confirmed on the current head 52f224c9187 (merged locally with #7041): BRAINSTools builds and passes 458/458, ANTs builds and passes 179/179, with no failures.

ScaleVersor3DTransform, ScaleSkewVersor3DTransform and
ComposeScaleSkewVersor3DTransform inherit VersorRigid3DTransform's
GetInverseTransform(), which builds the inverse as a rigid versor
transform, so Versor::Set() throws on the scaled or skewed matrix.

Invert them to a MatrixOffsetTransformBase, as the base class does. The
test checks each inverse in both directions and rebuilt from its
parameters.
Its ComputeMatrixParameters() decomposes a matrix into rotation, scale
and skew, so its inverse can keep the concrete type, as the other versor
transforms' inverses do. When the decomposed rotation is not orthogonal
to within Versor::Set()'s tolerance, as with a large skew, return the
MatrixOffsetTransformBase inverse instead of throwing.
@hjmjohnson

Copy link
Copy Markdown
Member Author

Two force-pushes: 52f224c9187 → b2bdfadcabe is content only (explicit InverseTransformBasePointer aliases, per compiler-cautions.md §2a); b2bdfadcabe → e8562cc5425 is a plain rebase on main. The factory-registration point Greptile raised predates this PR and is tracked in #7062.

hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Nov 1, 2026
hjmjohnson added a commit to hjmjohnson/ITK that referenced this pull request Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Core Issues affecting the Core module type:Bug Inconsistencies or issues which will cause an incorrect result under some or all circumstances type:Testing Ensure that the purpose of a class is met/the results on a wide set of test cases are correct

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant