ngff_zarr.itk_transform_to_ngff_transform

Convert ITK transforms to RFC-5 coordinate transformations.

The inverse of :mod:ngff_zarr.ngff_transform_to_itk_transform. Its purpose is persistence: a registration produces an ITK transform, and RFC-5 is where that result belongs once it is written next to the image.

The same two conventions apply in reverse – the spatial block and the offset are permuted from ITK’s fastest-axis-first order back to dims order, and ITK’s center of rotation is folded into the offset, since an RFC-5 affine has no center:

ITK:   y = A (x - c) + t + c
RFC-5: y = A x + b          with  b = t + c - A c

Module Contents

Classes

_FrameGeometry

The direction matrices and origins of an image pair, in ITK order.

Functions

_direct_parameter_count

Exact parameter count a directly decoded parameterization must carry.

_reject_non_linear

Refuse a transform that ITK itself reports as non-linear.

_working_precision

Guess whether the transform computes in single or double precision.

_working_scale

The magnitude the transform’s own numbers work at.

_reject_non_finite

Refuse an affine whose own numbers are not usable.

_check_affine_model

Refuse a transform the recovered affine does not actually describe.

_matrix_offset_by_probing

Recover (matrix, offset) by evaluating the transform.

_homogeneous

y = A x + b as a square matrix acting on homogeneous coordinates.

_matrix_offset_direct

(matrix, offset) read from the transform’s own numbers, or None.

_matrix_offset_from_itk

Reduce a native itk.Transform to a single matrix and offset.

_parameterization_name

The parameterization as a plain name, e.g. "Affine".

_matrix_offset_from_itkwasm

Decode an ITK-Wasm transform, falling back to itk when needed.

_itk_matrix_offset

Reduce any supported ITK transform input to a single matrix and offset.

_itk_axis_order

The spatial dims in ITK component order: x first, then y, then z.

_permutation_from_itk

The matrix sending an ITK-order vector to the dims-order vector.

_inverse_direction

The inverse of an RFC-4 direction matrix.

_frame_geometry

The geometry ngff_image_to_itk_image gives the two images.

_change_of_frame

Re-express y = A x + t through a change of frame on each side.

_check_frame_images

Validate the fixed/moving pair handed to a converter.

itk_transform_to_ngff_matrix

Convert an ITK transform to a matrix and offset in RFC-5 axis order.

itk_transform_to_ngff_transform

Convert an ITK transform to an RFC-5 coordinate transformation.

_simplify

Return a less expressive equivalent transformation, or None.

Data

API

ngff_zarr.itk_transform_to_ngff_transform._SPATIAL_DIMS

(‘x’, ‘y’, ‘z’)

ngff_zarr.itk_transform_to_ngff_transform._NON_LINEAR_PARAMETERIZATIONS

‘frozenset(…)’

ngff_zarr.itk_transform_to_ngff_transform._direct_parameter_count(parameterization: str, dimension: int) → int | None

Exact parameter count a directly decoded parameterization must carry.

What ITK’s MatrixOffsetTransformBase and friends pack, and therefore the ground truth for the transform’s dimensionality. None for the parameterizations this module does not decode arithmetically.

ngff_zarr.itk_transform_to_ngff_transform._reject_non_linear(itk_transform) → None

Refuse a transform that ITK itself reports as non-linear.

ngff_zarr.itk_transform_to_ngff_transform._AFFINE_CHECK_RTOL

None

ngff_zarr.itk_transform_to_ngff_transform._working_precision(*samples: numpy.ndarray) → str

Guess whether the transform computes in single or double precision.

itk.AffineTransform[itk.F, 3] answers TransformPoint with about seven digits, so holding it to a double-precision tolerance rejects every valid single-precision transform. Rather than read the ITK type name, which is a private spelling, look at whether the values coming back are exactly representable in float32: they are when the transform computes in float32, and a double-precision result generally is not. Guessing float32 for a double-precision transform that happens to return exact values only loosens the check on values that carry no round-off anyway.

ngff_zarr.itk_transform_to_ngff_transform._working_scale(offset: numpy.ndarray) → float

The magnitude the transform’s own numbers work at.

Both the probing step and the point the recovered affine is checked at follow it, because either one fixed near the origin stops meaning anything once the offset dominates: with a step of 1 and an offset of 1e15, T(h e_j) - T(0) keeps no significant digit and probing reports a zero matrix that a check near the origin cannot tell from the truth. An affine map is exact for any step, so there is no truncation error to trade against and the usual “small step” rule is exactly backwards here.

