Skip to content

API Reference

This page documents the core classes, methods, and functions in ZarrNii.


Core Classes

ZarrNii

The ZarrNii class provides tools for reading, writing, and transforming datasets in OME-Zarr and NIfTI formats.

Key Methods

Represents a Zarr-based image with NIfTI compatibility and OME-Zarr metadata.

Attributes:

Name Type Description
darr Array

The main dask array holding image data.

affine AffineTransform

The affine transformation matrix.

axes_order str

The order of the axes in the data array ('ZYX' or 'XYZ').

axes Optional[List[Dict]]

Metadata about the axes (from OME-Zarr).

coordinate_transformations Optional[List[Dict]]

Transformations applied to the data (from OME-Zarr metadata).

omero Optional[Dict]

Metadata related to visualization and channels (from OME-Zarr).


Methods

from_ome_zarr

Reads in an OME-Zarr file as a ZarrNii image, optionally as a reference.

Parameters:

Name Type Description Default
path str

Path to the OME-Zarr file.

required
level int

Pyramid level to load (default: 0).

0
channels list

Channels to load (default: [0]).

[0]
chunks str or tuple

Chunk size for dask array (default: "auto").

'auto'
z_level_offset int

Offset for Z downsampling level (default: -2).

-2
rechunk bool

Whether to rechunk the data (default: False).

False
storage_options dict

Storage options for Zarr.

None
orientation str

Default input orientation if none is specified in metadata (default: 'IPL').

'IPL'
as_ref bool

If True, creates an empty dask array with the correct shape instead of loading data.

False
zooms list or ndarray

Target voxel spacing in xyz (only valid if as_ref=True).

None

Returns:

Name Type Description
ZarrNii

A populated ZarrNii instance.

Raises:

Type Description
ValueError

If zooms is specified when as_ref=False.

Source code in zarrnii/core.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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
328
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
365
366
367
@classmethod
def from_ome_zarr(
    cls,
    path,
    level=0,
    channels=[0],
    chunks="auto",
    z_level_offset=-2,
    rechunk=False,
    storage_options=None,
    orientation="IPL",
    as_ref=False,
    zooms=None,
):
    """
    Reads in an OME-Zarr file as a ZarrNii image, optionally as a reference.

    Parameters:
        path (str): Path to the OME-Zarr file.
        level (int): Pyramid level to load (default: 0).
        channels (list): Channels to load (default: [0]).
        chunks (str or tuple): Chunk size for dask array (default: "auto").
        z_level_offset (int): Offset for Z downsampling level (default: -2).
        rechunk (bool): Whether to rechunk the data (default: False).
        storage_options (dict): Storage options for Zarr.
        orientation (str): Default input orientation if none is specified in metadata (default: 'IPL').
        as_ref (bool): If True, creates an empty dask array with the correct shape instead of loading data.
        zooms (list or np.ndarray): Target voxel spacing in xyz (only valid if as_ref=True).

    Returns:
        ZarrNii: A populated ZarrNii instance.

    Raises:
        ValueError: If `zooms` is specified when `as_ref=False`.
    """

    if not as_ref and zooms is not None:
        raise ValueError("`zooms` can only be used when `as_ref=True`.")

    # Determine the level and whether downsampling is required
    if not as_ref:
        (
            level,
            do_downsample,
            downsampling_kwargs,
        ) = cls.get_level_and_downsampling_kwargs(
            path, level, z_level_offset, storage_options=storage_options
        )
    else:
        do_downsample = False

    # Open the Zarr metadata
    store = zarr.open(path, mode="r")
    multiscales = store.attrs.get("multiscales", [{}])
    datasets = multiscales[0].get("datasets", [{}])
    coordinate_transformations = datasets[level].get(
        "coordinateTransformations", []
    )
    axes = multiscales[0].get("axes", [])
    omero = store.attrs.get("omero", {})

    # Read orientation metadata (default to `orientation` if not present)
    orientation = store.attrs.get("orientation", orientation)

    # Load data or metadata as needed
    darr_base = da.from_zarr(
        path, component=f"/{level}", storage_options=storage_options
    )[channels, :, :, :]
    shape = darr_base.shape

    affine = cls.construct_affine(coordinate_transformations, orientation)

    if zooms is not None:
        # Handle zoom adjustments
        in_zooms = np.sqrt(
            (affine[:3, :3] ** 2).sum(axis=0)
        )  # Current voxel spacing
        scaling_factor = in_zooms / zooms
        new_shape = [
            shape[0],
            int(np.floor(shape[1] * scaling_factor[2])),  # Z
            int(np.floor(shape[2] * scaling_factor[1])),  # Y
            int(np.floor(shape[3] * scaling_factor[0])),  # X
        ]
        np.fill_diagonal(affine[:3, :3], zooms)
    else:
        new_shape = shape

    # we want to downsample *before* we rechunk

    if as_ref:
        # Create an empty array with the updated shape
        darr = da.empty(new_shape, chunks=chunks, dtype=darr_base.dtype)
    else:
        darr = darr_base

    znimg = cls(
        darr,
        affine=AffineTransform.from_array(affine),
        axes_order="ZYX",
        axes=axes,
        coordinate_transformations=coordinate_transformations,
        omero=omero,
    )

    if do_downsample:
        znimg = znimg.downsample(**downsampling_kwargs)

    if rechunk:
        znimg.darr = znimg.darr.rechunk(chunks)

    return znimg

from_nifti

Creates a ZarrNii instance from a NIfTI file. Populates OME-Zarr metadata based on the NIfTI affine matrix.

Parameters:

Name Type Description Default
path str

Path to the NIfTI file.

required
chunks str or tuple

Chunk size for dask array (default: "auto").

'auto'
as_ref bool

If True, creates an empty dask array with the correct shape instead of loading data.

False
zooms list or ndarray

Target voxel spacing in xyz (only valid if as_ref=True).

None

Returns:

Name Type Description
ZarrNii

A populated ZarrNii instance.

Raises:

Type Description
ValueError

If zooms is specified when as_ref=False.

Source code in zarrnii/core.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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
@classmethod
def from_nifti(cls, path, chunks="auto", as_ref=False, zooms=None):
    """
    Creates a ZarrNii instance from a NIfTI file. Populates OME-Zarr metadata
    based on the NIfTI affine matrix.

    Parameters:
        path (str): Path to the NIfTI file.
        chunks (str or tuple): Chunk size for dask array (default: "auto").
        as_ref (bool): If True, creates an empty dask array with the correct shape instead of loading data.
        zooms (list or np.ndarray): Target voxel spacing in xyz (only valid if as_ref=True).

    Returns:
        ZarrNii: A populated ZarrNii instance.

    Raises:
        ValueError: If `zooms` is specified when `as_ref=False`.
    """
    if not as_ref and zooms is not None:
        raise ValueError("`zooms` can only be used when `as_ref=True`.")

    # Load the NIfTI file and extract metadata
    nii = nib.load(path)
    shape = nii.header.get_data_shape()
    affine = nii.affine

    # Adjust shape and affine if zooms are provided
    if zooms is not None:
        in_zooms = np.sqrt(
            (affine[:3, :3] ** 2).sum(axis=0)
        )  # Current voxel spacing
        scaling_factor = in_zooms / zooms
        new_shape = [
            int(np.floor(shape[0] * scaling_factor[2])),  # Z
            int(np.floor(shape[1] * scaling_factor[1])),  # Y
            int(np.floor(shape[2] * scaling_factor[0])),  # X
        ]
        np.fill_diagonal(affine[:3, :3], zooms)
    else:
        new_shape = shape

    if as_ref:
        # Create an empty dask array with the adjusted shape
        darr = da.empty((1, *new_shape), chunks=chunks, dtype="float32")
    else:
        # Load the NIfTI data and convert to a dask array
        data = np.expand_dims(nii.get_fdata(), axis=0)  # Add a channel dimension
        darr = da.from_array(data, chunks=chunks)

    # Define axes order and metadata
    axes_order = "XYZ"
    axes = [
        {"name": "channel", "type": "channel", "unit": None},
        {"name": "x", "type": "space", "unit": "millimeter"},
        {"name": "y", "type": "space", "unit": "millimeter"},
        {"name": "z", "type": "space", "unit": "millimeter"},
    ]

    # Extract coordinate transformations from the affine matrix
    scale = np.sqrt((affine[:3, :3] ** 2).sum(axis=0))  # Diagonal scales
    translation = affine[:3, 3]  # Translation vector
    coordinate_transformations = [
        {"type": "scale", "scale": [1] + scale.tolist()},  # Add channel scale
        {
            "type": "translation",
            "translation": [0] + translation.tolist(),
        },  # Add channel translation
    ]

    # Define basic Omero metadata
    omero = {
        "channels": [{"label": "Channel-0"}],  # Placeholder channel information
        "rdefs": {"model": "color"},
    }

    # Create and return the ZarrNii instance
    return cls(
        darr,
        affine=AffineTransform.from_array(affine),
        axes_order=axes_order,
        axes=axes,
        coordinate_transformations=coordinate_transformations,
        omero=omero,
    )

to_ome_zarr

Save the current ZarrNii instance to an OME-Zarr file, always writing axes in ZYX order.

Parameters:

Name Type Description Default
filename str

Output path for the OME-Zarr file.

required
max_layer int

Maximum number of downsampling layers (default: 4).

4
scaling_method str

Method for downsampling (default: "local_mean").

'local_mean'
**kwargs

Additional arguments for write_image.

{}
Source code in zarrnii/core.py
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
def to_ome_zarr(self, filename, max_layer=4, scaling_method="local_mean", **kwargs):
    """
    Save the current ZarrNii instance to an OME-Zarr file, always writing
    axes in ZYX order.

    Parameters:
        filename (str): Output path for the OME-Zarr file.
        max_layer (int): Maximum number of downsampling layers (default: 4).
        scaling_method (str): Method for downsampling (default: "local_mean").
        **kwargs: Additional arguments for `write_image`.
    """
    # Always write OME-Zarr axes as ZYX
    axes = [
        {"name": "c", "type": "channel", "unit": None},
        {"name": "z", "type": "space", "unit": "micrometer"},
        {"name": "y", "type": "space", "unit": "micrometer"},
        {"name": "x", "type": "space", "unit": "micrometer"},
    ]

    # Reorder data if the axes order is XYZ (NIfTI-like)
    if self.axes_order == "XYZ":
        out_darr = da.moveaxis(
            self.darr, (0, 1, 2, 3), (0, 3, 2, 1)
        )  # Reorder to ZYX
        #   flip_xfm = np.diag((-1, -1, -1, 1))  # Apply flips for consistency
        # out_affine = flip_xfm @ self.affine
        out_affine = self.reorder_affine_xyz_zyx(self.affine.matrix)
    #  voxdim = np.flip(voxdim)  # Adjust voxel dimensions to ZYX
    else:
        out_darr = self.darr
        out_affine = self.affine.matrix

    # Extract offset and voxel dimensions from the affine matrix
    offset = out_affine[:3, 3]
    voxdim = np.sqrt((out_affine[:3, :3] ** 2).sum(axis=0))  # Extract scales

    # Prepare coordinate transformations
    coordinate_transformations = []
    for layer in range(max_layer + 1):
        scale = [
            1,
            voxdim[0],
            (2**layer) * voxdim[1],  # Downsampling in Y
            (2**layer) * voxdim[2],  # Downsampling in X
        ]
        translation = [
            0,
            offset[0],
            offset[1] / (2**layer),
            offset[2] / (2**layer),
        ]
        coordinate_transformations.append(
            [
                {"type": "scale", "scale": scale},
                {"type": "translation", "translation": translation},
            ]
        )

    # Set up Zarr store
    store = zarr.storage.FSStore(filename, dimension_separator="/", mode="w")
    group = zarr.group(store, overwrite=True)

    # Add metadata for orientation
    group.attrs["orientation"] = affine_to_orientation(
        out_affine
    )  # Write current orientation

    # Set up scaler for multi-resolution pyramid
    if max_layer == 0:
        scaler = None
    else:
        scaler = Scaler(max_layer=max_layer, method=scaling_method)

    # Write the data to OME-Zarr
    with ProgressBar():
        write_image(
            image=out_darr,
            group=group,
            scaler=scaler,
            coordinate_transformations=coordinate_transformations,
            axes=axes,
            **kwargs,
        )

to_nifti

Convert the current ZarrNii instance to a NIfTI-1 image (Nifti1Image) and optionally save it to a file.

Parameters:

Name Type Description Default
filename str

Output path for the NIfTI file. If None, the function returns the NIfTI object.

None

Returns:

Type Description

nib.Nifti1Image: The NIfTI-1 image representation of the ZarrNii instance if filename is not provided.

Notes
  • Reorders data to XYZ order if the current axes_order is ZYX.
  • Adjusts the affine matrix accordingly to match the reordered data.
Source code in zarrnii/core.py
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
def to_nifti(self, filename=None):
    """
    Convert the current ZarrNii instance to a NIfTI-1 image (Nifti1Image)
    and optionally save it to a file.

    Parameters:
        filename (str, optional): Output path for the NIfTI file. If None,
                                  the function returns the NIfTI object.

    Returns:
        nib.Nifti1Image: The NIfTI-1 image representation of the ZarrNii instance
                         if `filename` is not provided.

    Notes:
        - Reorders data to XYZ order if the current `axes_order` is ZYX.
        - Adjusts the affine matrix accordingly to match the reordered data.
    """

    # Reorder data to match NIfTI's expected XYZ order if necessary
    if self.axes_order == "ZYX":
        data = da.moveaxis(
            self.darr, (0, 1, 2, 3), (0, 3, 2, 1)
        ).compute()  # Reorder to XYZ
        affine = self.reorder_affine_xyz_zyx(
            self.affine.matrix
        )  # Reorder affine to match
    else:
        data = self.darr.compute()
        affine = self.affine.matrix  # No reordering needed
    # Create the NIfTI-1 image
    nii_img = nib.Nifti1Image(
        data[0], affine
    )  # Remove the channel dimension for NIfTI

    # Save the NIfTI file if a filename is provided
    if filename:
        nib.save(nii_img, filename)
    else:
        return nii_img

downsample

Downsamples the ZarrNii instance by local mean reduction.

Parameters:

Name Type Description Default
along_x int

Downsampling factor along the X-axis (default: 1).

1
along_y int

Downsampling factor along the Y-axis (default: 1).

1
along_z int

Downsampling factor along the Z-axis (default: 1).

1
level int

If specified, calculates downsampling factors based on the level, with Z-axis adjusted by z_level_offset.

None
z_level_offset int

Offset for the Z-axis downsampling factor when using level (default: -2).

-2

Returns:

Name Type Description
ZarrNii

A new ZarrNii instance with the downsampled data and updated affine.

Notes
  • If level is provided, downsampling factors are calculated as:
    • along_x = along_y = 2**level
    • along_z = 2**max(level + z_level_offset, 0)
  • Updates the affine matrix to reflect the new voxel size after downsampling.
  • Uses dask.array.coarsen for efficient reduction along specified axes.
Example

Downsample by specific factors

downsampled_znimg = znimg.downsample(along_x=2, along_y=2, along_z=1)

Downsample using a pyramid level

downsampled_znimg = znimg.downsample(level=2)

Source code in zarrnii/core.py
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
def downsample(
    self, along_x=1, along_y=1, along_z=1, level=None, z_level_offset=-2
):
    """
    Downsamples the ZarrNii instance by local mean reduction.

    Parameters:
        along_x (int, optional): Downsampling factor along the X-axis (default: 1).
        along_y (int, optional): Downsampling factor along the Y-axis (default: 1).
        along_z (int, optional): Downsampling factor along the Z-axis (default: 1).
        level (int, optional): If specified, calculates downsampling factors based on the level,
                               with Z-axis adjusted by `z_level_offset`.
        z_level_offset (int, optional): Offset for the Z-axis downsampling factor when using `level`
                                        (default: -2).

    Returns:
        ZarrNii: A new ZarrNii instance with the downsampled data and updated affine.

    Notes:
        - If `level` is provided, downsampling factors are calculated as:
            - `along_x = along_y = 2**level`
            - `along_z = 2**max(level + z_level_offset, 0)`
        - Updates the affine matrix to reflect the new voxel size after downsampling.
        - Uses `dask.array.coarsen` for efficient reduction along specified axes.

    Example:
        # Downsample by specific factors
        downsampled_znimg = znimg.downsample(along_x=2, along_y=2, along_z=1)

        # Downsample using a pyramid level
        downsampled_znimg = znimg.downsample(level=2)
    """
    # Calculate downsampling factors if level is specified
    if level is not None:
        along_x = 2**level
        along_y = 2**level
        level_z = max(level + z_level_offset, 0)
        along_z = 2**level_z

    # Determine axes mapping based on axes_order
    if self.axes_order == "XYZ":
        axes = {0: 1, 1: along_x, 2: along_y, 3: along_z}  # (C, X, Y, Z)
    else:
        axes = {0: 1, 1: along_z, 2: along_y, 3: along_x}  # (C, Z, Y, X)

    # Perform local mean reduction using coarsen
    agg_func = np.mean
    darr_scaled = da.coarsen(agg_func, x=self.darr, axes=axes, trim_excess=True)

    # Update the affine matrix to reflect downsampling
    scaling_matrix = np.diag((along_x, along_y, along_z, 1))
    new_affine = AffineTransform.from_array(scaling_matrix @ self.affine.matrix)

    # Create and return a new ZarrNii instance
    return ZarrNii.from_darr(
        darr_scaled, affine=new_affine, axes_order=self.axes_order
    )

upsample

Upsamples the ZarrNii instance using scipy.ndimage.zoom.

Parameters:

Name Type Description Default
along_x int

Upsampling factor along the X-axis (default: 1).

1
along_y int

Upsampling factor along the Y-axis (default: 1).

1
along_z int

Upsampling factor along the Z-axis (default: 1).

1
to_shape tuple

Target shape for upsampling. Should include all dimensions (e.g., (c, z, y, x) for ZYX or (c, x, y, z) for XYZ). If provided, along_x, along_y, and along_z are ignored.

None

Returns:

Name Type Description
ZarrNii

A new ZarrNii instance with the upsampled data and updated affine.

Notes
  • This method supports both direct scaling via along_* factors or target shape via to_shape.
  • If to_shape is provided, chunk sizes and scaling factors are dynamically calculated.
  • Currently, the method assumes axes_order != 'XYZ' for proper affine scaling.
  • The affine matrix is updated to reflect the new voxel size after upsampling.
Example

Upsample with scaling factors

upsampled_znimg = znimg.upsample(along_x=2, along_y=2, along_z=2)

Upsample to a specific shape

upsampled_znimg = znimg.upsample(to_shape=(1, 256, 256, 256))

Source code in zarrnii/core.py
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
def upsample(self, along_x=1, along_y=1, along_z=1, to_shape=None):
    """
    Upsamples the ZarrNii instance using `scipy.ndimage.zoom`.

    Parameters:
        along_x (int, optional): Upsampling factor along the X-axis (default: 1).
        along_y (int, optional): Upsampling factor along the Y-axis (default: 1).
        along_z (int, optional): Upsampling factor along the Z-axis (default: 1).
        to_shape (tuple, optional): Target shape for upsampling. Should include all dimensions
                                     (e.g., `(c, z, y, x)` for ZYX or `(c, x, y, z)` for XYZ).
                                     If provided, `along_x`, `along_y`, and `along_z` are ignored.

    Returns:
        ZarrNii: A new ZarrNii instance with the upsampled data and updated affine.

    Notes:
        - This method supports both direct scaling via `along_*` factors or target shape via `to_shape`.
        - If `to_shape` is provided, chunk sizes and scaling factors are dynamically calculated.
        - Currently, the method assumes `axes_order != 'XYZ'` for proper affine scaling.
        - The affine matrix is updated to reflect the new voxel size after upsampling.

    Example:
        # Upsample with scaling factors
        upsampled_znimg = znimg.upsample(along_x=2, along_y=2, along_z=2)

        # Upsample to a specific shape
        upsampled_znimg = znimg.upsample(to_shape=(1, 256, 256, 256))
    """
    # Determine scaling and chunks based on input parameters
    if to_shape is None:
        if self.axes_order == "XYZ":
            scaling = (1, along_x, along_y, along_z)
        else:
            scaling = (1, along_z, along_y, along_x)

        chunks_out = tuple(
            tuple(c * scale for c in chunks_i)
            for chunks_i, scale in zip(self.darr.chunks, scaling)
        )
    else:
        chunks_out, scaling = self.__get_upsampled_chunks(to_shape)

    # Define block-wise upsampling function
    def zoom_blocks(x, block_info=None):
        """
        Scales blocks to the desired size using `scipy.ndimage.zoom`.

        Parameters:
            x (np.ndarray): Input block data.
            block_info (dict, optional): Metadata about the current block.

        Returns:
            np.ndarray: The upscaled block.
        """
        # Calculate scaling factors based on input and output chunk shapes
        scaling = tuple(
            out_n / in_n
            for out_n, in_n in zip(block_info[None]["chunk-shape"], x.shape)
        )
        return zoom(x, scaling, order=1, prefilter=False)

    # Perform block-wise upsampling
    darr_scaled = da.map_blocks(
        zoom_blocks, self.darr, dtype=self.darr.dtype, chunks=chunks_out
    )

    # Update the affine matrix to reflect the new voxel size
    if self.axes_order == "XYZ":
        scaling_matrix = np.diag(
            (1 / scaling[1], 1 / scaling[2], 1 / scaling[3], 1)
        )
    else:
        scaling_matrix = np.diag(
            (1 / scaling[-1], 1 / scaling[-2], 1 / scaling[-3], 1)
        )
    new_affine = AffineTransform.from_array(scaling_matrix @ self.affine.matrix)

    # Return a new ZarrNii instance with the upsampled data
    return ZarrNii.from_darr(
        darr_scaled.rechunk(), affine=new_affine, axes_order=self.axes_order
    )

Remaining Methods and Functions

Represents a Zarr-based image with NIfTI compatibility and OME-Zarr metadata.

Attributes:

Name Type Description
darr Array

The main dask array holding image data.

affine AffineTransform

The affine transformation matrix.

axes_order str

The order of the axes in the data array ('ZYX' or 'XYZ').

axes Optional[List[Dict]]

Metadata about the axes (from OME-Zarr).

coordinate_transformations Optional[List[Dict]]

Transformations applied to the data (from OME-Zarr metadata).

omero Optional[Dict]

Metadata related to visualization and channels (from OME-Zarr).

affine = None class-attribute instance-attribute

axes = field(default=None, metadata={'description': 'Metadata about the axes'}) class-attribute instance-attribute

axes_order = 'ZYX' class-attribute instance-attribute

coordinate_transformations = field(default=None, metadata={'description': 'OME-Zarr coordinate transformations'}) class-attribute instance-attribute

darr instance-attribute

omero = field(default=None, metadata={'description': 'OME-Zarr Omero metadata'}) class-attribute instance-attribute

__get_upsampled_chunks(target_shape, return_scaling=True)

Calculates new chunk sizes for a dask array to match a target shape, while ensuring the chunks sum precisely to the target shape. Optionally, returns the scaling factors for each dimension.

This method is useful for upsampling data or ensuring 1:1 correspondence between downsampled and upsampled arrays.

Parameters:

Name Type Description Default
target_shape tuple

The desired shape of the array after upsampling.

required
return_scaling bool

Whether to return the scaling factors for each dimension (default: True).

True

Returns:

Name Type Description
tuple

new_chunks (tuple): A tuple of tuples specifying the new chunk sizes for each dimension. scaling (list): A list of scaling factors for each dimension (only if return_scaling=True).

OR

tuple

new_chunks (tuple): A tuple of tuples specifying the new chunk sizes for each dimension (if return_scaling=False).

Notes
  • The scaling factor for each dimension is calculated as: scaling_factor = target_shape[dim] / original_shape[dim]
  • The last chunk in each dimension is adjusted to account for rounding errors, ensuring the sum of chunks matches the target shape.
Example

Calculate upsampled chunks and scaling factors

new_chunks, scaling = znimg.__get_upsampled_chunks((256, 256, 256)) print("New chunks:", new_chunks) print("Scaling factors:", scaling)

Calculate only the new chunks

new_chunks = znimg.__get_upsampled_chunks((256, 256, 256), return_scaling=False)

Source code in zarrnii/core.py
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
def __get_upsampled_chunks(self, target_shape, return_scaling=True):
    """
    Calculates new chunk sizes for a dask array to match a target shape,
    while ensuring the chunks sum precisely to the target shape. Optionally,
    returns the scaling factors for each dimension.

    This method is useful for upsampling data or ensuring 1:1 correspondence
    between downsampled and upsampled arrays.

    Parameters:
        target_shape (tuple): The desired shape of the array after upsampling.
        return_scaling (bool, optional): Whether to return the scaling factors
                                         for each dimension (default: True).

    Returns:
        tuple:
            new_chunks (tuple): A tuple of tuples specifying the new chunk sizes
                                for each dimension.
            scaling (list): A list of scaling factors for each dimension
                            (only if `return_scaling=True`).

        OR

        tuple:
            new_chunks (tuple): A tuple of tuples specifying the new chunk sizes
                                for each dimension (if `return_scaling=False`).

    Notes:
        - The scaling factor for each dimension is calculated as:
          `scaling_factor = target_shape[dim] / original_shape[dim]`
        - The last chunk in each dimension is adjusted to account for rounding
          errors, ensuring the sum of chunks matches the target shape.

    Example:
        # Calculate upsampled chunks and scaling factors
        new_chunks, scaling = znimg.__get_upsampled_chunks((256, 256, 256))
        print("New chunks:", new_chunks)
        print("Scaling factors:", scaling)

        # Calculate only the new chunks
        new_chunks = znimg.__get_upsampled_chunks((256, 256, 256), return_scaling=False)
    """
    new_chunks = []
    scaling = []

    for dim, (orig_shape, orig_chunks, new_shape) in enumerate(
        zip(self.darr.shape, self.darr.chunks, target_shape)
    ):
        # Calculate the scaling factor for this dimension
        scaling_factor = new_shape / orig_shape

        # Scale each chunk size and round to get an initial estimate
        scaled_chunks = [
            int(round(chunk * scaling_factor)) for chunk in orig_chunks
        ]
        total = sum(scaled_chunks)

        # Adjust the chunks to ensure they sum up to the target shape exactly
        diff = new_shape - total
        if diff != 0:
            # Correct rounding errors by adjusting the last chunk size in the dimension
            scaled_chunks[-1] += diff

        new_chunks.append(tuple(scaled_chunks))
        scaling.append(scaling_factor)

    if return_scaling:
        return tuple(new_chunks), scaling
    else:
        return tuple(new_chunks)

align_affine_to_input_orientation(affine, orientation) staticmethod

Reorders and flips the affine matrix to align with the specified input orientation.

Parameters:

Name Type Description Default
affine ndarray

Initial affine matrix.

required
in_orientation str

Input orientation (e.g., 'RAS').

required

Returns:

Type Description

np.ndarray: Reordered and flipped affine matrix.

Source code in zarrnii/core.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
@staticmethod
def align_affine_to_input_orientation(affine, orientation):
    """
    Reorders and flips the affine matrix to align with the specified input orientation.

    Parameters:
        affine (np.ndarray): Initial affine matrix.
        in_orientation (str): Input orientation (e.g., 'RAS').

    Returns:
        np.ndarray: Reordered and flipped affine matrix.
    """
    axis_map = {"R": 0, "L": 0, "A": 1, "P": 1, "S": 2, "I": 2}
    sign_map = {"R": 1, "L": -1, "A": 1, "P": -1, "S": 1, "I": -1}

    input_axes = [axis_map[ax] for ax in orientation]
    input_signs = [sign_map[ax] for ax in orientation]

    reordered_affine = np.zeros_like(affine)
    for i, (axis, sign) in enumerate(zip(input_axes, input_signs)):
        reordered_affine[i, :3] = sign * affine[axis, :3]
        reordered_affine[i, 3] = sign * affine[i, 3]
    reordered_affine[3, :] = affine[3, :]  # Preserve homogeneous row

    return reordered_affine

apply_transform(*tfms, ref_znimg)

Apply a sequence of transformations to the current ZarrNii instance to align it with the reference ZarrNii instance (ref_znimg).

This is a lazy operation and doesn't perform computations until .compute() is called on the returned dask array.

Parameters:

Name Type Description Default
*tfms

Transformations to apply. Each transformation should be a Transform (or subclass) object.

()
ref_znimg ZarrNii

The reference ZarrNii instance to align with.

required

Returns:

Name Type Description
ZarrNii

A new ZarrNii instance with the transformations applied.

Notes
  • The transformations are applied in the following order:
  • The affine transformation of the reference image.
  • The transformations passed as *tfms.
  • The inverse affine transformation of the current image.
  • The data in the returned ZarrNii is lazily interpolated using dask.array.map_blocks.
Example

transformed_znimg = znimg.apply_transform( transform1, transform2, ref_znimg=ref_image )

Source code in zarrnii/core.py
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def apply_transform(self, *tfms, ref_znimg):
    """
    Apply a sequence of transformations to the current ZarrNii instance
    to align it with the reference ZarrNii instance (`ref_znimg`).

    This is a lazy operation and doesn't perform computations until
    `.compute()` is called on the returned dask array.

    Parameters:
        *tfms: Transformations to apply. Each transformation should be a
               Transform (or subclass) object.
        ref_znimg (ZarrNii): The reference ZarrNii instance to align with.

    Returns:
        ZarrNii: A new ZarrNii instance with the transformations applied.

    Notes:
        - The transformations are applied in the following order:
          1. The affine transformation of the reference image.
          2. The transformations passed as `*tfms`.
          3. The inverse affine transformation of the current image.
        - The data in the returned ZarrNii is lazily interpolated using
          `dask.array.map_blocks`.

    Example:
        transformed_znimg = znimg.apply_transform(
            transform1, transform2, ref_znimg=ref_image
        )
    """
    # Initialize the list of transformations to apply
    tfms_to_apply = [ref_znimg.affine]  # Start with the reference image affine

    # Append all transformations passed as arguments
    tfms_to_apply.extend(tfms)

    # Append the inverse of the current image's affine
    tfms_to_apply.append(self.affine.invert())

    # Create a new ZarrNii instance for the interpolated image
    interp_znimg = ref_znimg

    # Lazily apply the transformations using dask
    interp_znimg.darr = da.map_blocks(
        interp_by_block,  # Function to interpolate each block
        ref_znimg.darr,  # Reference image data
        dtype=np.float32,  # Output data type
        transforms=tfms_to_apply,  # Transformations to apply
        flo_znimg=self,  # Floating image to align
    )

    return interp_znimg

apply_transform_flo_to_ref_indices(*tfms, ref_znimg, indices)

Transforms indices from the floating image space to the reference image space by applying a sequence of transformations.

Parameters:

Name Type Description Default
*tfms

Transform objects to apply. These can be AffineTransform, DisplacementTransform, or other subclasses of Transform.

()
ref_znimg ZarrNii

The reference ZarrNii instance defining the target space.

required
indices ndarray

3xN array of indices in the floating image space.

required

Returns:

Type Description

np.ndarray: 3xN array of transformed indices in the reference image space.

Notes
  • Indices are treated as vectors in homogeneous coordinates, enabling transformation via matrix multiplication.
  • Transformations are applied in the following order:
  • The affine transformation of the floating image.
  • The transformations passed as *tfms.
  • The inverse affine transformation of the reference image.
Example

transformed_indices = flo_znimg.apply_transform_flo_to_ref_indices( transform1, transform2, ref_znimg=ref_image, indices=indices_in_flo )

Source code in zarrnii/core.py
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
def apply_transform_flo_to_ref_indices(self, *tfms, ref_znimg, indices):
    """
    Transforms indices from the floating image space to the reference image space
    by applying a sequence of transformations.

    Parameters:
        *tfms: Transform objects to apply. These can be `AffineTransform`,
               `DisplacementTransform`, or other subclasses of `Transform`.
        ref_znimg (ZarrNii): The reference ZarrNii instance defining the target space.
        indices (np.ndarray): 3xN array of indices in the floating image space.

    Returns:
        np.ndarray: 3xN array of transformed indices in the reference image space.

    Notes:
        - Indices are treated as vectors in homogeneous coordinates, enabling
          transformation via matrix multiplication.
        - Transformations are applied in the following order:
          1. The affine transformation of the floating image.
          2. The transformations passed as `*tfms`.
          3. The inverse affine transformation of the reference image.

    Example:
        transformed_indices = flo_znimg.apply_transform_flo_to_ref_indices(
            transform1, transform2, ref_znimg=ref_image, indices=indices_in_flo
        )
    """
    # Initialize the list of transformations to apply
    tfms_to_apply = [self.affine]  # Start with the floating image affine

    # Append all provided transformations
    tfms_to_apply.extend(tfms)

    # Append the inverse affine transformation of the reference image
    tfms_to_apply.append(ref_znimg.affine.invert())

    # Ensure indices are in homogeneous coordinates (4xN matrix)
    homog = np.ones((1, indices.shape[1]))
    xfm_vecs = np.vstack((indices, homog))

    # Sequentially apply transformations
    for tfm in tfms_to_apply:
        xfm_vecs = tfm.apply_transform(xfm_vecs)

    # Return the transformed indices in non-homogeneous coordinates
    return xfm_vecs[:3, :]

apply_transform_ref_to_flo_indices(*tfms, ref_znimg, indices)

Transforms indices from the reference image space to the floating image space by applying a sequence of transformations.

Parameters:

Name Type Description Default
*tfms

Transform objects to apply. These can be AffineTransform, DisplacementTransform, or other subclasses of Transform.

()
ref_znimg ZarrNii

The reference ZarrNii instance defining the source space.

required
indices ndarray

3xN array of indices in the reference space.

required

Returns:

Type Description

np.ndarray: 3xN array of transformed indices in the floating image space.

Notes
  • Indices are treated as vectors in homogeneous coordinates, enabling transformation via matrix multiplication.
  • Transformations are applied in the following order:
  • The affine transformation of the reference image.
  • The transformations passed as *tfms.
  • The inverse affine transformation of the floating image.
Example

transformed_indices = flo_znimg.apply_transform_ref_to_flo_indices( transform1, transform2, ref_znimg=ref_image, indices=indices_in_ref )

Source code in zarrnii/core.py
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
def apply_transform_ref_to_flo_indices(self, *tfms, ref_znimg, indices):
    """
    Transforms indices from the reference image space to the floating image space
    by applying a sequence of transformations.

    Parameters:
        *tfms: Transform objects to apply. These can be `AffineTransform`,
               `DisplacementTransform`, or other subclasses of `Transform`.
        ref_znimg (ZarrNii): The reference ZarrNii instance defining the source space.
        indices (np.ndarray): 3xN array of indices in the reference space.

    Returns:
        np.ndarray: 3xN array of transformed indices in the floating image space.

    Notes:
        - Indices are treated as vectors in homogeneous coordinates, enabling
          transformation via matrix multiplication.
        - Transformations are applied in the following order:
          1. The affine transformation of the reference image.
          2. The transformations passed as `*tfms`.
          3. The inverse affine transformation of the floating image.

    Example:
        transformed_indices = flo_znimg.apply_transform_ref_to_flo_indices(
            transform1, transform2, ref_znimg=ref_image, indices=indices_in_ref
        )
    """
    # Initialize the list of transformations to apply
    tfms_to_apply = [ref_znimg.affine]  # Start with the reference image affine

    # Append all provided transformations
    tfms_to_apply.extend(tfms)

    # Append the inverse affine transformation of the current image
    tfms_to_apply.append(self.affine.invert())

    # Ensure indices are in homogeneous coordinates (4xN matrix)
    homog = np.ones((1, indices.shape[1]))
    xfm_vecs = np.vstack((indices, homog))

    # Sequentially apply transformations
    for tfm in tfms_to_apply:
        xfm_vecs = tfm.apply_transform(xfm_vecs)

    # Return the transformed indices in non-homogeneous coordinates
    return xfm_vecs[:3, :]

construct_affine(coordinate_transformations, orientation) staticmethod

Constructs the affine matrix based on OME-Zarr coordinate transformations and adjusts it for the input orientation.

Parameters:

Name Type Description Default
coordinate_transformations list

Coordinate transformations from OME-Zarr metadata.

required
orientation str

Input orientation (e.g., 'RAS').

required

Returns:

Type Description

np.ndarray: A 4x4 affine matrix.

Source code in zarrnii/core.py
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
@staticmethod
def construct_affine(coordinate_transformations, orientation):
    """
    Constructs the affine matrix based on OME-Zarr coordinate transformations
    and adjusts it for the input orientation.

    Parameters:
        coordinate_transformations (list): Coordinate transformations from OME-Zarr metadata.
        orientation (str): Input orientation (e.g., 'RAS').

    Returns:
        np.ndarray: A 4x4 affine matrix.
    """
    # Initialize affine as an identity matrix
    affine = np.eye(4)

    # Parse scales and translations
    scales = [1.0, 1.0, 1.0]
    translations = [0.0, 0.0, 0.0]

    for transform in coordinate_transformations:
        if transform["type"] == "scale":
            scales = transform["scale"][1:]  # Ignore the channel/time dimension
        elif transform["type"] == "translation":
            translations = transform["translation"][
                1:
            ]  # Ignore the channel/time dimension

    # Populate the affine matrix
    affine[:3, :3] = np.diag(scales)  # Set scaling
    affine[:3, 3] = translations  # Set translation

    # Reorder the affine matrix for the input orientation
    return ZarrNii.align_affine_to_input_orientation(affine, orientation)

crop_with_bounding_box(bbox_min, bbox_max, ras_coords=False)

Crops the ZarrNii instance using a bounding box and returns a new cropped instance.

Parameters:

Name Type Description Default
bbox_min tuple

Minimum corner of the bounding box (Z, Y, X) in voxel coordinates. If ras_coords=True, this should be in RAS space.

required
bbox_max tuple

Maximum corner of the bounding box (Z, Y, X) in voxel coordinates. If ras_coords=True, this should be in RAS space.

required
ras_coords bool

Whether the bounding box coordinates are in RAS space. If True, they will be converted to voxel coordinates using the affine.

False

Returns:

Name Type Description
ZarrNii

A new ZarrNii instance representing the cropped subregion.

Notes
  • When ras_coords=True, the bounding box coordinates are transformed from RAS to voxel space using the inverse of the affine transformation.
  • The affine transformation is updated to reflect the cropped region, ensuring spatial consistency.
Example

Define a bounding box in voxel space

bbox_min = (10, 20, 30) bbox_max = (50, 60, 70)

Crop the ZarrNii instance

cropped_znimg = znimg.crop_with_bounding_box(bbox_min, bbox_max)

Define a bounding box in RAS space

ras_min = (-10, -20, -30) ras_max = (10, 20, 30)

Crop using RAS coordinates

cropped_znimg_ras = znimg.crop_with_bounding_box(ras_min, ras_max, ras_coords=True)

Source code in zarrnii/core.py
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
def crop_with_bounding_box(self, bbox_min, bbox_max, ras_coords=False):
    """
    Crops the ZarrNii instance using a bounding box and returns a new cropped instance.

    Parameters:
        bbox_min (tuple): Minimum corner of the bounding box (Z, Y, X) in voxel coordinates.
                         If `ras_coords=True`, this should be in RAS space.
        bbox_max (tuple): Maximum corner of the bounding box (Z, Y, X) in voxel coordinates.
                         If `ras_coords=True`, this should be in RAS space.
        ras_coords (bool): Whether the bounding box coordinates are in RAS space.
                           If True, they will be converted to voxel coordinates using the affine.

    Returns:
        ZarrNii: A new ZarrNii instance representing the cropped subregion.

    Notes:
        - When `ras_coords=True`, the bounding box coordinates are transformed from RAS to voxel space
          using the inverse of the affine transformation.
        - The affine transformation is updated to reflect the cropped region, ensuring spatial consistency.

    Example:
        # Define a bounding box in voxel space
        bbox_min = (10, 20, 30)
        bbox_max = (50, 60, 70)

        # Crop the ZarrNii instance
        cropped_znimg = znimg.crop_with_bounding_box(bbox_min, bbox_max)

        # Define a bounding box in RAS space
        ras_min = (-10, -20, -30)
        ras_max = (10, 20, 30)

        # Crop using RAS coordinates
        cropped_znimg_ras = znimg.crop_with_bounding_box(ras_min, ras_max, ras_coords=True)
    """
    # Convert RAS coordinates to voxel coordinates if needed
    if ras_coords:
        bbox_min = np.round(self.affine.invert() @ np.array(bbox_min)).astype(int)
        bbox_max = np.round(self.affine.invert() @ np.array(bbox_max)).astype(int)
        bbox_min = tuple(bbox_min[:3].flatten())
        bbox_max = tuple(bbox_max[:3].flatten())

    # Slice the dask array based on the bounding box
    darr_cropped = self.darr[
        :,
        bbox_min[0] : bbox_max[0],  # Z
        bbox_min[1] : bbox_max[1],  # Y
        bbox_min[2] : bbox_max[2],  # X
    ]

    # Update the affine to reflect the cropped region
    trans_vox = np.eye(4, 4)
    trans_vox[:3, 3] = bbox_min  # Translation for the cropped region
    new_affine = self.affine @ trans_vox

    # Create and return a new ZarrNii instance for the cropped region
    return ZarrNii.from_darr(
        darr_cropped, affine=new_affine, axes_order=self.axes_order
    )

divide_by_downsampled(znimg_ds)

Divides the current dask array by another dask array (znimg_ds), which is assumed to be a downsampled version of the current array.

This method upscales the downsampled array to match the resolution of the current array before performing element-wise division.

Parameters:

Name Type Description Default
znimg_ds ZarrNii

A ZarrNii instance representing the downsampled array.

required

Returns:

Name Type Description
ZarrNii

A new ZarrNii instance containing the result of the division.

Notes
  • The chunking of the current array is adjusted to ensure 1:1 correspondence with the chunks of the downsampled array after upscaling.
  • The division operation is performed block-wise using dask.array.map_blocks.
Example

znimg_divided = znimg.divide_by_downsampled(downsampled_znimg) print("Result shape:", znimg_divided.darr.shape)

Source code in zarrnii/core.py
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
def divide_by_downsampled(self, znimg_ds):
    """
    Divides the current dask array by another dask array (`znimg_ds`),
    which is assumed to be a downsampled version of the current array.

    This method upscales the downsampled array to match the resolution
    of the current array before performing element-wise division.

    Parameters:
        znimg_ds (ZarrNii): A ZarrNii instance representing the downsampled array.

    Returns:
        ZarrNii: A new ZarrNii instance containing the result of the division.

    Notes:
        - The chunking of the current array is adjusted to ensure 1:1 correspondence
          with the chunks of the downsampled array after upscaling.
        - The division operation is performed block-wise using `dask.array.map_blocks`.

    Example:
        znimg_divided = znimg.divide_by_downsampled(downsampled_znimg)
        print("Result shape:", znimg_divided.darr.shape)
    """
    # Calculate upsampled chunks for the downsampled array
    target_chunks = znimg_ds.__get_upsampled_chunks(
        self.darr.shape, return_scaling=False
    )

    # Rechunk the current high-resolution array to match the target chunks
    darr_rechunk = self.darr.rechunk(chunks=target_chunks)

    # Define the block-wise operation for zooming and division
    def block_zoom_and_divide_by(x1, x2, block_info=None):
        """
        Zooms x2 to match the size of x1 and performs element-wise division.

        Parameters:
            x1 (np.ndarray): High-resolution block from the current array.
            x2 (np.ndarray): Downsampled block from `znimg_ds`.
            block_info (dict, optional): Metadata about the current block.

        Returns:
            np.ndarray: The result of `x1 / zoom(x2, scaling)`.
        """
        # Calculate the scaling factors for zooming
        scaling = tuple(n1 / n2 for n1, n2 in zip(x1.shape, x2.shape))
        return x1 / zoom(x2, scaling, order=1, prefilter=False)

    # Perform block-wise division
    darr_div = da.map_blocks(
        block_zoom_and_divide_by,
        darr_rechunk,
        znimg_ds.darr,
        dtype=self.darr.dtype,
    )

    # Return the result as a new ZarrNii instance
    return ZarrNii(darr_div, self.affine, self.axes_order)

downsample(along_x=1, along_y=1, along_z=1, level=None, z_level_offset=-2)

Downsamples the ZarrNii instance by local mean reduction.

Parameters:

Name Type Description Default
along_x int

Downsampling factor along the X-axis (default: 1).

1
along_y int

Downsampling factor along the Y-axis (default: 1).

1
along_z int

Downsampling factor along the Z-axis (default: 1).

1
level int

If specified, calculates downsampling factors based on the level, with Z-axis adjusted by z_level_offset.

None
z_level_offset int

Offset for the Z-axis downsampling factor when using level (default: -2).

-2

Returns:

Name Type Description
ZarrNii

A new ZarrNii instance with the downsampled data and updated affine.

Notes
  • If level is provided, downsampling factors are calculated as:
    • along_x = along_y = 2**level
    • along_z = 2**max(level + z_level_offset, 0)
  • Updates the affine matrix to reflect the new voxel size after downsampling.
  • Uses dask.array.coarsen for efficient reduction along specified axes.
Example

Downsample by specific factors

downsampled_znimg = znimg.downsample(along_x=2, along_y=2, along_z=1)

Downsample using a pyramid level

downsampled_znimg = znimg.downsample(level=2)

Source code in zarrnii/core.py
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
def downsample(
    self, along_x=1, along_y=1, along_z=1, level=None, z_level_offset=-2
):
    """
    Downsamples the ZarrNii instance by local mean reduction.

    Parameters:
        along_x (int, optional): Downsampling factor along the X-axis (default: 1).
        along_y (int, optional): Downsampling factor along the Y-axis (default: 1).
        along_z (int, optional): Downsampling factor along the Z-axis (default: 1).
        level (int, optional): If specified, calculates downsampling factors based on the level,
                               with Z-axis adjusted by `z_level_offset`.
        z_level_offset (int, optional): Offset for the Z-axis downsampling factor when using `level`
                                        (default: -2).

    Returns:
        ZarrNii: A new ZarrNii instance with the downsampled data and updated affine.

    Notes:
        - If `level` is provided, downsampling factors are calculated as:
            - `along_x = along_y = 2**level`
            - `along_z = 2**max(level + z_level_offset, 0)`
        - Updates the affine matrix to reflect the new voxel size after downsampling.
        - Uses `dask.array.coarsen` for efficient reduction along specified axes.

    Example:
        # Downsample by specific factors
        downsampled_znimg = znimg.downsample(along_x=2, along_y=2, along_z=1)

        # Downsample using a pyramid level
        downsampled_znimg = znimg.downsample(level=2)
    """
    # Calculate downsampling factors if level is specified
    if level is not None:
        along_x = 2**level
        along_y = 2**level
        level_z = max(level + z_level_offset, 0)
        along_z = 2**level_z

    # Determine axes mapping based on axes_order
    if self.axes_order == "XYZ":
        axes = {0: 1, 1: along_x, 2: along_y, 3: along_z}  # (C, X, Y, Z)
    else:
        axes = {0: 1, 1: along_z, 2: along_y, 3: along_x}  # (C, Z, Y, X)

    # Perform local mean reduction using coarsen
    agg_func = np.mean
    darr_scaled = da.coarsen(agg_func, x=self.darr, axes=axes, trim_excess=True)

    # Update the affine matrix to reflect downsampling
    scaling_matrix = np.diag((along_x, along_y, along_z, 1))
    new_affine = AffineTransform.from_array(scaling_matrix @ self.affine.matrix)

    # Create and return a new ZarrNii instance
    return ZarrNii.from_darr(
        darr_scaled, affine=new_affine, axes_order=self.axes_order
    )

from_array(array, affine=None, chunks='auto', orientation='RAS', axes_order='XYZ', axes=None, coordinate_transformations=None, omero=None, spacing=(1, 1, 1), origin=(0, 0, 0)) classmethod

Creates a ZarrNii instance from an existing numpy array.

Parameters:

Name Type Description Default
array ndarray

Input numpy array.

required
affine AffineTransform or ndarray

Affine transform to associate with the array. If None, an affine will be created based on the orientation, spacing, and origin.

None
orientation str

Orientation string used to generate an affine matrix (default: "RAS").

'RAS'
chunks str or tuple

Chunk size for dask array (default: "auto").

'auto'
axes_order str

The axes order of the input array (default: "ZYX").

'XYZ'
axes list

Axes metadata for OME-Zarr. If None, default axes are generated.

None
coordinate_transformations list

Coordinate transformations for OME-Zarr metadata.

None
omero dict

Omero metadata for OME-Zarr.

None
spacing tuple

Voxel spacing along each axis (default: (1, 1, 1)).

(1, 1, 1)
origin tuple

Origin point in physical space (default: (0, 0, 0)).

(0, 0, 0)

Returns:

Name Type Description
ZarrNii

A populated ZarrNii instance.

Source code in zarrnii/core.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
@classmethod
def from_array(
    cls,
    array,
    affine=None,
    chunks="auto",
    orientation="RAS",
    axes_order="XYZ",
    axes=None,
    coordinate_transformations=None,
    omero=None,
    spacing=(1, 1, 1),
    origin=(0, 0, 0),
):
    """
    Creates a ZarrNii instance from an existing numpy array.

    Parameters:
        array (np.ndarray): Input numpy array.
        affine (AffineTransform or np.ndarray, optional): Affine transform to associate with the array.
            If None, an affine will be created based on the orientation, spacing, and origin.
        orientation (str, optional): Orientation string used to generate an affine matrix (default: "RAS").
        chunks (str or tuple): Chunk size for dask array (default: "auto").
        axes_order (str): The axes order of the input array (default: "ZYX").
        axes (list, optional): Axes metadata for OME-Zarr. If None, default axes are generated.
        coordinate_transformations (list, optional): Coordinate transformations for OME-Zarr metadata.
        omero (dict, optional): Omero metadata for OME-Zarr.
        spacing (tuple, optional): Voxel spacing along each axis (default: (1, 1, 1)).
        origin (tuple, optional): Origin point in physical space (default: (0, 0, 0)).


    Returns:
        ZarrNii: A populated ZarrNii instance.

    """

    return cls.from_darr(
        da.from_array(array, chunks=chunks),
        affine=affine,
        orientation=orientation,
        axes_order=axes_order,
        axes=axes,
        coordinate_transformations=coordinate_transformations,
        omero=omero,
        spacing=spacing,
        origin=origin,
    )

from_darr(darr, affine=None, orientation='RAS', axes_order='XYZ', axes=None, coordinate_transformations=None, omero=None, spacing=(1, 1, 1), origin=(0, 0, 0)) classmethod

Creates a ZarrNii instance from an existing Dask array.

Parameters:

Name Type Description Default
darr Array

Input Dask array.

required
affine AffineTransform or ndarray

Affine transform to associate with the array. If None, an affine will be created based on the orientation, spacing, and origin.

None
orientation str

Orientation string used to generate an affine matrix (default: "RAS").

'RAS'
axes_order str

The axes order of the input array (default: "XYZ").

'XYZ'
axes list

Axes metadata for OME-Zarr. If None, default axes are generated.

None
coordinate_transformations list

Coordinate transformations for OME-Zarr metadata.

None
omero dict

Omero metadata for OME-Zarr.

None
spacing tuple

Voxel spacing along each axis (default: (1, 1, 1)).

(1, 1, 1)
origin tuple

Origin point in physical space (default: (0, 0, 0)).

(0, 0, 0)

Returns:

Name Type Description
ZarrNii

A populated ZarrNii instance.

Source code in zarrnii/core.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@classmethod
def from_darr(
    cls,
    darr,
    affine=None,
    orientation="RAS",
    axes_order="XYZ",
    axes=None,
    coordinate_transformations=None,
    omero=None,
    spacing=(1, 1, 1),
    origin=(0, 0, 0),
):
    """
    Creates a ZarrNii instance from an existing Dask array.

    Parameters:
        darr (da.Array): Input Dask array.
        affine (AffineTransform or np.ndarray, optional): Affine transform to associate with the array.
            If None, an affine will be created based on the orientation, spacing, and origin.
        orientation (str, optional): Orientation string used to generate an affine matrix (default: "RAS").
        axes_order (str): The axes order of the input array (default: "XYZ").
        axes (list, optional): Axes metadata for OME-Zarr. If None, default axes are generated.
        coordinate_transformations (list, optional): Coordinate transformations for OME-Zarr metadata.
        omero (dict, optional): Omero metadata for OME-Zarr.
        spacing (tuple, optional): Voxel spacing along each axis (default: (1, 1, 1)).
        origin (tuple, optional): Origin point in physical space (default: (0, 0, 0)).

    Returns:
        ZarrNii: A populated ZarrNii instance.
    """

    # Generate affine from orientation if not explicitly provided
    if affine is None:
        affine = orientation_to_affine(orientation, spacing, origin)

    # Generate default axes if none are provided
    if axes is None:
        axes = [{"name": "c", "type": "channel", "unit": None}] + [
            {"name": ax, "type": "space", "unit": "micrometer"} for ax in axes_order
        ]

    # Generate default coordinate transformations if none are provided
    if coordinate_transformations is None:
        # Derive scale and translation from the affine
        scale = np.sqrt((affine[:3, :3] ** 2).sum(axis=0))  # Diagonal scales
        translation = affine[:3, 3]  # Translation vector
        coordinate_transformations = [
            {"type": "scale", "scale": [1] + scale.tolist()},  # Add channel scale
            {
                "type": "translation",
                "translation": [0] + translation.tolist(),
            },  # Add channel translation
        ]

    # Generate default omero metadata if none is provided
    if omero is None:
        omero = {
            "channels": [{"label": f"Channel-{i}"} for i in range(darr.shape[0])],
            "rdefs": {"model": "color"},
        }

    # Create and return the ZarrNii instance
    return cls(
        darr,
        affine=AffineTransform.from_array(affine),
        axes_order=axes_order,
        axes=axes,
        coordinate_transformations=coordinate_transformations,
        omero=omero,
    )

from_nifti(path, chunks='auto', as_ref=False, zooms=None) classmethod

Creates a ZarrNii instance from a NIfTI file. Populates OME-Zarr metadata based on the NIfTI affine matrix.

Parameters:

Name Type Description Default
path str

Path to the NIfTI file.

required
chunks str or tuple

Chunk size for dask array (default: "auto").

'auto'
as_ref bool

If True, creates an empty dask array with the correct shape instead of loading data.

False
zooms list or ndarray

Target voxel spacing in xyz (only valid if as_ref=True).

None

Returns:

Name Type Description
ZarrNii

A populated ZarrNii instance.

Raises:

Type Description
ValueError

If zooms is specified when as_ref=False.

Source code in zarrnii/core.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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
@classmethod
def from_nifti(cls, path, chunks="auto", as_ref=False, zooms=None):
    """
    Creates a ZarrNii instance from a NIfTI file. Populates OME-Zarr metadata
    based on the NIfTI affine matrix.

    Parameters:
        path (str): Path to the NIfTI file.
        chunks (str or tuple): Chunk size for dask array (default: "auto").
        as_ref (bool): If True, creates an empty dask array with the correct shape instead of loading data.
        zooms (list or np.ndarray): Target voxel spacing in xyz (only valid if as_ref=True).

    Returns:
        ZarrNii: A populated ZarrNii instance.

    Raises:
        ValueError: If `zooms` is specified when `as_ref=False`.
    """
    if not as_ref and zooms is not None:
        raise ValueError("`zooms` can only be used when `as_ref=True`.")

    # Load the NIfTI file and extract metadata
    nii = nib.load(path)
    shape = nii.header.get_data_shape()
    affine = nii.affine

    # Adjust shape and affine if zooms are provided
    if zooms is not None:
        in_zooms = np.sqrt(
            (affine[:3, :3] ** 2).sum(axis=0)
        )  # Current voxel spacing
        scaling_factor = in_zooms / zooms
        new_shape = [
            int(np.floor(shape[0] * scaling_factor[2])),  # Z
            int(np.floor(shape[1] * scaling_factor[1])),  # Y
            int(np.floor(shape[2] * scaling_factor[0])),  # X
        ]
        np.fill_diagonal(affine[:3, :3], zooms)
    else:
        new_shape = shape

    if as_ref:
        # Create an empty dask array with the adjusted shape
        darr = da.empty((1, *new_shape), chunks=chunks, dtype="float32")
    else:
        # Load the NIfTI data and convert to a dask array
        data = np.expand_dims(nii.get_fdata(), axis=0)  # Add a channel dimension
        darr = da.from_array(data, chunks=chunks)

    # Define axes order and metadata
    axes_order = "XYZ"
    axes = [
        {"name": "channel", "type": "channel", "unit": None},
        {"name": "x", "type": "space", "unit": "millimeter"},
        {"name": "y", "type": "space", "unit": "millimeter"},
        {"name": "z", "type": "space", "unit": "millimeter"},
    ]

    # Extract coordinate transformations from the affine matrix
    scale = np.sqrt((affine[:3, :3] ** 2).sum(axis=0))  # Diagonal scales
    translation = affine[:3, 3]  # Translation vector
    coordinate_transformations = [
        {"type": "scale", "scale": [1] + scale.tolist()},  # Add channel scale
        {
            "type": "translation",
            "translation": [0] + translation.tolist(),
        },  # Add channel translation
    ]

    # Define basic Omero metadata
    omero = {
        "channels": [{"label": "Channel-0"}],  # Placeholder channel information
        "rdefs": {"model": "color"},
    }

    # Create and return the ZarrNii instance
    return cls(
        darr,
        affine=AffineTransform.from_array(affine),
        axes_order=axes_order,
        axes=axes,
        coordinate_transformations=coordinate_transformations,
        omero=omero,
    )

from_ome_zarr(path, level=0, channels=[0], chunks='auto', z_level_offset=-2, rechunk=False, storage_options=None, orientation='IPL', as_ref=False, zooms=None) classmethod

Reads in an OME-Zarr file as a ZarrNii image, optionally as a reference.

Parameters:

Name Type Description Default
path str

Path to the OME-Zarr file.

required
level int

Pyramid level to load (default: 0).

0
channels list

Channels to load (default: [0]).

[0]
chunks str or tuple

Chunk size for dask array (default: "auto").

'auto'
z_level_offset int

Offset for Z downsampling level (default: -2).

-2
rechunk bool

Whether to rechunk the data (default: False).

False
storage_options dict

Storage options for Zarr.

None
orientation str

Default input orientation if none is specified in metadata (default: 'IPL').

'IPL'
as_ref bool

If True, creates an empty dask array with the correct shape instead of loading data.

False
zooms list or ndarray

Target voxel spacing in xyz (only valid if as_ref=True).

