Skip to content

OME-ZARR conversion plugin

Write any array or image file to a pyramidal OME-ZARR store, add resolution levels to an existing store, or store a label image inside an OME-ZARR under the NGFF labels/ group. Uses only the core dependencies for arrays and .zarr inputs; reading other file formats needs the optional bioio extra (pip install "patchworks[bioio]").

Pyramids downsample X and Y onlyZ (and channel/time) are kept at full resolution, matching anisotropic microscopy stacks.

to_ome_zarr

patchworks.plugins.ome_zarr.to_ome_zarr(source: Union[da.Array, np.ndarray, str, Path], out_path: Union[str, Path], *, axes: Union[str, None] = None, pixel_size: Union[PixelSize, tuple, None] = None, scene: int = 0, sequence_pattern: Union[str, None] = None, n_levels: int = 5, downscale: int = 2, chunks: Union[tuple[int, ...], None] = None, shard: ShardSpec = False, reuse_pyramid: bool = False, progress: bool = True, overwrite: bool = False) -> str

Write source as a pyramidal, calibrated OME-ZARR store.

source may be a dask/NumPy array, a .zarr store, an Imaris .ims file, any image format readable by bioio (CZI, LIF, ND2, OME-TIFF, …), or (with sequence_pattern set) a glob over a folder of single-plane TIFFs. File inputs are read lazily; the pyramid is built level-by-level from disk with bounded chunks, so the full volume never needs to fit in RAM. Only x/y are downsampled; z (and channel/time) stay full-resolution.

Parameters:

Name Type Description Default
source (Array, ndarray, str or Path)

Array or path to convert.

required
out_path str or Path

Destination .zarr store (a directory).

required
axes str

One character per array dimension, e.g. "zyx" or "cyx". None → inferred from the file metadata or the array dimensions.

None
pixel_size (dict, tuple or None)

Physical voxel size in micrometers, as {"z": .., "y": .., "x": ..} or a tuple aligned to the spatial axes. None → read from the input (bioio/Imaris/OME-ZARR); falls back to 1.0 (uncalibrated) for bare arrays.

None
scene int

Scene index for multi-scene bioio files.

0
sequence_pattern str

When given, source is treated as a glob pattern over a folder of single-plane TIFFs (e.g. "folder/*.tif") instead of a single file, and this is the regex parsing each file name into axis labels and indices via named groups, e.g. r"_T(?P<T>\d+)_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+". Each file becomes exactly one chunk, read lazily on access (no data duplicated) — see :func:tifffile.TiffSequence. The result is always reordered to patchworks' czyx convention regardless of the order named groups appear in the pattern, and any singleton non-spatial axis (e.g. a constant T0) is dropped, so a real channel axis always ends up first.

None
n_levels int

Maximum number of pyramid levels including full resolution.

5
downscale int

Per-level X/Y downsampling factor (default 2).

2
chunks tuple of int

Chunk shape for the written levels. None → a bounded default.

None
shard bool or tuple of int

Pack many chunks into one shard file (zarr v3), cutting the file count ~100× on huge arrays. False (default) → unsharded, maximum reader compatibility. True → auto-pick a ~512 MB shard. A tuple sets an explicit shard shape (clamped to a chunk multiple). Sharded writes hold ~one shard per worker in RAM. Requires zarr v3 (ignored otherwise).

False
progress bool

Show a per-level dask progress bar (default True). Set False to silence it.

True
reuse_pyramid bool

Imaris .ims only. Copy the file's own resolution levels instead of rebuilding the pyramid (faster, no recompute), keeping each level's native scale. Ignored for other inputs; falls back to a rebuild if the Imaris levels can't be read. Default False (rebuild, for a consistent XY-only, nearest-neighbour NGFF pyramid).

False
overwrite bool

Overwrite an existing store at out_path.

False

Returns:

Type Description
str

The path to the written store (str(out_path)).

Examples:

>>> from patchworks.plugins.ome_zarr import to_ome_zarr
>>> to_ome_zarr("scan.ims", "scan.zarr", n_levels=4)
'scan.zarr'
>>> to_ome_zarr(
...     "ZT18_Male4_Left/*.tif",
...     "ZT18_Male4_Left.zarr",
...     sequence_pattern=r"_T(?P<T>\d+)_Z(?P<Z>\d+)_C(?P<C>\d+)_V\d+",
...     shard=True,
... )
'ZT18_Male4_Left.zarr'
Source code in src/patchworks/plugins/ome_zarr.py
def to_ome_zarr(
    source: Union[da.Array, np.ndarray, str, Path],
    out_path: Union[str, Path],
    *,
    axes: Union[str, None] = None,
    pixel_size: Union[PixelSize, tuple, None] = None,
    scene: int = 0,
    sequence_pattern: Union[str, None] = None,
    n_levels: int = 5,
    downscale: int = 2,
    chunks: Union[tuple[int, ...], None] = None,
    shard: ShardSpec = False,
    reuse_pyramid: bool = False,
    progress: bool = True,
    overwrite: bool = False,
) -> str:
    """Write *source* as a pyramidal, calibrated OME-ZARR store.

    *source* may be a dask/NumPy array, a ``.zarr`` store, an Imaris ``.ims``
    file, any image format readable by bioio (CZI, LIF, ND2, OME-TIFF, …), or
    (with *sequence_pattern* set) a glob over a folder of single-plane TIFFs.
    File inputs are read lazily; the pyramid is built level-by-level from disk
    with bounded chunks, so the full volume never needs to fit in RAM. Only
    ``x``/``y`` are downsampled; ``z`` (and channel/time) stay full-resolution.

    Parameters
    ----------
    source : da.Array, np.ndarray, str or Path
        Array or path to convert.
    out_path : str or Path
        Destination ``.zarr`` store (a directory).
    axes : str, optional
        One character per array dimension, e.g. ``"zyx"`` or ``"cyx"``.
        ``None`` → inferred from the file metadata or the array dimensions.
    pixel_size : dict, tuple or None, optional
        Physical voxel size in micrometers, as ``{"z": .., "y": .., "x": ..}``
        or a tuple aligned to the spatial axes. ``None`` → read from the input
        (bioio/Imaris/OME-ZARR); falls back to 1.0 (uncalibrated) for bare
        arrays.
    scene : int, optional
        Scene index for multi-scene bioio files.
    sequence_pattern : str, optional
        When given, *source* is treated as a glob pattern over a folder of
        single-plane TIFFs (e.g. ``"folder/*.tif"``) instead of a single
        file, and this is the regex parsing each file name into axis labels
        and indices via named groups, e.g.
        ``r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+"``. Each file
        becomes exactly one chunk, read lazily on access (no data
        duplicated) — see :func:`tifffile.TiffSequence`. The result is
        always reordered to patchworks' ``czyx`` convention regardless of
        the order named groups appear in the pattern, and any singleton
        non-spatial axis (e.g. a constant ``T0``) is dropped, so a real
        channel axis always ends up first.
    n_levels : int, optional
        Maximum number of pyramid levels including full resolution.
    downscale : int, optional
        Per-level X/Y downsampling factor (default 2).
    chunks : tuple of int, optional
        Chunk shape for the written levels. ``None`` → a bounded default.
    shard : bool or tuple of int, optional
        Pack many chunks into one shard file (zarr v3), cutting the file count
        ~100× on huge arrays. ``False`` (default) → unsharded, maximum reader
        compatibility. ``True`` → auto-pick a ~512 MB shard. A tuple sets an
        explicit shard shape (clamped to a chunk multiple). Sharded writes hold
        ~one shard per worker in RAM. Requires zarr v3 (ignored otherwise).
    progress : bool, optional
        Show a per-level dask progress bar (default ``True``). Set ``False`` to
        silence it.
    reuse_pyramid : bool, optional
        *Imaris ``.ims`` only.* Copy the file's **own** resolution levels
        instead of rebuilding the pyramid (faster, no recompute), keeping each
        level's native scale. Ignored for other inputs; falls back to a
        rebuild if the Imaris levels can't be read. Default ``False`` (rebuild,
        for a consistent XY-only, nearest-neighbour NGFF pyramid).
    overwrite : bool, optional
        Overwrite an existing store at *out_path*.

    Returns
    -------
    str
        The path to the written store (``str(out_path)``).

    Examples
    --------
    >>> from patchworks.plugins.ome_zarr import to_ome_zarr
    >>> to_ome_zarr("scan.ims", "scan.zarr", n_levels=4)
    'scan.zarr'
    >>> to_ome_zarr(
    ...     "ZT18_Male4_Left/*.tif",
    ...     "ZT18_Male4_Left.zarr",
    ...     sequence_pattern=r"_T(?P<T>\\d+)_Z(?P<Z>\\d+)_C(?P<C>\\d+)_V\\d+",
    ...     shard=True,
    ... )  # doctest: +SKIP
    'ZT18_Male4_Left.zarr'
    """
    if downscale < 2:
        raise ValueError("downscale must be >= 2")
    if n_levels < 1:
        raise ValueError("n_levels must be >= 1")

    # Reuse an Imaris file's own resolution pyramid instead of rebuilding it.
    if (
        reuse_pyramid
        and isinstance(source, (str, Path))
        and str(source).lower().endswith(".ims")
    ):
        try:
            return _write_imaris_pyramid(
                str(source),
                str(out_path),
                chunks=chunks,
                overwrite=overwrite,
                shard=shard,
                progress=progress,
            )
        except Exception as exc:
            logger.warning(
                "reuse_pyramid failed (%s); rebuilding the pyramid instead.",
                exc,
            )

    arr, axes, detected = _to_dask(source, axes, scene, sequence_pattern)
    if len(axes) != arr.ndim:
        raise ValueError(
            f"axes {axes!r} has {len(axes)} entries but array is {arr.ndim}-D"
        )

    ps = _normalize_pixel_size(pixel_size, axes) if pixel_size else detected
    base_scale = _base_scale(axes, ps)

    out = str(out_path)
    zarr.open_group(out, mode="w" if overwrite else "w-")
    with _bounded_scheduler(arr):
        datasets = _write_pyramid(
            arr,
            axes,
            out,
            n_levels=n_levels,
            downscale=downscale,
            chunks=chunks,
            base_scale=base_scale,
            shard=shard,
            progress=progress,
        )
    _write_multiscales(out, axes, datasets, Path(out).stem, calibrated=bool(ps))
    return out

add_pyramid

patchworks.plugins.ome_zarr.add_pyramid(group_path: Union[str, Path], *, base: str = '0', axes: Union[str, None] = None, pixel_size: Union[PixelSize, tuple, None] = None, n_levels: int = 5, downscale: int = 2, chunks: Union[tuple[int, ...], None] = None, shard: ShardSpec = False, progress: bool = True) -> str

Add downsampled pyramid levels to an existing single-resolution zarr.

Reads the full-resolution array already at group_path/base, writes the missing levels next to it (lazily, from disk), and (re)writes the NGFF multiscales metadata. Existing calibration is preserved; pass pixel_size to set it.

Parameters:

Name Type Description Default
group_path str or Path

Zarr group containing the full-resolution array at base.

required
base str

Component name of the existing full-resolution level (default "0"). Auto-detected from existing multiscales metadata if present, overriding this.

'0'
axes str

One letter per axis, e.g. "zyx". None → inferred from existing metadata, or from the array's dimensionality.

None
pixel_size (dict, tuple or None)

Physical voxel size in micrometers. None → read from the store's existing calibration, if any.

None
n_levels int

Maximum number of levels including the existing full-resolution one (default 5).

5
downscale int

Per-level X/Y downsampling factor (default 2).

2
chunks tuple of int

Chunk shape for the written levels. None → a bounded default.

None
shard bool or tuple of int

Sharding request (see :func:to_ome_zarr's shard).

False
progress bool

Show a per-level dask progress bar (default True).

True

Returns:

Type Description
str

The path to the updated group.

Examples:

>>> add_pyramid("scan.zarr", n_levels=4)
'scan.zarr'
Source code in src/patchworks/plugins/ome_zarr.py
def add_pyramid(
    group_path: Union[str, Path],
    *,
    base: str = "0",
    axes: Union[str, None] = None,
    pixel_size: Union[PixelSize, tuple, None] = None,
    n_levels: int = 5,
    downscale: int = 2,
    chunks: Union[tuple[int, ...], None] = None,
    shard: ShardSpec = False,
    progress: bool = True,
) -> str:
    """Add downsampled pyramid levels to an existing single-resolution zarr.

    Reads the full-resolution array already at ``group_path/base``, writes the
    missing levels next to it (lazily, from disk), and (re)writes the NGFF
    ``multiscales`` metadata. Existing calibration is preserved; pass
    *pixel_size* to set it.

    Parameters
    ----------
    group_path : str or Path
        Zarr group containing the full-resolution array at *base*.
    base : str, optional
        Component name of the existing full-resolution level (default
        ``"0"``). Auto-detected from existing ``multiscales`` metadata if
        present, overriding this.
    axes : str, optional
        One letter per axis, e.g. ``"zyx"``. ``None`` → inferred from
        existing metadata, or from the array's dimensionality.
    pixel_size : dict, tuple or None, optional
        Physical voxel size in micrometers. ``None`` → read from the store's
        existing calibration, if any.
    n_levels : int, optional
        Maximum number of levels including the existing full-resolution one
        (default 5).
    downscale : int, optional
        Per-level X/Y downsampling factor (default 2).
    chunks : tuple of int, optional
        Chunk shape for the written levels. ``None`` → a bounded default.
    shard : bool or tuple of int, optional
        Sharding request (see :func:`to_ome_zarr`'s *shard*).
    progress : bool, optional
        Show a per-level dask progress bar (default ``True``).

    Returns
    -------
    str
        The path to the updated group.

    Examples
    --------
    >>> add_pyramid("scan.zarr", n_levels=4)  # doctest: +SKIP
    'scan.zarr'
    """
    if downscale < 2:
        raise ValueError("downscale must be >= 2")
    if n_levels < 1:
        raise ValueError("n_levels must be >= 1")

    gp = str(group_path)
    root = zarr.open_group(gp, mode="r")
    multiscales = read_ngff_attr(root.attrs, "multiscales")
    if multiscales:
        base = multiscales[0]["datasets"][0]["path"]
        if axes is None:
            axes = "".join(a["name"] for a in multiscales[0]["axes"])

    base_arr = da.from_zarr(gp, component=base)
    if axes is None:
        axes = _default_axes(base_arr.ndim)
    if len(axes) != base_arr.ndim:
        raise ValueError(
            f"axes {axes!r} has {len(axes)} entries but array is "
            f"{base_arr.ndim}-D"
        )

    if pixel_size:
        ps = _normalize_pixel_size(pixel_size, axes)
    else:
        ps = _read_zarr_calibration(gp, axes)
    base_scale = _base_scale(axes, ps)

    datasets = _write_pyramid(
        base_arr,
        axes,
        gp,
        n_levels=n_levels,
        downscale=downscale,
        chunks=chunks,
        base_scale=base_scale,
        base_name=base,
        write_base=False,
        shard=shard,
        progress=progress,
    )
    _write_multiscales(gp, axes, datasets, Path(gp).stem, calibrated=bool(ps))
    return gp

write_labels

patchworks.plugins.ome_zarr.write_labels(image_store: Union[str, Path], labels: Union[da.Array, np.ndarray], *, name: str = 'labels', axes: Union[str, None] = None, pixel_size: Union[PixelSize, tuple, None] = None, n_levels: int = 5, downscale: int = 2, chunks: Union[tuple[int, ...], None] = None, shard: ShardSpec = False, progress: bool = True, overwrite: bool = False, n_objects: Union[int, None] = None) -> str

Store labels inside image_store under the NGFF labels/ group.

The labels are written as their own multi-scale pyramid at image_store/labels/<name>/ and registered in image_store/labels/.zattrs, so the image and its segmentation live in a single OME-ZARR store. Calibration is inherited from the parent image unless pixel_size is given.

Parameters:

Name Type Description Default
image_store str or Path

OME-ZARR store this label image belongs to.

required
labels Array or ndarray

Integer label array (0 = background), same spatial shape as the image.

required
name str

Label image name under labels/ (default "labels").

'labels'
axes str

One letter per axis. None → inferred from labels' dimensionality.

None
pixel_size (dict, tuple or None)

Physical voxel size in micrometers. None → inherited from the parent image's own calibration.

None
n_levels int

Maximum number of pyramid levels including full resolution (default 5).

5
downscale int

Per-level X/Y downsampling factor (default 2).

2
chunks tuple of int

Chunk shape for the written levels. None → a bounded default.

None
shard bool or tuple of int

Sharding request (see :func:to_ome_zarr's shard).

False
progress bool

Show a per-level dask progress bar (default True).

True
overwrite bool

Replace an existing label image of the same name (default False).

False
n_objects int or None

Exact non-background object count, if known — forwarded to :func:register_labels; see its docstring for what this enables.

None

Returns:

Type Description
str

Path to the written label group (image_store/labels/<name>).

Examples:

>>> from patchworks import merge_tile_labels
>>> merged, n = merge_tile_labels(
...     "stage.zarr",
...     input_component="staged",
...     write_to="merged.zarr",
...     sequential_labels=True,
...     return_count=True,
... )
>>> write_labels(
...     "scan.zarr", merged, name="cells", n_objects=n
... )
'scan.zarr/labels/cells'
Source code in src/patchworks/plugins/ome_zarr.py
def write_labels(
    image_store: Union[str, Path],
    labels: Union[da.Array, np.ndarray],
    *,
    name: str = "labels",
    axes: Union[str, None] = None,
    pixel_size: Union[PixelSize, tuple, None] = None,
    n_levels: int = 5,
    downscale: int = 2,
    chunks: Union[tuple[int, ...], None] = None,
    shard: ShardSpec = False,
    progress: bool = True,
    overwrite: bool = False,
    n_objects: Union[int, None] = None,
) -> str:
    """Store *labels* inside *image_store* under the NGFF ``labels/`` group.

    The labels are written as their own multi-scale pyramid at
    ``image_store/labels/<name>/`` and registered in
    ``image_store/labels/.zattrs``, so the image and its segmentation live in a
    single OME-ZARR store. Calibration is inherited from the parent image
    unless *pixel_size* is given.

    Parameters
    ----------
    image_store : str or Path
        OME-ZARR store this label image belongs to.
    labels : da.Array or np.ndarray
        Integer label array (0 = background), same spatial shape as the
        image.
    name : str, optional
        Label image name under ``labels/`` (default ``"labels"``).
    axes : str, optional
        One letter per axis. ``None`` → inferred from *labels*'
        dimensionality.
    pixel_size : dict, tuple or None, optional
        Physical voxel size in micrometers. ``None`` → inherited from the
        parent image's own calibration.
    n_levels : int, optional
        Maximum number of pyramid levels including full resolution
        (default 5).
    downscale : int, optional
        Per-level X/Y downsampling factor (default 2).
    chunks : tuple of int, optional
        Chunk shape for the written levels. ``None`` → a bounded default.
    shard : bool or tuple of int, optional
        Sharding request (see :func:`to_ome_zarr`'s *shard*).
    progress : bool, optional
        Show a per-level dask progress bar (default ``True``).
    overwrite : bool, optional
        Replace an existing label image of the same *name* (default
        ``False``).
    n_objects : int or None, optional
        Exact non-background object count, if known — forwarded to
        :func:`register_labels`; see its docstring for what this enables.

    Returns
    -------
    str
        Path to the written label group (``image_store/labels/<name>``).

    Examples
    --------
    >>> from patchworks import merge_tile_labels
    >>> merged, n = merge_tile_labels(
    ...     "stage.zarr",
    ...     input_component="staged",
    ...     write_to="merged.zarr",
    ...     sequential_labels=True,
    ...     return_count=True,
    ... )  # doctest: +SKIP
    >>> write_labels(
    ...     "scan.zarr", merged, name="cells", n_objects=n
    ... )  # doctest: +SKIP
    'scan.zarr/labels/cells'
    """
    arr = labels if isinstance(labels, da.Array) else da.asarray(labels)
    if axes is None:
        axes = _default_axes(arr.ndim)
    if len(axes) != arr.ndim:
        raise ValueError(
            f"axes {axes!r} has {len(axes)} entries but array is {arr.ndim}-D"
        )

    store = str(image_store)
    root = zarr.open_group(store, mode="a")
    parent = root.require_group("labels")
    if overwrite and name in parent:
        del parent[name]
    parent.require_group(name)

    label_group = f"{store}/labels/{name}"
    base = arr.rechunk(chunks or _default_chunks(arr.shape, axes))
    _to_zarr_level(base, label_group, "0", shard, progress)
    return register_labels(
        store,
        name,
        axes=axes,
        pixel_size=pixel_size,
        n_levels=n_levels,
        downscale=downscale,
        chunks=chunks,
        shard=shard,
        progress=progress,
        n_objects=n_objects,
    )

register_labels

patchworks.plugins.ome_zarr.register_labels(image_store: Union[str, Path], name: str = 'labels', *, axes: Union[str, None] = None, pixel_size: Union[PixelSize, tuple, None] = None, n_levels: int = 5, downscale: int = 2, chunks: Union[tuple[int, ...], None] = None, shard: ShardSpec = False, progress: bool = True, n_objects: Union[int, None] = None) -> str

Pyramidalise and register an existing labels/<name>/0 base level.

Assumes the full-resolution label array already exists at image_store/labels/<name>/0. Adds the downsampled levels, tags the group with NGFF image-label metadata, lists name in labels/.zattrs, and inherits the parent image's pixel calibration (unless pixel_size is given).

Parameters:

Name Type Description Default
image_store str or Path

OME-ZARR store path containing the image this label belongs to.

required
name str

Label image name under labels/ (default "labels").

'labels'
axes str

One letter per axis. None → inferred from the label array.

None
pixel_size (dict, tuple or None)

Physical voxel size in micrometers. None → inherited from the parent image's own calibration.

None
n_levels int

Maximum number of pyramid levels including full resolution (default 5).

5
downscale int

Per-level X/Y downsampling factor (default 2).

2
chunks tuple of int

Chunk shape for the written levels. None → a bounded default.

None
shard bool or tuple of int

Sharding request (see :func:to_ome_zarr's shard).

False
progress bool

Show a per-level dask progress bar (default True).

True
n_objects int or None

Exact non-background object count, if known (e.g. from :func:patchworks.merge_tile_labels's return_count=True after sequential_labels=True, which means ids == range(1, n_objects + 1) by construction). When given, written into the label group's attrs as n_objects/sequential_labels so a downstream reader (e.g. napari-chunked-regionprops, https://github.com/imcf/napari-chunked-regionprops) can use the known id set instead of re-deriving it with a full-volume scan of its own.

None

Returns:

Type Description
str

Path to the label group (image_store/labels/<name>).

Examples:

>>> register_labels("scan.zarr", "cells")
'scan.zarr/labels/cells'
Source code in src/patchworks/plugins/ome_zarr.py
def register_labels(
    image_store: Union[str, Path],
    name: str = "labels",
    *,
    axes: Union[str, None] = None,
    pixel_size: Union[PixelSize, tuple, None] = None,
    n_levels: int = 5,
    downscale: int = 2,
    chunks: Union[tuple[int, ...], None] = None,
    shard: ShardSpec = False,
    progress: bool = True,
    n_objects: Union[int, None] = None,
) -> str:
    """Pyramidalise and register an existing ``labels/<name>/0`` base level.

    Assumes the full-resolution label array already exists at
    ``image_store/labels/<name>/0``. Adds the downsampled levels, tags the
    group with NGFF ``image-label`` metadata, lists *name* in
    ``labels/.zattrs``, and inherits the parent image's pixel calibration
    (unless *pixel_size* is given).

    Parameters
    ----------
    image_store : str or Path
        OME-ZARR store path containing the image this label belongs to.
    name : str, optional
        Label image name under ``labels/`` (default ``"labels"``).
    axes : str, optional
        One letter per axis. ``None`` → inferred from the label array.
    pixel_size : dict, tuple or None, optional
        Physical voxel size in micrometers. ``None`` → inherited from the
        parent image's own calibration.
    n_levels : int, optional
        Maximum number of pyramid levels including full resolution
        (default 5).
    downscale : int, optional
        Per-level X/Y downsampling factor (default 2).
    chunks : tuple of int, optional
        Chunk shape for the written levels. ``None`` → a bounded default.
    shard : bool or tuple of int, optional
        Sharding request (see :func:`to_ome_zarr`'s *shard*).
    progress : bool, optional
        Show a per-level dask progress bar (default ``True``).
    n_objects : int or None, optional
        Exact non-background object count, if known (e.g. from
        :func:`patchworks.merge_tile_labels`'s ``return_count=True`` after
        ``sequential_labels=True``, which means ``ids == range(1, n_objects
        + 1)`` by construction). When given, written into the label group's
        attrs as ``n_objects``/``sequential_labels`` so a downstream reader
        (e.g. napari-chunked-regionprops,
        https://github.com/imcf/napari-chunked-regionprops) can use the
        known id set instead of re-deriving it with a full-volume scan of
        its own.

    Returns
    -------
    str
        Path to the label group (``image_store/labels/<name>``).

    Examples
    --------
    >>> register_labels("scan.zarr", "cells")  # doctest: +SKIP
    'scan.zarr/labels/cells'
    """
    store = str(image_store)
    group = f"{store}/labels/{name}"
    if not pixel_size:
        arr0 = da.from_zarr(group, component="0")
        lab_axes = axes or _default_axes(arr0.ndim)
        pixel_size = _read_zarr_calibration(store, lab_axes)
    add_pyramid(
        group,
        base="0",
        axes=axes,
        pixel_size=pixel_size,
        n_levels=n_levels,
        downscale=downscale,
        chunks=chunks,
        shard=shard,
        progress=progress,
    )
    grp = zarr.open_group(group, mode="a")
    write_ngff_attrs(grp, **{"image-label": {"version": ngff_version()}})
    if n_objects is not None:
        # patchworks' own hints, not NGFF keys, so they stay at the top level
        # where a consumer can find them without knowing the layout.
        grp.attrs["n_objects"] = int(n_objects)
        grp.attrs["sequential_labels"] = True

    labels_grp = zarr.open_group(f"{store}/labels", mode="a")
    registered = list(read_ngff_attr(labels_grp.attrs, "labels", []) or [])
    if name not in registered:
        registered.append(name)
    write_ngff_attrs(labels_grp, labels=registered)
    return group

read_pixel_size

patchworks.plugins.ome_zarr.read_pixel_size(store: Union[str, Path]) -> PixelSize

Physical voxel size recorded in an OME-ZARR's level-0 metadata.

The calibration the conversion carried over from the source file, as {"z": .., "y": .., "x": ..} in micrometers. Axes left at scale 1.0 (uncalibrated) are omitted, so an empty dict means the store carries no usable calibration.

Use this instead of retyping voxel sizes into a config: a deconvolution told the wrong voxel size produces a plausible-looking but wrong result.

Parameters:

Name Type Description Default
store str or Path

Path of the OME-ZARR group.

required

Returns:

Type Description
dict

{axis: size} for calibrated spatial axes.

Examples:

>>> read_pixel_size("scan.zarr")
{'z': 0.2, 'y': 0.1, 'x': 0.1}
Source code in src/patchworks/plugins/ome_zarr.py
def read_pixel_size(store: Union[str, Path]) -> PixelSize:
    """Physical voxel size recorded in an OME-ZARR's level-0 metadata.

    The calibration the conversion carried over from the source file, as
    ``{"z": .., "y": .., "x": ..}`` in micrometers. Axes left at scale 1.0
    (uncalibrated) are omitted, so an empty dict means the store carries no
    usable calibration.

    Use this instead of retyping voxel sizes into a config: a deconvolution
    told the wrong voxel size produces a plausible-looking but wrong result.

    Parameters
    ----------
    store : str or Path
        Path of the OME-ZARR group.

    Returns
    -------
    dict
        ``{axis: size}`` for calibrated spatial axes.

    Examples
    --------
    >>> read_pixel_size("scan.zarr")  # doctest: +SKIP
    {'z': 0.2, 'y': 0.1, 'x': 0.1}
    """
    return _read_zarr_calibration(store, "")

NGFF metadata layout

NGFF 0.4 is defined over zarr v2 and puts its keys at the top level; 0.5 is the zarr-v3 revision and nests them under ome. patchworks writes whichever matches the store, and reads both.

patchworks.plugins.ome_zarr.ngff_version() -> str

NGFF version matching the zarr format this build writes.

Source code in src/patchworks/plugins/ome_zarr.py
def ngff_version() -> str:
    """NGFF version matching the zarr format this build writes."""
    return _NGFF_VERSION_V3 if _ZARR_V3 else _NGFF_VERSION

patchworks.plugins.ome_zarr.read_ngff_attr(attrs, key: str, default=None)

Read an NGFF key from either layout.

Accepts both the 0.4 top-level placement and the 0.5 ome nesting, so stores written by any patchworks version (or another tool) still load.

Parameters:

Name Type Description Default
attrs Mapping

A zarr group's attributes.

required
key str

NGFF key, e.g. "multiscales", "labels", "image-label".

required
default Any

Returned when the key is absent from both layouts.

None
Source code in src/patchworks/plugins/ome_zarr.py
def read_ngff_attr(attrs, key: str, default=None):
    """Read an NGFF key from either layout.

    Accepts both the 0.4 top-level placement and the 0.5 ``ome`` nesting, so
    stores written by any patchworks version (or another tool) still load.

    Parameters
    ----------
    attrs : Mapping
        A zarr group's attributes.
    key : str
        NGFF key, e.g. ``"multiscales"``, ``"labels"``, ``"image-label"``.
    default : Any, optional
        Returned when the key is absent from both layouts.
    """
    attrs = dict(attrs)
    if key in attrs:
        return attrs[key]
    nested = attrs.get("ome")
    if isinstance(nested, dict) and key in nested:
        return nested[key]
    return default

patchworks.plugins.ome_zarr.write_ngff_attrs(group, **entries) -> None

Write NGFF keys in the layout matching the store's zarr version.

On zarr v3 the keys are merged into the ome attribute (0.5); on v2 they go to the top level (0.4). Merging matters because several keys are written at different times onto the same group -- multiscales by the pyramid, then image-label when the labels are registered.

Source code in src/patchworks/plugins/ome_zarr.py
def write_ngff_attrs(group, **entries) -> None:
    """Write NGFF keys in the layout matching the store's zarr version.

    On zarr v3 the keys are merged into the ``ome`` attribute (0.5); on v2
    they go to the top level (0.4). Merging matters because several keys are
    written at different times onto the same group -- ``multiscales`` by the
    pyramid, then ``image-label`` when the labels are registered.
    """
    if not _ZARR_V3:
        for key, value in entries.items():
            group.attrs[key] = value
        return
    existing = dict(group.attrs).get("ome")
    merged = dict(existing) if isinstance(existing, dict) else {}
    merged["version"] = _NGFF_VERSION_V3
    merged.update(entries)
    group.attrs["ome"] = merged