ngff_zarr.itk_transform_to_ngff_transform._reject_non_finite(itk_transform, matrix, offset) → None

Refuse an affine whose own numbers are not usable.

ngff_zarr.itk_transform_to_ngff_transform._check_affine_model(itk_transform, matrix, offset, samples) → None

Refuse a transform the recovered affine does not actually describe.

Neither reading a transform’s matrix nor probing it proves the transform is affine. Probing a deformation succeeds and returns a plausible affine that is simply wrong away from the probed points, and AzimuthElevationToCartesianTransform inherits an affine’s matrix and offset, overrides TransformPoint, and still reports itself linear (InsightSoftwareConsortium/ITK#6791). So the recovered model is confronted with one more evaluation, at a point no probe reached.

ngff_zarr.itk_transform_to_ngff_transform._matrix_offset_by_probing(itk_transform, dimension: int)

Recover (matrix, offset) by evaluating the transform.

The fallback for a transform that carries no matrix and offset of its own: offset = T(0) and matrix[:, j] = (T(h e_j) - T(0)) / h. Exact for any linear transform whatever parameterization it stores, but it pays for that in cancellation, which is why the step follows the transform’s own scale (see :func:_working_scale). The limit it leaves: a matrix coefficient whose contribution at the probe scale falls below the rounding of the offset itself is unrecoverable by evaluation and folds into the offset. Reading the matrix directly has no such limit, so this runs only where that is impossible.

ngff_zarr.itk_transform_to_ngff_transform._homogeneous(matrix: numpy.ndarray, offset: numpy.ndarray) → numpy.ndarray

y = A x + b as a square matrix acting on homogeneous coordinates.

ngff_zarr.itk_transform_to_ngff_transform._matrix_offset_direct(itk_transform, dimension: int)

(matrix, offset) read from the transform’s own numbers, or None.

Every MatrixOffsetTransformBase – an affine, and the rigid, rotation and similarity types built on it – evaluates y = GetMatrix() x + GetOffset() with the center of rotation already folded into the offset, whatever it stores its parameters as. Reading that pair beats recomposing it from three evaluations, which is a difference of nearly equal points: a transform combining a large offset with a small matrix coefficient, as nanometre coordinates produce, loses the coefficient to cancellation and would be persisted as a singular mapping.

None for a transform that carries no such pair, which then falls back to probing.

ngff_zarr.itk_transform_to_ngff_transform._matrix_offset_from_itk(itk_transform, dimension: int)

Reduce a native itk.Transform to a single matrix and offset.

ngff_zarr.itk_transform_to_ngff_transform._parameterization_name(transform_type) → str

The parameterization as a plain name, e.g. "Affine".

itkwasm.TransformParameterizations mixes in str, and since Python 3.11 str() on such a member returns "TransformParameterizations.Affine" rather than its value. Reading .value keeps a member and a plain string on the same footing.

ngff_zarr.itk_transform_to_ngff_transform._matrix_offset_from_itkwasm(entry, dimension: int)

Decode an ITK-Wasm transform, falling back to itk when needed.

ngff_zarr.itk_transform_to_ngff_transform._itk_matrix_offset(transform, dimension: int)

Reduce any supported ITK transform input to a single matrix and offset.

ngff_zarr.itk_transform_to_ngff_transform._itk_axis_order(spatial)

The spatial dims in ITK component order: x first, then y, then z.

ITK orders points fastest-axis-first by name, not by reversing however the caller spelled dims: reversing is only right for the canonical (z, y, x).

ngff_zarr.itk_transform_to_ngff_transform._permutation_from_itk(spatial) → numpy.ndarray

The matrix sending an ITK-order vector to the dims-order vector.

ngff_zarr.itk_transform_to_ngff_transform._inverse_direction(direction: numpy.ndarray) → numpy.ndarray

The inverse of an RFC-4 direction matrix.

Every column names one LPS axis with a sign, so the matrix is a signed permutation: orthogonal, and its transpose is its exact inverse. Going through np.linalg.inv would round where the transpose does not, and would answer a direction that somehow came through singular with a LinAlgError rather than with geometry.

class ngff_zarr.itk_transform_to_ngff_transform._FrameGeometry

Bases: typing.NamedTuple

The direction matrices and origins of an image pair, in ITK order.

A tuple, so it still unpacks straight into :func:_change_of_frame, whose arguments it names in order.

direction_in: numpy.ndarray

None

direction_out: numpy.ndarray

None

origin_in: numpy.ndarray

None

origin_out: numpy.ndarray

None

ngff_zarr.itk_transform_to_ngff_transform._frame_geometry(
fixed,
moving,
itk_dims,
) → ngff_zarr.itk_transform_to_ngff_transform._FrameGeometry

The geometry ngff_image_to_itk_image gives the two images.

ngff_zarr.itk_transform_to_ngff_transform._change_of_frame(
matrix,
offset,
direction_in,
direction_out,
origin_in,
origin_out,
)

Re-express y = A x + t through a change of frame on each side.

ngff_image_to_itk_image builds each image with origin = translation and the RFC-4 direction matrix D, so an intrinsic point p sits at the physical point phi(p) = D (p - o) + o. Given a mapping T between two frames, this returns phi_out^-1 . T . phi_in for the directions and origins supplied:

M = D_out^-1 A D_in
b = D_out^-1 (A (I - D_in) o_in + t - o_out) + o_out

phi^-1 has the same shape as phi with D^-1 in place of D, so the same formula serves both conversions: ITK to RFC-5 passes the images’ directions, RFC-5 to ITK passes their inverses. The scales cancel on both sides; the translations do not, which is why orientations alone would not be enough. With no anatomical orientation every direction is the identity and the mapping comes back unchanged.

ngff_zarr.itk_transform_to_ngff_transform._check_frame_images(fixed, moving, spatial)

Validate the fixed/moving pair handed to a converter.

ngff_zarr.itk_transform_to_ngff_transform.itk_transform_to_ngff_matrix(
transform,
dims: collections.abc.Sequence[str],
*,
fixed=None,
moving=None,
) → tuple[numpy.ndarray, numpy.ndarray]

Convert an ITK transform to a matrix and offset in RFC-5 axis order.

Parameters:
  • transform – An itk.Transform (including a CompositeTransform), an ITK-Wasm Transform, or an ITK-Wasm TransformList.

  • dims (Sequence[str]) – The axis names of the coordinate system the result should be expressed on, in RFC-5 (Zarr) order, e.g. ("z", "y", "x"). Only the spatial axes take part in the transform.

Returns:

(matrix, offset) over the spatial axes, in Zarr order.

Return type:

tuple[numpy.ndarray, numpy.ndarray]

Raises:

NotImplementedError – If the transform is not linear.

ngff_zarr.itk_transform_to_ngff_transform.itk_transform_to_ngff_transform(
transform,
dims: collections.abc.Sequence[str],
simplify: bool = True,
*,
fixed=None,
moving=None,
) → ngff_zarr.v06.zarr_metadata.Transform

Convert an ITK transform to an RFC-5 coordinate transformation.

This is what lets a registration result be written into an OME-Zarr store: run the registration, convert the transform, and attach it to the multiscales metadata.

Parameters:
  • transform – An itk.Transform (including the CompositeTransform an Elastix registration returns), an ITK-Wasm Transform, or an ITK-Wasm TransformList.

  • dims (Sequence[str]) – The axis names of the coordinate system the transformation is expressed on, in RFC-5 (Zarr) order. Non-spatial axes (t, c) are left untransformed.

  • simplify (bool) – Return the least expressive transformation that represents the mapping exactly – identity, translation, scale, or a sequence of scale and translation – falling back to affine. RFC-5 recommends this. A mirror never simplifies to a scale, since RFC-5 requires strictly positive scale factors. Note that multiscales > datasets accepts only a single scale, a single identity, or a two-element sequence of scale and translation, so a bare translation or an affine belongs in the multiscales-level coordinateTransformations instead. Set to False to always get an affine.

  • fixed (NgffImage, optional) – The fixed and moving images the ITK transform was produced on. An ITK transform acts on ITK physical space, which includes the direction matrix derived from RFC-4 anatomical orientation; an RFC-5 transformation acts on the intrinsic coordinate systems. Passing both images lets the conversion change frames exactly. Omitting them is exact only when neither image carries an anatomical orientation.

  • moving (NgffImage, optional) – See fixed. Pass both or neither.

Returns:

An RFC-5 coordinate transformation over dims.

Return type:

Transform

Raises:

NotImplementedError – If the transform is not linear. A non-linear registration has no affine equivalent.

ngff_zarr.itk_transform_to_ngff_transform._simplify(
matrix: numpy.ndarray,
offset: numpy.ndarray,
ndim: int,
) → ngff_zarr.v06.zarr_metadata.Transform | None

Return a less expressive equivalent transformation, or None.