Skip to content

ENH: Write the demo's OME-Zarr transform as a sequence of elastix stages - #417

Merged
thewtex merged 1 commit into
mainfrom
ome-zarr-stages
Oct 7, 2026
Merged

thewtex merged 1 commit into
mainfrom
ome-zarr-stages

Conversation

@thewtex

@thewtex thewtex commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

The demo app writes the fixed-to-moving transform as an RFC-5 coordinate transformation in two places: the registered image's .ozx and the standalone transform .ozx. Until now that was a single affine, the product of all the elastix stages. It is now a sequence with one named entry per stage, listed in the order a point passes through them:

Stage RFC-5 form
translation translation
rigid sequence of a rotation, then a translation
affine affine

Why

  • Keep the stage results. Readers can inspect or reuse a single stage, such as the rigid alignment, instead of only the composed matrix.
  • Use the most specific type for each stage. RFC-5 prefers a translation over an equivalent affine. It has no rigid type, and its rotation carries no offset, so the rigid stage is a nested rotation + translation sequence.

Implementation details

  • Stage conversion: buildFixedToMovingTransform (src/io/rfc5-transform.ts) converts each stage separately with ngff-zarr's itkTransformToNgffTransform. ITK applies the last transform in its composite list first, so the list is reversed into RFC-5 order, where the first entry is applied first.
  • Frames: the translation and rigid stages are written in the fixed image's intrinsic frame. The affine stage also carries the change into the moving image's frame.
    • This keeps the translation a pure translation and the rotation proper (determinant 1, as RFC-5 requires), even when the two images disagree about an axis direction. Only an affine can hold that mirror.
    • The sequence composes to the previous single affine, up to rounding.
  • Caller-supplied stage lists: the change of frame goes on the last affine stage, and any later stages are written in the moving frame. If no stage is an affine, a trailing change_of_frame affine is appended, unless the change is exactly the identity.
  • Validation: assertTransformMatchesSystems now checks every stage, nested ones included. It checks matrix sizes, finite values, and that each rotation is orthonormal with determinant 1, which the v0.6 writer does not check.
  • Summary card: the registration summary still shows the composed matrix, which equals the composition of the written stages.
  • Docs: docs/decisions/ome-zarr-transform-output.md gains sections on the one-transformation-per-stage layout and on the frame each stage is written in. The README and architecture overview are updated to match.

Testing

  • Unit tests (pnpm test:unit): all 310 pass. rfc5-transform.ts coverage is 98.5% of lines and 100% of functions. The new tests cover:
    • stage names, types and order, and the rigid stage's rotation and translation;
    • that the stages compose to the whole-list affine;
    • a round trip through ngff-zarr's ngffTransformToItkTransform with a flipped axis;
    • the rules for which frame each stage uses and when change_of_frame is appended;
    • the validator's rejections.
  • Playwright (test/outputs.spec.ts, test/register-3d.spec.ts, Chromium): 15/15 pass. They check the written stage structure for 2D and 3D registrations and that the embedded and standalone transforms match.
  • Typecheck: clean.
  • CodeRabbit: no findings.

🤖 Generated with Claude Code

The registered image's OZX and the standalone transform OZX now write the
fixed-to-moving transform as an RFC-5 `sequence` of elastix's stages, in
the order a point passes through them, instead of the single affine they
compose to:

- `translation`: a `translation`;
- `rigid`: a nested `sequence` of a `rotation` and then a `translation`,
  since RFC-5 has no rigid type and a `rotation` carries no offset;
- `affine`: an `affine`.

Each stage is converted on its own. The translation and rigid stages are
expressed in the fixed image's intrinsic frame and the affine stage also
carries the change into the moving image's, so the translation stays a
pure translation and the rotation a proper rotation (RFC-5 requires a
determinant of one) even when the two images disagree about an axis
direction. The sequence composes to the previous affine up to rounding.
For a caller-supplied stage list, the change of frame goes on the last
affine stage, with later stages in the moving frame, or onto a trailing
`change_of_frame` affine when no stage is an affine.

`assertTransformMatchesSystems` now validates every stage, including the
rotation's orthonormal rows and determinant, which the v0.6 writer does
not check. The decision note, README, and architecture overview describe
the new layout, and the unit and Playwright tests check the stage
structure and that the stages compose to the whole-list affine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thewtex thewtex changed the title ENH: Keep the elastix stages in the demo's OME-Zarr transform output ENH: Write the demo's OME-Zarr transform as a sequence of elastix stages Oct 7, 2026
@thewtex

thewtex commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

@N-Dekker splitting the OME-Zarr transform

@thewtex
thewtex merged commit 7a31940 into main Oct 7, 2026
20 checks passed
@thewtex
thewtex deleted the ome-zarr-stages branch October 7, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant