Skip to content

zarrnii.transform

Spatial transform classes used for image registration and resampling.

Transformation classes for spatial transformations in ZarrNii.

Classes

zarrnii.transform.Transform

Bases: ABC

Abstract base class for spatial transformations.

This class defines the interface that all transformation classes must implement to be used with ZarrNii. Transformations convert coordinates from one space to another (e.g., subject space to template space).

Functions

zarrnii.transform.Transform.apply_transform(vecs) abstractmethod

Apply transformation to coordinate vectors.

Parameters:

  • vecs (ndarray) –

    Input coordinates as numpy array. Shape can be: - (3,) for single 3D point - (3, N) for N 3D points - (4,) for single homogeneous coordinate - (4, N) for N homogeneous coordinates

Returns:

  • ndarray

    Transformed coordinates with same shape as input

Raises:

  • NotImplementedError

    If not implemented by subclass

Source code in zarrnii/transform.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@abstractmethod
def apply_transform(self, vecs: np.ndarray) -> np.ndarray:
    """Apply transformation to coordinate vectors.

    Args:
        vecs: Input coordinates as numpy array. Shape can be:
            - (3,) for single 3D point
            - (3, N) for N 3D points
            - (4,) for single homogeneous coordinate
            - (4, N) for N homogeneous coordinates

    Returns:
        Transformed coordinates with same shape as input

    Raises:
        NotImplementedError: If not implemented by subclass
    """
    pass

zarrnii.transform.AffineTransform

Bases: Transform

Affine transformation for spatial coordinate mapping.

Represents a 4x4 affine transformation matrix that can be used to transform 3D coordinates between different coordinate systems. Supports various operations including matrix multiplication, inversion, and point transformation.

Attributes:

  • matrix (ndarray) –

    4x4 affine transformation matrix

Functions

zarrnii.transform.AffineTransform.from_txt(path, invert=False) classmethod

Create AffineTransform from text file containing matrix.

Parameters:

  • path (Union[str, bytes]) –

    Path to text file containing 4x4 affine matrix

  • invert (bool, default: False ) –

    Whether to invert the matrix after loading

Returns:

  • 'AffineTransform'

    AffineTransform instance with loaded matrix

Raises:

  • OSError

    If file cannot be read

  • ValueError

    If file does not contain valid 4x4 matrix

Source code in zarrnii/transform.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@classmethod
def from_txt(
    cls, path: Union[str, bytes], invert: bool = False
) -> "AffineTransform":
    """Create AffineTransform from text file containing matrix.

    Args:
        path: Path to text file containing 4x4 affine matrix
        invert: Whether to invert the matrix after loading

    Returns:
        AffineTransform instance with loaded matrix

    Raises:
        OSError: If file cannot be read
        ValueError: If file does not contain valid 4x4 matrix
    """
    matrix = np.loadtxt(path)
    if invert:
        matrix = np.linalg.inv(matrix)
    return cls(matrix=matrix)
zarrnii.transform.AffineTransform.from_array(matrix, invert=False) classmethod

Create AffineTransform from numpy array.

Parameters:

  • matrix (ndarray) –

    4x4 numpy array representing affine transformation

  • invert (bool, default: False ) –

    Whether to invert the matrix

Returns:

  • 'AffineTransform'

    AffineTransform instance with the matrix

Raises:

  • ValueError

    If matrix is not 4x4

Source code in zarrnii/transform.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@classmethod
def from_array(cls, matrix: np.ndarray, invert: bool = False) -> "AffineTransform":
    """Create AffineTransform from numpy array.

    Args:
        matrix: 4x4 numpy array representing affine transformation
        invert: Whether to invert the matrix

    Returns:
        AffineTransform instance with the matrix

    Raises:
        ValueError: If matrix is not 4x4
    """
    if matrix.shape != (4, 4):
        raise ValueError(f"Matrix must be 4x4, got shape {matrix.shape}")

    if invert:
        matrix = np.linalg.inv(matrix)
    return cls(matrix=matrix)
zarrnii.transform.AffineTransform.identity() classmethod

Create identity transformation.

Returns:

  • 'AffineTransform'

    AffineTransform representing identity transformation (no change)

Source code in zarrnii/transform.py
100
101
102
103
104
105
106
107
@classmethod
def identity(cls) -> "AffineTransform":
    """Create identity transformation.

    Returns:
        AffineTransform representing identity transformation (no change)
    """
    return cls(matrix=np.eye(4, 4))
zarrnii.transform.AffineTransform.apply_transform(vecs)

Apply transformation to coordinate vectors.

Parameters:

  • vecs (ndarray) –

    Input coordinates to transform

Returns:

  • ndarray

    Transformed coordinates

Source code in zarrnii/transform.py
198
199
200
201
202
203
204
205
206
207
def apply_transform(self, vecs: np.ndarray) -> np.ndarray:
    """Apply transformation to coordinate vectors.

    Args:
        vecs: Input coordinates to transform

    Returns:
        Transformed coordinates
    """
    return self @ vecs
zarrnii.transform.AffineTransform.invert()

Return the inverse of the matrix transformation.

Returns:

  • 'AffineTransform'

    New AffineTransform with inverted matrix

Raises:

  • LinAlgError

    If matrix is singular and cannot be inverted

Source code in zarrnii/transform.py
209
210
211
212
213
214
215
216
217
218
def invert(self) -> "AffineTransform":
    """Return the inverse of the matrix transformation.

    Returns:
        New AffineTransform with inverted matrix

    Raises:
        np.linalg.LinAlgError: If matrix is singular and cannot be inverted
    """
    return AffineTransform.from_array(np.linalg.inv(self.matrix))
zarrnii.transform.AffineTransform.update_for_orientation(input_orientation, output_orientation)

Update the matrix to map from input orientation to output orientation.

Parameters:

  • input_orientation (str) –

    Current anatomical orientation (e.g., 'RPI')

  • output_orientation (str) –

    Target anatomical orientation (e.g., 'RAS')

Returns:

  • 'AffineTransform'

    New AffineTransform updated for orientation mapping

Raises:

  • ValueError

    If orientations are invalid or cannot be matched

Source code in zarrnii/transform.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def update_for_orientation(
    self, input_orientation: str, output_orientation: str
) -> "AffineTransform":
    """Update the matrix to map from input orientation to output orientation.

    Args:
        input_orientation: Current anatomical orientation (e.g., 'RPI')
        output_orientation: Target anatomical orientation (e.g., 'RAS')

    Returns:
        New AffineTransform updated for orientation mapping

    Raises:
        ValueError: If orientations are invalid or cannot be matched
    """

    # Define a mapping of anatomical directions to axis indices and flips
    axis_map = {
        "R": (0, 1),
        "L": (0, -1),
        "A": (1, 1),
        "P": (1, -1),
        "S": (2, 1),
        "I": (2, -1),
    }

    # Parse the input and output orientations
    input_axes = [axis_map[ax] for ax in input_orientation]
    output_axes = [axis_map[ax] for ax in output_orientation]

    # Create a mapping from input to output
    reorder_indices = [None] * 3
    flip_signs = [1] * 3

    for out_idx, (out_axis, out_sign) in enumerate(output_axes):
        for in_idx, (in_axis, in_sign) in enumerate(input_axes):
            if out_axis == in_axis:  # Match axis
                reorder_indices[out_idx] = in_idx
                flip_signs[out_idx] = out_sign * in_sign
                break

    # Reorder and flip the affine matrix
    reordered_matrix = np.zeros_like(self.matrix)
    for i, (reorder_idx, flip_sign) in enumerate(zip(reorder_indices, flip_signs)):
        if reorder_idx is None:
            raise ValueError(
                f"Cannot match all axes from {input_orientation} to {output_orientation}."
            )
        reordered_matrix[i, :3] = flip_sign * self.matrix[reorder_idx, :3]
        reordered_matrix[i, 3] = flip_sign * self.matrix[reorder_idx, 3]
    reordered_matrix[3, :] = self.matrix[3, :]  # Preserve the homogeneous row

    return AffineTransform.from_array(reordered_matrix)

zarrnii.transform.DisplacementTransform

Bases: Transform

Non-linear displacement field transformation.

Represents a displacement field transformation where each point in space has an associated displacement vector. Uses interpolation to compute displacements for arbitrary coordinates.

Attributes:

  • disp_xyz (ndarray) –

    Displacement vectors at grid points (4D array: x, y, z, vector_component)

  • disp_grid (Tuple[ndarray, ...]) –

    Grid coordinates for displacement field

  • disp_affine (AffineTransform) –

    Affine transformation from world to displacement field coordinates

Functions

zarrnii.transform.DisplacementTransform.from_nifti(path) classmethod

Create DisplacementTransform from NIfTI file.

Parameters:

  • path (Union[str, bytes]) –

    Path to NIfTI displacement field file

Returns:

  • 'DisplacementTransform'

    DisplacementTransform instance loaded from file

Raises:

  • OSError

    If file cannot be read

  • ValueError

    If file format is invalid

Source code in zarrnii/transform.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
@classmethod
def from_nifti(cls, path: Union[str, bytes]) -> "DisplacementTransform":
    """Create DisplacementTransform from NIfTI file.

    Args:
        path: Path to NIfTI displacement field file

    Returns:
        DisplacementTransform instance loaded from file

    Raises:
        OSError: If file cannot be read
        ValueError: If file format is invalid
    """
    disp_nib = nib.load(path)
    disp_xyz = disp_nib.get_fdata().squeeze()
    disp_affine = AffineTransform.from_array(disp_nib.affine)

    # Convert from ITK transform convention
    # ITK uses opposite sign convention for x and y displacements
    disp_xyz[:, :, :, 0] = -disp_xyz[:, :, :, 0]
    disp_xyz[:, :, :, 1] = -disp_xyz[:, :, :, 1]

    # Create grid coordinates
    disp_grid = (
        np.arange(disp_xyz.shape[0]),
        np.arange(disp_xyz.shape[1]),
        np.arange(disp_xyz.shape[2]),
    )

    return cls(
        disp_xyz=disp_xyz,
        disp_grid=disp_grid,
        disp_affine=disp_affine,
    )
zarrnii.transform.DisplacementTransform.apply_transform(vecs)

Apply displacement transformation to coordinate vectors.

Transforms input coordinates by interpolating displacement vectors from the displacement field and adding them to the input coordinates.

Parameters:

  • vecs (ndarray) –

    Input coordinates as numpy array. Shape should be (3, N) for N points or (3,) for single point

Returns:

  • ndarray

    Transformed coordinates with same shape as input

Notes

Points outside the displacement field domain are filled with zero displacement (no transformation).

Source code in zarrnii/transform.py
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def apply_transform(self, vecs: np.ndarray) -> np.ndarray:
    """Apply displacement transformation to coordinate vectors.

    Transforms input coordinates by interpolating displacement vectors
    from the displacement field and adding them to the input coordinates.

    Args:
        vecs: Input coordinates as numpy array. Shape should be (3, N) for
            N points or (3,) for single point

    Returns:
        Transformed coordinates with same shape as input

    Notes:
        Points outside the displacement field domain are filled with
        zero displacement (no transformation).
    """
    # Transform points to voxel space of the displacement field
    vox_vecs = self.disp_affine.invert() @ vecs

    # Initialize displacement vectors
    disp_vecs = np.zeros(vox_vecs.shape)

    # Interpolate displacement for each spatial dimension (x, y, z)
    for ax in range(3):
        disp_vecs[ax, :] = interpn(
            self.disp_grid,
            self.disp_xyz[:, :, :, ax].squeeze(),
            vox_vecs[:3, :].T,
            method="linear",
            bounds_error=False,
            fill_value=0,
        )

    # Add displacement to original coordinates
    return vecs + disp_vecs