None

Returns:

Name Type Description
ZarrNii

A populated ZarrNii instance.

Raises:

Type Description
ValueError

If zooms is specified when as_ref=False.

Source code in zarrnii/core.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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
328
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
365
366
367
@classmethod
def from_ome_zarr(
    cls,
    path,
    level=0,
    channels=[0],
    chunks="auto",
    z_level_offset=-2,
    rechunk=False,
    storage_options=None,
    orientation="IPL",
    as_ref=False,
    zooms=None,
):
    """
    Reads in an OME-Zarr file as a ZarrNii image, optionally as a reference.

    Parameters:
        path (str): Path to the OME-Zarr file.
        level (int): Pyramid level to load (default: 0).
        channels (list): Channels to load (default: [0]).
        chunks (str or tuple): Chunk size for dask array (default: "auto").
        z_level_offset (int): Offset for Z downsampling level (default: -2).
        rechunk (bool): Whether to rechunk the data (default: False).
        storage_options (dict): Storage options for Zarr.
        orientation (str): Default input orientation if none is specified in metadata (default: 'IPL').
        as_ref (bool): If True, creates an empty dask array with the correct shape instead of loading data.
        zooms (list or np.ndarray): Target voxel spacing in xyz (only valid if as_ref=True).

    Returns:
        ZarrNii: A populated ZarrNii instance.

    Raises:
        ValueError: If `zooms` is specified when `as_ref=False`.
    """

    if not as_ref and zooms is not None:
        raise ValueError("`zooms` can only be used when `as_ref=True`.")

    # Determine the level and whether downsampling is required
    if not as_ref:
        (
            level,
            do_downsample,
            downsampling_kwargs,
        ) = cls.get_level_and_downsampling_kwargs(
            path, level, z_level_offset, storage_options=storage_options
        )
    else:
        do_downsample = False

    # Open the Zarr metadata
    store = zarr.open(path, mode="r")
    multiscales = store.attrs.get("multiscales", [{}])
    datasets = multiscales[0].get("datasets", [{}])
    coordinate_transformations = datasets[level].get(
        "coordinateTransformations", []
    )
    axes = multiscales[0].get("axes", [])
    omero = store.attrs.get("omero", {})

    # Read orientation metadata (default to `orientation` if not present)
    orientation = store.attrs.get("orientation", orientation)

    # Load data or metadata as needed
    darr_base = da.from_zarr(
        path, component=f"/{level}", storage_options=storage_options
    )[channels, :, :, :]
    shape = darr_base.shape

    affine = cls.construct_affine(coordinate_transformations, orientation)

    if zooms is not None:
        # Handle zoom adjustments
        in_zooms = np.sqrt(
            (affine[:3, :3] ** 2).sum(axis=0)
        )  # Current voxel spacing
        scaling_factor = in_zooms / zooms
        new_shape = [
            shape[0],
            int(np.floor(shape[1] * scaling_factor[2])),  # Z
            int(np.floor(shape[2] * scaling_factor[1])),  # Y
            int(np.floor(shape[3] * scaling_factor[0])),  # X
        ]
        np.fill_diagonal(affine[:3, :3], zooms)
    else:
        new_shape = shape

    # we want to downsample *before* we rechunk

    if as_ref:
        # Create an empty array with the updated shape
        darr = da.empty(new_shape, chunks=chunks, dtype=darr_base.dtype)
    else:
        darr = darr_base

    znimg = cls(
        darr,
        affine=AffineTransform.from_array(affine),
        axes_order="ZYX",
        axes=axes,
        coordinate_transformations=coordinate_transformations,
        omero=omero,
    )

    if do_downsample:
        znimg = znimg.downsample(**downsampling_kwargs)

    if rechunk:
        znimg.darr = znimg.darr.rechunk(chunks)

    return znimg

get_bounded_subregion(points)

Extracts a bounded subregion of the dask array containing the specified points, along with the grid points for interpolation.

If the points extend beyond the domain of the dask array, the extent is capped at the boundaries. If all points are outside the domain, the function returns (None, None).

Parameters:

Name Type Description Default
points ndarray

Nx3 or Nx4 array of coordinates in the array's space. If Nx4, the last column is assumed to be the homogeneous coordinate and is ignored.

required

Returns:

Name Type Description
tuple

grid_points (tuple): A tuple of three 1D arrays representing the grid points along each axis (X, Y, Z) in the subregion. subvol (np.ndarray or None): The extracted subregion as a NumPy array. Returns None if all points are outside the array domain.

Notes
  • The function uses compute() on the dask array to immediately load the subregion, as Dask doesn't support the type of indexing required for interpolation.
  • A padding of 1 voxel is applied around the extent of the points.
Example

grid_points, subvol = znimg.get_bounded_subregion(points) if subvol is not None: print("Subvolume shape:", subvol.shape)

Source code in zarrnii/core.py
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
def get_bounded_subregion(self, points: np.ndarray):
    """
    Extracts a bounded subregion of the dask array containing the specified points,
    along with the grid points for interpolation.

    If the points extend beyond the domain of the dask array, the extent is capped
    at the boundaries. If all points are outside the domain, the function returns
    `(None, None)`.

    Parameters:
        points (np.ndarray): Nx3 or Nx4 array of coordinates in the array's space.
                             If Nx4, the last column is assumed to be the homogeneous
                             coordinate and is ignored.

    Returns:
        tuple:
            grid_points (tuple): A tuple of three 1D arrays representing the grid
                                 points along each axis (X, Y, Z) in the subregion.
            subvol (np.ndarray or None): The extracted subregion as a NumPy array.
                                         Returns `None` if all points are outside
                                         the array domain.

    Notes:
        - The function uses `compute()` on the dask array to immediately load the
          subregion, as Dask doesn't support the type of indexing required for
          interpolation.
        - A padding of 1 voxel is applied around the extent of the points.

    Example:
        grid_points, subvol = znimg.get_bounded_subregion(points)
        if subvol is not None:
            print("Subvolume shape:", subvol.shape)
    """
    pad = 1  # Padding around the extent of the points

    # Compute the extent of the points in the array's coordinate space
    min_extent = np.floor(points.min(axis=1)[:3] - pad).astype("int")
    max_extent = np.ceil(points.max(axis=1)[:3] + pad).astype("int")

    # Clip the extents to ensure they stay within the bounds of the array
    clip_min = np.zeros_like(min_extent)
    clip_max = np.array(self.darr.shape[-3:])  # Z, Y, X dimensions

    min_extent = np.clip(min_extent, clip_min, clip_max)
    max_extent = np.clip(max_extent, clip_min, clip_max)

    # Check if all points are outside the domain
    if np.any(max_extent <= min_extent):
        return None, None

    # Extract the subvolume using the computed extents
    subvol = self.darr[
        :,
        min_extent[0] : max_extent[0],
        min_extent[1] : max_extent[1],
        min_extent[2] : max_extent[2],
    ].compute()

    # Generate grid points for interpolation
    grid_points = (
        np.arange(min_extent[0], max_extent[0]),  # Z
        np.arange(min_extent[1], max_extent[1]),  # Y
        np.arange(min_extent[2], max_extent[2]),  # X
    )

    return grid_points, subvol

get_bounding_box_around_label(label_number, padding=0, ras_coords=False)

Calculates the bounding box around a given label in the ZarrNii instance.

Parameters:

Name Type Description Default
label_number int

The label value for which the bounding box is computed.

required
padding int

Extra padding added around the bounding box in voxel units (default: 0).

0
ras_coords bool

If True, returns the bounding box coordinates in RAS space. Otherwise, returns voxel coordinates (default: False).

False

Returns:

Name Type Description
tuple

bbox_min (np.ndarray): Minimum corner of the bounding box (Z, Y, X) as a (3, 1) array. bbox_max (np.ndarray): Maximum corner of the bounding box (Z, Y, X) as a (3, 1) array.

Notes
  • The function uses da.argwhere to locate the indices of the specified label lazily.
  • Padding is added symmetrically around the bounding box, and the result is clipped to ensure it remains within the array bounds.
  • If ras_coords=True, the bounding box coordinates are transformed to RAS space using the affine transformation.
Example

Compute bounding box for label 1 with 5-voxel padding

bbox_min, bbox_max = znimg.get_bounding_box_around_label(1, padding=5)

Get the bounding box in RAS space

bbox_min_ras, bbox_max_ras = znimg.get_bounding_box_around_label(1, padding=5, ras_coords=True)

Source code in zarrnii/core.py
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
def get_bounding_box_around_label(self, label_number, padding=0, ras_coords=False):
    """
    Calculates the bounding box around a given label in the ZarrNii instance.

    Parameters:
        label_number (int): The label value for which the bounding box is computed.
        padding (int, optional): Extra padding added around the bounding box in voxel units (default: 0).
        ras_coords (bool, optional): If True, returns the bounding box coordinates in RAS space.
                                     Otherwise, returns voxel coordinates (default: False).

    Returns:
        tuple:
            bbox_min (np.ndarray): Minimum corner of the bounding box (Z, Y, X) as a (3, 1) array.
            bbox_max (np.ndarray): Maximum corner of the bounding box (Z, Y, X) as a (3, 1) array.

    Notes:
        - The function uses `da.argwhere` to locate the indices of the specified label lazily.
        - Padding is added symmetrically around the bounding box, and the result is clipped to
          ensure it remains within the array bounds.
        - If `ras_coords=True`, the bounding box coordinates are transformed to RAS space using
          the affine transformation.

    Example:
        # Compute bounding box for label 1 with 5-voxel padding
        bbox_min, bbox_max = znimg.get_bounding_box_around_label(1, padding=5)

        # Get the bounding box in RAS space
        bbox_min_ras, bbox_max_ras = znimg.get_bounding_box_around_label(1, padding=5, ras_coords=True)
    """
    # Locate the indices of the specified label
    indices = da.argwhere(self.darr == label_number).compute()

    if indices.size == 0:
        raise ValueError(f"Label {label_number} not found in the array.")

    # Compute the minimum and maximum extents in each dimension
    bbox_min = (
        indices.min(axis=0).reshape((4, 1))[1:] - padding
    )  # Exclude channel axis
    bbox_max = indices.max(axis=0).reshape((4, 1))[1:] + 1 + padding

    # Clip the extents to ensure they stay within bounds
    bbox_min = np.clip(bbox_min, 0, np.array(self.darr.shape[1:]).reshape(3, 1))
    bbox_max = np.clip(bbox_max, 0, np.array(self.darr.shape[1:]).reshape(3, 1))

    # Convert to RAS coordinates if requested
    if ras_coords:
        bbox_min = self.affine @ bbox_min
        bbox_max = self.affine @ bbox_max

    return bbox_min, bbox_max

get_level_and_downsampling_kwargs(ome_zarr_path, level, z_level_offset=-2, storage_options=None) staticmethod

Determines the appropriate pyramid level and additional downsampling factors for an OME-Zarr dataset.

Parameters:

Name Type Description Default
ome_zarr_path str or MutableMapping

Path to the OME-Zarr dataset or a MutableMapping store.

required
level int

Desired downsampling level.

required
z_level_offset int

Offset to adjust the Z-axis downsampling level (default: -2).

-2
storage_options dict

Storage options for accessing remote or custom storage.

None

Returns:

Name Type Description
tuple
  • level (int): The selected pyramid level (capped by the maximum level).
  • do_downsample (bool): Whether additional downsampling is required.
  • downsampling_kwargs (dict): Factors for downsampling along X, Y, and Z.
Notes
  • If the requested level exceeds the available pyramid levels, the function calculates additional downsampling factors (level_xy, level_z) for XY and Z axes.
Source code in zarrnii/core.py
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
@staticmethod
def get_level_and_downsampling_kwargs(
    ome_zarr_path, level, z_level_offset=-2, storage_options=None
):
    """
    Determines the appropriate pyramid level and additional downsampling factors for an OME-Zarr dataset.

    Parameters:
        ome_zarr_path (str or MutableMapping): Path to the OME-Zarr dataset or a `MutableMapping` store.
        level (int): Desired downsampling level.
        z_level_offset (int, optional): Offset to adjust the Z-axis downsampling level (default: -2).
        storage_options (dict, optional): Storage options for accessing remote or custom storage.

    Returns:
        tuple:
            - level (int): The selected pyramid level (capped by the maximum level).
            - do_downsample (bool): Whether additional downsampling is required.
            - downsampling_kwargs (dict): Factors for downsampling along X, Y, and Z.

    Notes:
        - If the requested level exceeds the available pyramid levels, the function calculates
          additional downsampling factors (`level_xy`, `level_z`) for XY and Z axes.
    """
    max_level = ZarrNii.get_max_level(
        ome_zarr_path, storage_options=storage_options
    )

    # Determine the pyramid level and additional downsampling factors
    if level > max_level:  # Requested level exceeds pyramid levels
        level_xy = level - max_level
        level_z = max(level + z_level_offset, 0)
        level = max_level
    else:
        level_xy = 0
        level_z = max(level + z_level_offset, 0)

    # print(
    #    f"level: {level}, level_xy: {level_xy}, level_z: {level_z}, max_level: {max_level}"
    # )

    # Determine if additional downsampling is needed
    do_downsample = level_xy > 0 or level_z > 0

    # Return the level, downsampling flag, and downsampling parameters
    return (
        level,
        do_downsample,
        {
            "along_x": 2**level_xy,
            "along_y": 2**level_xy,
            "along_z": 2**level_z,
        },
    )

get_max_level(path, storage_options=None) staticmethod

Retrieves the maximum level of multiscale downsampling in an OME-Zarr dataset.

Parameters:

Name Type Description Default
path str or MutableMapping

Path to the OME-Zarr dataset or a MutableMapping store.

required
storage_options dict

Storage options for accessing remote or custom storage.

None

Returns:

Name Type Description
int

The maximum level of multiscale downsampling (zero-based index).

None

If no multiscale levels are found.

Notes
  • The function assumes that the Zarr dataset follows the OME-Zarr specification with multiscale metadata.
Source code in zarrnii/core.py
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
@staticmethod
def get_max_level(path, storage_options=None):
    """
    Retrieves the maximum level of multiscale downsampling in an OME-Zarr dataset.

    Parameters:
        path (str or MutableMapping): Path to the OME-Zarr dataset or a `MutableMapping` store.
        storage_options (dict, optional): Storage options for accessing remote or custom storage.

    Returns:
        int: The maximum level of multiscale downsampling (zero-based index).
        None: If no multiscale levels are found.

    Notes:
        - The function assumes that the Zarr dataset follows the OME-Zarr specification
          with multiscale metadata.
    """
    # Determine the store type
    if isinstance(path, MutableMapping):
        store = path
    else:
        store = fsspec.get_mapper(path, storage_options=storage_options)

    # Open the Zarr group
    group = zarr.open(store, mode="r")

    # Access the multiscale metadata
    multiscales = group.attrs.get("multiscales", [])

    # Determine the maximum level
    if multiscales:
        max_level = len(multiscales[0]["datasets"]) - 1
        return max_level
    else:
        print("No multiscale levels found.")
        return None

get_orientation()

Get the anatomical orientation of the dataset based on its affine transformation.

This function determines the orientation string (e.g., 'RAS', 'LPI') of the dataset by analyzing the affine transformation matrix.

Returns:

Name Type Description
str

The orientation string corresponding to the dataset's affine transformation.

Source code in zarrnii/core.py
463
464
465
466
467
468
469
470
471
472
473
def get_orientation(self):
    """
    Get the anatomical orientation of the dataset based on its affine transformation.

    This function determines the orientation string (e.g., 'RAS', 'LPI') of the dataset
    by analyzing the affine transformation matrix.

    Returns:
        str: The orientation string corresponding to the dataset's affine transformation.
    """
    return affine_to_orientation(self.affine)

get_origin(axes_order='ZYX')

Get the origin (translation) from the affine matrix, with optional reordering based on axis order.

The origin is represented as the translation component in the affine matrix (the last column). If the affine matrix's axis order is different from the provided axes_order, the matrix will be reordered accordingly before extracting the origin.

axes_order : str, optional The desired order of axes (e.g., 'ZYX', 'XYZ'). The default is 'ZYX'. If the current affine matrix has a different axis order, it will be reordered to match this.

ndarray A 3-element array representing the translation (origin) in world coordinates.

Notes: If the affine's axis order is already the same as axes_order, no reordering will be performed.

Source code in zarrnii/core.py
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
def get_origin(self, axes_order="ZYX"):
    """
    Get the origin (translation) from the affine matrix, with optional reordering based on axis order.

    The origin is represented as the translation component in the affine matrix (the last column).
    If the affine matrix's axis order is different from the provided `axes_order`, the matrix will
    be reordered accordingly before extracting the origin.

    Parameters:
    axes_order : str, optional
        The desired order of axes (e.g., 'ZYX', 'XYZ'). The default is 'ZYX'. If the current affine
        matrix has a different axis order, it will be reordered to match this.

    Returns:
    ndarray
        A 3-element array representing the translation (origin) in world coordinates.

    Notes:
    If the affine's axis order is already the same as `axes_order`, no reordering will be performed.
    """
    if axes_order == self.axes_order:
        affine = self.affine
    else:
        affine = self.reorder_affine_xyz_zyx(self.affine)

    return affine[:3, 3]

get_zooms(axes_order='ZYX')

Get the voxel spacing (zoom factors) from the affine matrix, with optional reordering based on axis order.

The zoom factors are derived from the diagonal elements of the upper-left 3x3 part of the affine matrix, representing the voxel spacing along each axis. If the affine matrix's axis order is different from the provided axes_order, the matrix will be reordered accordingly before extracting the zoom factors.

axes_order : str, optional The desired order of axes (e.g., 'ZYX', 'XYZ'). The default is 'ZYX'. If the current affine matrix has a different axis order, it will be reordered to match this.

ndarray A 3-element array representing the voxel spacing in each dimension (x, y, z).

Notes: If the affine's axis order is already the same as axes_order, no reordering will be performed.

Source code in zarrnii/core.py
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
def get_zooms(self, axes_order="ZYX"):
    """
    Get the voxel spacing (zoom factors) from the affine matrix, with optional reordering based on axis order.

    The zoom factors are derived from the diagonal elements of the upper-left 3x3 part of the affine
    matrix, representing the voxel spacing along each axis. If the affine matrix's axis order is
    different from the provided `axes_order`, the matrix will be reordered accordingly before extracting
    the zoom factors.

    Parameters:
    axes_order : str, optional
        The desired order of axes (e.g., 'ZYX', 'XYZ'). The default is 'ZYX'. If the current affine
        matrix has a different axis order, it will be reordered to match this.

    Returns:
    ndarray
        A 3-element array representing the voxel spacing in each dimension (x, y, z).

    Notes:
    If the affine's axis order is already the same as `axes_order`, no reordering will be performed.
    """
    if axes_order == self.axes_order:
        affine = self.affine
    else:
        affine = self.reorder_affine_xyz_zyx(self.affine)

    return np.sqrt((affine[:3, :3] ** 2).sum(axis=0))  # Extract scales

reorder_affine_xyz_zyx(affine) staticmethod

Reorders the affine matrix from ZYX to XYZ axes order and adjusts the translation.

Parameters:

Name Type Description Default
affine ndarray

Affine matrix in ZYX order.

required

Returns:

Type Description

np.ndarray: Affine matrix reordered to XYZ order.

Source code in zarrnii/core.py
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
@staticmethod
def reorder_affine_xyz_zyx(affine):
    """
    Reorders the affine matrix from ZYX to XYZ axes order and adjusts the translation.

    Parameters:
        affine (np.ndarray): Affine matrix in ZYX order.

    Returns:
        np.ndarray: Affine matrix reordered to XYZ order.
    """
    # Reordering matrix to go from ZYX to XYZ
    reorder_xfm = np.array(
        [
            [0, 0, 1, 0],  # Z -> X
            [0, 1, 0, 0],  # Y -> Y
            [1, 0, 0, 0],  # X -> Z
            [0, 0, 0, 1],  # Homogeneous row
        ]
    )

    # Apply reordering to the affine matrix
    affine_reordered = affine @ reorder_xfm

    # Adjust translation (last column)
    translation_zyx = affine[:3, 3]
    reorder_perm = [2, 1, 0]  # Map ZYX -> XYZ
    translation_xyz = translation_zyx[reorder_perm]

    # Update reordered affine with adjusted translation
    affine_reordered[:3, 3] = translation_xyz
    return affine_reordered

to_nifti(filename=None)

Convert the current ZarrNii instance to a NIfTI-1 image (Nifti1Image) and optionally save it to a file.

Parameters:

Name Type Description Default
filename str

Output path for the NIfTI file. If None, the function returns the NIfTI object.

None

Returns:

Type Description

nib.Nifti1Image: The NIfTI-1 image representation of the ZarrNii instance if filename is not provided.

Notes
  • Reorders data to XYZ order if the current axes_order is ZYX.
  • Adjusts the affine matrix accordingly to match the reordered data.
Source code in zarrnii/core.py
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
def to_nifti(self, filename=None):
    """
    Convert the current ZarrNii instance to a NIfTI-1 image (Nifti1Image)
    and optionally save it to a file.

    Parameters:
        filename (str, optional): Output path for the NIfTI file. If None,
                                  the function returns the NIfTI object.

    Returns:
        nib.Nifti1Image: The NIfTI-1 image representation of the ZarrNii instance
                         if `filename` is not provided.

    Notes:
        - Reorders data to XYZ order if the current `axes_order` is ZYX.
        - Adjusts the affine matrix accordingly to match the reordered data.
    """

    # Reorder data to match NIfTI's expected XYZ order if necessary
    if self.axes_order == "ZYX":
        data = da.moveaxis(
            self.darr, (0, 1, 2, 3), (0, 3, 2, 1)
        ).compute()  # Reorder to XYZ
        affine = self.reorder_affine_xyz_zyx(
            self.affine.matrix
        )  # Reorder affine to match
    else:
        data = self.darr.compute()
        affine = self.affine.matrix  # No reordering needed
    # Create the NIfTI-1 image
    nii_img = nib.Nifti1Image(
        data[0], affine
    )  # Remove the channel dimension for NIfTI

    # Save the NIfTI file if a filename is provided
    if filename:
        nib.save(nii_img, filename)
    else:
        return nii_img

to_ome_zarr(filename, max_layer=4, scaling_method='local_mean', **kwargs)

Save the current ZarrNii instance to an OME-Zarr file, always writing axes in ZYX order.

Parameters:

Name Type Description Default
filename str

Output path for the OME-Zarr file.

required
max_layer int

Maximum number of downsampling layers (default: 4).

4
scaling_method str

Method for downsampling (default: "local_mean").

'local_mean'
**kwargs

Additional arguments for write_image.

{}
Source code in zarrnii/core.py
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
def to_ome_zarr(self, filename, max_layer=4, scaling_method="local_mean", **kwargs):
    """
    Save the current ZarrNii instance to an OME-Zarr file, always writing
    axes in ZYX order.

    Parameters:
        filename (str): Output path for the OME-Zarr file.
        max_layer (int): Maximum number of downsampling layers (default: 4).
        scaling_method (str): Method for downsampling (default: "local_mean").
        **kwargs: Additional arguments for `write_image`.
    """
    # Always write OME-Zarr axes as ZYX
    axes = [
        {"name": "c", "type": "channel", "unit": None},
        {"name": "z", "type": "space", "unit": "micrometer"},
        {"name": "y", "type": "space", "unit": "micrometer"},
        {"name": "x", "type": "space", "unit": "micrometer"},
    ]

    # Reorder data if the axes order is XYZ (NIfTI-like)
    if self.axes_order == "XYZ":
        out_darr = da.moveaxis(
            self.darr, (0, 1, 2, 3), (0, 3, 2, 1)
        )  # Reorder to ZYX
        #   flip_xfm = np.diag((-1, -1, -1, 1))  # Apply flips for consistency
        # out_affine = flip_xfm @ self.affine
        out_affine = self.reorder_affine_xyz_zyx(self.affine.matrix)
    #  voxdim = np.flip(voxdim)  # Adjust voxel dimensions to ZYX
    else:
        out_darr = self.darr
        out_affine = self.affine.matrix

    # Extract offset and voxel dimensions from the affine matrix
    offset = out_affine[:3, 3]
    voxdim = np.sqrt((out_affine[:3, :3] ** 2).sum(axis=0))  # Extract scales

    # Prepare coordinate transformations
    coordinate_transformations = []
    for layer in range(max_layer + 1):
        scale = [
            1,
            voxdim[0],
            (2**layer) * voxdim[1],  # Downsampling in Y
            (2**layer) * voxdim[2],  # Downsampling in X
        ]
        translation = [
            0,
            offset[0],
            offset[1] / (2**layer),
            offset[2] / (2**layer),
        ]
        coordinate_transformations.append(
            [
                {"type": "scale", "scale": scale},
                {"type": "translation", "translation": translation},
            ]
        )

    # Set up Zarr store
    store = zarr.storage.FSStore(filename, dimension_separator="/", mode="w")
    group = zarr.group(store, overwrite=True)

    # Add metadata for orientation
    group.attrs["orientation"] = affine_to_orientation(
        out_affine
    )  # Write current orientation

    # Set up scaler for multi-resolution pyramid
    if max_layer == 0:
        scaler = None
    else:
        scaler = Scaler(max_layer=max_layer, method=scaling_method)

    # Write the data to OME-Zarr
    with ProgressBar():
        write_image(
            image=out_darr,
            group=group,
            scaler=scaler,
            coordinate_transformations=coordinate_transformations,
            axes=axes,
            **kwargs,
        )

upsample(along_x=1, along_y=1, along_z=1, to_shape=None)

Upsamples the ZarrNii instance using scipy.ndimage.zoom.

Parameters:

Name Type Description Default
along_x int

Upsampling factor along the X-axis (default: 1).

1
along_y int

Upsampling factor along the Y-axis (default: 1).

1
along_z int

Upsampling factor along the Z-axis (default: 1).

1
to_shape tuple

Target shape for upsampling. Should include all dimensions (e.g., (c, z, y, x) for ZYX or (c, x, y, z) for XYZ). If provided, along_x, along_y, and along_z are ignored.

None

Returns:

Name Type Description
ZarrNii

A new ZarrNii instance with the upsampled data and updated affine.

Notes
  • This method supports both direct scaling via along_* factors or target shape via to_shape.
  • If to_shape is provided, chunk sizes and scaling factors are dynamically calculated.
  • Currently, the method assumes axes_order != 'XYZ' for proper affine scaling.
  • The affine matrix is updated to reflect the new voxel size after upsampling.
Example

Upsample with scaling factors

upsampled_znimg = znimg.upsample(along_x=2, along_y=2, along_z=2)

Upsample to a specific shape

upsampled_znimg = znimg.upsample(to_shape=(1, 256, 256, 256))

Source code in zarrnii/core.py
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
def upsample(self, along_x=1, along_y=1, along_z=1, to_shape=None):
    """
    Upsamples the ZarrNii instance using `scipy.ndimage.zoom`.

    Parameters:
        along_x (int, optional): Upsampling factor along the X-axis (default: 1).
        along_y (int, optional): Upsampling factor along the Y-axis (default: 1).
        along_z (int, optional): Upsampling factor along the Z-axis (default: 1).
        to_shape (tuple, optional): Target shape for upsampling. Should include all dimensions
                                     (e.g., `(c, z, y, x)` for ZYX or `(c, x, y, z)` for XYZ).
                                     If provided, `along_x`, `along_y`, and `along_z` are ignored.

    Returns:
        ZarrNii: A new ZarrNii instance with the upsampled data and updated affine.

    Notes:
        - This method supports both direct scaling via `along_*` factors or target shape via `to_shape`.
        - If `to_shape` is provided, chunk sizes and scaling factors are dynamically calculated.
        - Currently, the method assumes `axes_order != 'XYZ'` for proper affine scaling.
        - The affine matrix is updated to reflect the new voxel size after upsampling.

    Example:
        # Upsample with scaling factors
        upsampled_znimg = znimg.upsample(along_x=2, along_y=2, along_z=2)

        # Upsample to a specific shape
        upsampled_znimg = znimg.upsample(to_shape=(1, 256, 256, 256))
    """
    # Determine scaling and chunks based on input parameters
    if to_shape is None:
        if self.axes_order == "XYZ":
            scaling = (1, along_x, along_y, along_z)
        else:
            scaling = (1, along_z, along_y, along_x)

        chunks_out = tuple(
            tuple(c * scale for c in chunks_i)
            for chunks_i, scale in zip(self.darr.chunks, scaling)
        )
    else:
        chunks_out, scaling = self.__get_upsampled_chunks(to_shape)

    # Define block-wise upsampling function
    def zoom_blocks(x, block_info=None):
        """
        Scales blocks to the desired size using `scipy.ndimage.zoom`.

        Parameters:
            x (np.ndarray): Input block data.
            block_info (dict, optional): Metadata about the current block.

        Returns:
            np.ndarray: The upscaled block.
        """
        # Calculate scaling factors based on input and output chunk shapes
        scaling = tuple(
            out_n / in_n
            for out_n, in_n in zip(block_info[None]["chunk-shape"], x.shape)
        )
        return zoom(x, scaling, order=1, prefilter=False)

    # Perform block-wise upsampling
    darr_scaled = da.map_blocks(
        zoom_blocks, self.darr, dtype=self.darr.dtype, chunks=chunks_out
    )

    # Update the affine matrix to reflect the new voxel size
    if self.axes_order == "XYZ":
        scaling_matrix = np.diag(
            (1 / scaling[1], 1 / scaling[2], 1 / scaling[3], 1)
        )
    else:
        scaling_matrix = np.diag(
            (1 / scaling[-1], 1 / scaling[-2], 1 / scaling[-3], 1)
        )
    new_affine = AffineTransform.from_array(scaling_matrix @ self.affine.matrix)

    # Return a new ZarrNii instance with the upsampled data
    return ZarrNii.from_darr(
        darr_scaled.rechunk(), affine=new_affine, axes_order=self.axes_order
    )

Bases: Transform

matrix = None class-attribute instance-attribute

__array__()

Define how the object behaves when converted to a numpy array. Returns the matrix of the affine transform.

Source code in zarrnii/transform.py
45
46
47
48
49
50
def __array__(self):
    """
    Define how the object behaves when converted to a numpy array.
    Returns the matrix of the affine transform.
    """
    return self.matrix

__getitem__(key)

Enable array-like indexing on the matrix.

Source code in zarrnii/transform.py
52
53
54
55
56
def __getitem__(self, key):
    """
    Enable array-like indexing on the matrix.
    """
    return self.matrix[key]

__matmul__(other)

Perform matrix multiplication with another object.

  • other (np.ndarray or AffineTransform): The object to multiply with:
    • (3,) or (3, 1): A 3D point or vector (voxel coordinates).
    • (3, N): A batch of N 3D points or vectors (voxel coordinates).
    • (4,) or (4, 1): A 4D point/vector in homogeneous coordinates.
    • (4, N): A batch of N 4D points in homogeneous coordinates.
    • (4, 4): Another affine transformation matrix.
  • np.ndarray or AffineTransform:
    • Transformed 3D point(s) or vector(s) as a numpy array.
    • A new AffineTransform object if multiplying two affine matrices.

Raises: - ValueError: If the shape of other is unsupported. - TypeError: If other is not an np.ndarray or AffineTransform.

Source code in zarrnii/transform.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def __matmul__(self, other):
    """
    Perform matrix multiplication with another object.

    Parameters:
    - other (np.ndarray or AffineTransform): The object to multiply with:
        - (3,) or (3, 1): A 3D point or vector (voxel coordinates).
        - (3, N): A batch of N 3D points or vectors (voxel coordinates).
        - (4,) or (4, 1): A 4D point/vector in homogeneous coordinates.
        - (4, N): A batch of N 4D points in homogeneous coordinates.
        - (4, 4): Another affine transformation matrix.

    Returns:
    - np.ndarray or AffineTransform:
        - Transformed 3D point(s) or vector(s) as a numpy array.
        - A new AffineTransform object if multiplying two affine matrices.

    Raises:
    - ValueError: If the shape of `other` is unsupported.
    - TypeError: If `other` is not an np.ndarray or AffineTransform.
    """
    if isinstance(other, np.ndarray):
        if other.shape == (3,):
            # Single 3D point/vector
            homog_point = np.append(other, 1)  # Convert to homogeneous coordinates
            result = self.matrix @ homog_point
            return result[:3] / result[3]  # Convert back to 3D
        elif len(other.shape) == 2 and other.shape[0] == 3:
            # Batch of 3D points/vectors (3 x N)
            homog_points = np.vstack(
                [other, np.ones((1, other.shape[1]))]
            )  # Add homogeneous row
            transformed_points = (
                self.matrix @ homog_points
            )  # Apply affine transform
            return (
                transformed_points[:3] / transformed_points[3]
            )  # Convert back to 3D
        elif other.shape == (4,):
            # Single 4D point/vector
            result = self.matrix @ other
            return result[:3] / result[3]
        elif len(other.shape) == 2 and other.shape[0] == 4:
            # Batch of 4D points in homogeneous coordinates (4 x N)
            transformed_points = self.matrix @ other  # Apply affine transform
            return transformed_points  # No conversion needed, stays in 4D space
        elif other.shape == (4, 4):
            # Matrix multiplication with another affine matrix
            return AffineTransform.from_array(self.matrix @ other)
        else:
            raise ValueError(f"Unsupported shape for multiplication: {other.shape}")
    elif isinstance(other, AffineTransform):
        # Matrix multiplication with another AffineTransform object
        return AffineTransform.from_array(self.matrix @ other.matrix)
    else:
        raise TypeError(f"Unsupported type for multiplication: {type(other)}")

__setitem__(key, value)

Enable array-like assignment to the matrix.

Source code in zarrnii/transform.py
58
59
60
61
62
def __setitem__(self, key, value):
    """
    Enable array-like assignment to the matrix.
    """
    self.matrix[key] = value

apply_transform(vecs)

Source code in zarrnii/transform.py
121
122
def apply_transform(self, vecs: np.array) -> np.array:
    return self @ vecs

from_array(matrix, invert=False) classmethod

Source code in zarrnii/transform.py
34
35
36
37
38
39
@classmethod
def from_array(cls, matrix, invert=False):
    if invert:
        matrix = np.linalg.inv(matrix)

    return cls(matrix=matrix)

from_txt(path, invert=False) classmethod

Source code in zarrnii/transform.py
26
27
28
29
30
31
32
@classmethod
def from_txt(cls, path, invert=False):
    matrix = np.loadtxt(path)
    if invert:
        matrix = np.linalg.inv(matrix)

    return cls(matrix=matrix)

identity() classmethod

Source code in zarrnii/transform.py
41
42
43
@classmethod
def identity(cls):
    return cls(matrix=np.eye(4, 4))

invert()

Return the inverse of the matrix transformation.

Source code in zarrnii/transform.py
124
125
126
def invert(self):
    """Return the inverse of the matrix transformation."""
    return AffineTransform.from_array(np.linalg.inv(self.matrix))

update_for_orientation(input_orientation, output_orientation)

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

Parameters:

Name Type Description Default
input_orientation str

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

required
output_orientation str

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

required
Source code in zarrnii/transform.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def update_for_orientation(self, input_orientation, output_orientation):
    """
    Update the matrix to map from the input orientation to the output orientation.

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

    # 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)