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 only — Z (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, ngff_version: Union[str, None] = 'auto', compression: Union[str, None] = None, downsample: str = 'mean') -> 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 NGFF pyramid).

False
overwrite bool

Overwrite an existing store at out_path.

False
ngff_version str or None

NGFF version (and therefore zarr format) to write: "auto" (default) writes 0.5 (zarr v3), while "0.4" pins the older, zarr-v2 layout for tools that cannot read 0.5 yet. 0.4 has no sharding codec, so shard is ignored there. See :func:ngff_version.

'auto'
downsample ('mean', 'mode', 'nearest')

How the pyramid's X/Y levels are made from the one above: "mean" (default) block-averages, which is what an image wants; "nearest" keeps every downscale-th pixel (what this wrote before); "mode" takes each block's most frequent non-zero value, which is what a label image wants.

"mean"
compression str

Codec for the arrays written ("zstd", "zstd:3", "blosc", "blosc:lz4", "none", ...; see :func:patchworks.compression). None (default) uses the active setting, zstd level 1 unless changed.

None

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,
    ngff_version: Union[str, None] = "auto",
    compression: Union[str, None] = None,
    downsample: str = "mean",
) -> 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 NGFF pyramid).
    overwrite : bool, optional
        Overwrite an existing store at *out_path*.
    ngff_version : str or None, optional
        NGFF version (and therefore zarr format) to write: ``"auto"``
        (default) writes 0.5 (zarr v3), while ``"0.4"`` pins the older, zarr-v2 layout for tools that
        cannot read 0.5 yet. 0.4 has no sharding codec, so ``shard`` is
        ignored there. See :func:`ngff_version`.
    downsample : {"mean", "mode", "nearest"}, optional
        How the pyramid's X/Y levels are made from the one above: ``"mean"``
        (default) block-averages, which is what an image wants;
        ``"nearest"`` keeps every *downscale*-th pixel (what this wrote
        before); ``"mode"`` takes each block's most frequent non-zero value,
        which is what a label image wants.

    compression : str, optional
        Codec for the arrays written (``"zstd"``, ``"zstd:3"``, ``"blosc"``,
        ``"blosc:lz4"``, ``"none"``, ...; see
        :func:`patchworks.compression`). ``None`` (default) uses the active
        setting, zstd level 1 unless changed.
    Returns
    -------
    str
        The path to the written store (``str(out_path)``).

    Examples
    --------
    >>> from patchworks.plugins.ome_zarr import to_ome_zarr  # doctest: +SKIP
    >>> to_ome_zarr("scan.ims", "scan.zarr", n_levels=4)  # doctest: +SKIP
    'scan.zarr'
    >>> to_ome_zarr(  # doctest: +SKIP
    ...     "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'
    """
    with _writing_ngff(ngff_version), _compression_scope(compression):
        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,
                )

        _check_downsample(downsample)
        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)
        _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,
                downsample=downsample,
            )
        _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, ngff_version: Union[str, None] = 'auto', compression: Union[str, None] = None, downsample: Union[str, None] = None, translation: Union[PixelSize, None] = None) -> 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
ngff_version str or None

NGFF version (and therefore zarr format) to write: "auto" (default) writes 0.5 (zarr v3), while "0.4" pins the older, zarr-v2 layout for tools that cannot read 0.5 yet. 0.4 has no sharding codec, so shard is ignored there. See :func:ngff_version.

'auto'
downsample ('mean', 'mode', 'nearest')

How each level is made from the one above (see :func:to_ome_zarr). None (default) picks "mode" for an NGFF label image (an image-label group, or one under labels/) and "mean" otherwise.

"mean"
translation dict

Physical offset {axis: offset} of the base level's first voxel. None keeps whatever the store already records (zero if nothing).

None
compression str

Codec for the arrays written ("zstd", "zstd:3", "blosc", "blosc:lz4", "none", ...; see :func:patchworks.compression). None (default) uses the active setting, zstd level 1 unless changed.

None

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,
    ngff_version: Union[str, None] = "auto",
    compression: Union[str, None] = None,
    downsample: Union[str, None] = None,
    translation: Union[PixelSize, None] = None,
) -> 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``).
    ngff_version : str or None, optional
        NGFF version (and therefore zarr format) to write: ``"auto"``
        (default) writes 0.5 (zarr v3), while ``"0.4"`` pins the older, zarr-v2 layout for tools that
        cannot read 0.5 yet. 0.4 has no sharding codec, so ``shard`` is
        ignored there. See :func:`ngff_version`.
    downsample : {"mean", "mode", "nearest"} or None, optional
        How each level is made from the one above (see :func:`to_ome_zarr`).
        ``None`` (default) picks ``"mode"`` for an NGFF label image (an
        ``image-label`` group, or one under ``labels/``) and ``"mean"``
        otherwise.
    translation : dict, optional
        Physical offset ``{axis: offset}`` of the base level's first voxel.
        ``None`` keeps whatever the store already records (zero if nothing).

    compression : str, optional
        Codec for the arrays written (``"zstd"``, ``"zstd:3"``, ``"blosc"``,
        ``"blosc:lz4"``, ``"none"``, ...; see
        :func:`patchworks.compression`). ``None`` (default) uses the active
        setting, zstd level 1 unless changed.
    Returns
    -------
    str
        The path to the updated group.

    Examples
    --------
    >>> add_pyramid("scan.zarr", n_levels=4)  # doctest: +SKIP
    'scan.zarr'
    """
    with _writing_ngff(ngff_version), _compression_scope(compression):
        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")
        if downsample is None:
            is_label = (
                read_ngff_attr(root.attrs, "image-label") is not None
                or "labels" in Path(gp).parts[:-1]
            )
            downsample = "mode" if is_label else "mean"
        _check_downsample(downsample)
        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)
        if translation is None:
            translation = read_translation(gp)
        base_translation = [float(translation.get(a, 0.0)) for a in axes]

        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,
            downsample=downsample,
            base_translation=base_translation,
        )
        _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, ngff_version: Union[str, None] = 'auto', compression: Union[str, None] = None, level: int = 0, provenance: Union[dict, 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
level int

The image pyramid level labels were segmented at (default 0); their calibration and offset are taken from that level. See :func:register_labels.

0
ngff_version str or None

NGFF version (and therefore zarr format) to write: "auto" (default) writes 0.5 (zarr v3), while "0.4" pins the older, zarr-v2 layout for tools that cannot read 0.5 yet. 0.4 has no sharding codec, so shard is ignored there. See :func:ngff_version.

'auto'
compression str

Codec for the arrays written ("zstd", "zstd:3", "blosc", "blosc:lz4", "none", ...; see :func:patchworks.compression). None (default) uses the active setting, zstd level 1 unless changed.

None
provenance dict

How the labels were made (see :func:patchworks.provenance), stored in the label group's attrs under "patchworks".

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,
    ngff_version: Union[str, None] = "auto",
    compression: Union[str, None] = None,
    level: int = 0,
    provenance: Union[dict, 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.
    level : int, optional
        The image pyramid level *labels* were segmented at (default 0);
        their calibration and offset are taken from that level. See
        :func:`register_labels`.
    ngff_version : str or None, optional
        NGFF version (and therefore zarr format) to write: ``"auto"``
        (default) writes 0.5 (zarr v3), while ``"0.4"`` pins the older, zarr-v2 layout for tools that
        cannot read 0.5 yet. 0.4 has no sharding codec, so ``shard`` is
        ignored there. See :func:`ngff_version`.

    compression : str, optional
        Codec for the arrays written (``"zstd"``, ``"zstd:3"``, ``"blosc"``,
        ``"blosc:lz4"``, ``"none"``, ...; see
        :func:`patchworks.compression`). ``None`` (default) uses the active
        setting, zstd level 1 unless changed.
    provenance : dict, optional
        How the labels were made (see :func:`patchworks.provenance`),
        stored in the label group's attrs under ``"patchworks"``.
    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'
    """
    with _writing_ngff(ngff_version), _compression_scope(compression):
        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 = _open_group(store)
        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,
            level=level,
            provenance=provenance,
        )

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, ngff_version: Union[str, None] = 'auto', compression: Union[str, None] = None, level: int = 0, provenance: Union[dict, 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 calibration at level.

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
level int

The image pyramid level the labels were segmented at (default 0). Their voxel size and offset are that level's, not level 0's: taking level 0's for labels made at level 1 draws them at half size, drifting further off the image the further from the origin.

0
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
ngff_version str or None

NGFF version (and therefore zarr format) to write: "auto" (default) writes 0.5 (zarr v3), while "0.4" pins the older, zarr-v2 layout for tools that cannot read 0.5 yet. 0.4 has no sharding codec, so shard is ignored there. See :func:ngff_version.

'auto'
compression str

Codec for the arrays written ("zstd", "zstd:3", "blosc", "blosc:lz4", "none", ...; see :func:patchworks.compression). None (default) uses the active setting, zstd level 1 unless changed.

None
provenance dict

How the labels were made (see :func:patchworks.provenance), stored in the label group's attrs under "patchworks".

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,
    ngff_version: Union[str, None] = "auto",
    compression: Union[str, None] = None,
    level: int = 0,
    provenance: Union[dict, 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 calibration at *level*.
    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``).
    level : int, optional
        The image pyramid level the labels were segmented at (default 0).
        Their voxel size and offset are that level's, not level 0's: taking
        level 0's for labels made at level 1 draws them at half size,
        drifting further off the image the further from the origin.
    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.
    ngff_version : str or None, optional
        NGFF version (and therefore zarr format) to write: ``"auto"``
        (default) writes 0.5 (zarr v3), while ``"0.4"`` pins the older, zarr-v2 layout for tools that
        cannot read 0.5 yet. 0.4 has no sharding codec, so ``shard`` is
        ignored there. See :func:`ngff_version`.

    compression : str, optional
        Codec for the arrays written (``"zstd"``, ``"zstd:3"``, ``"blosc"``,
        ``"blosc:lz4"``, ``"none"``, ...; see
        :func:`patchworks.compression`). ``None`` (default) uses the active
        setting, zstd level 1 unless changed.
    provenance : dict, optional
        How the labels were made (see :func:`patchworks.provenance`),
        stored in the label group's attrs under ``"patchworks"``.
    Returns
    -------
    str
        Path to the label group (``image_store/labels/<name>``).

    Examples
    --------
    >>> register_labels("scan.zarr", "cells")  # doctest: +SKIP
    'scan.zarr/labels/cells'
    """
    with _writing_ngff(ngff_version), _compression_scope(compression):
        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, level=level)
        add_pyramid(
            group,
            base="0",
            axes=axes,
            pixel_size=pixel_size,
            n_levels=n_levels,
            downscale=downscale,
            chunks=chunks,
            shard=shard,
            progress=progress,
            # Majority vote, not a mean (averaging ids invents objects) and not
            # decimation (which drops every object its sampled voxel misses).
            downsample="mode",
            translation=read_translation(store, level),
        )
        grp = _open_group(group)
        write_ngff_attrs(
            grp, **{"image-label": {"version": _ngff_version_for(grp)}}
        )
        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
        write_provenance(grp, provenance)

        labels_grp = _open_group(f"{store}/labels")
        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

reshard_level

patchworks.plugins.ome_zarr.reshard_level(group_path: Union[str, Path], component: str = '0', shard: ShardSpec = True, progress: bool = True) -> str

Rewrite an existing array in place, sharded, via one dask pass.

For an array that had to be written unsharded because many writers filled it a chunk at a time -- a label group's level 0, written tile by tile by concurrent segment jobs, or by the merge's own worker pool -- a shard cannot be produced while those writers are running: they would read-modify-write the same shard file and silently lose each other's chunks. Doing it afterwards, single-threaded through :func:_to_zarr_level, is safe because one writer owns every shard.

The cost is one extra full read+write of the level, which is why this is opt-in rather than implied by shard.

Array attributes are carried across. That matters for a label level 0: the merge records how far it got there, and losing that would let a re-run merge already-merged ids and collide unrelated objects.

Parameters:

Name Type Description Default
group_path str or Path

Path of the zarr group holding the array.

required
component str

Array name within the group (default "0").

'0'
shard bool or tuple of int

Sharding request; see :func:_shard_for. False is a no-op.

True
progress bool

Show a dask progress bar for the rewrite.

True

Returns:

Type Description
str

The path of the array that was rewritten.

Source code in src/patchworks/plugins/ome_zarr.py
def reshard_level(
    group_path: Union[str, Path],
    component: str = "0",
    shard: ShardSpec = True,
    progress: bool = True,
) -> str:
    """Rewrite an existing array in place, sharded, via one dask pass.

    For an array that had to be written *unsharded* because many writers
    filled it a chunk at a time -- a label group's level 0, written tile by
    tile by concurrent segment jobs, or by the merge's own worker pool -- a
    shard cannot be produced while those writers are running: they would
    read-modify-write the same shard file and silently lose each other's
    chunks. Doing it afterwards, single-threaded through
    :func:`_to_zarr_level`, is safe because one writer owns every shard.

    The cost is one extra full read+write of the level, which is why this is
    opt-in rather than implied by ``shard``.

    Array attributes are carried across. That matters for a label level 0:
    the merge records how far it got there, and losing that would let a
    re-run merge already-merged ids and collide unrelated objects.

    Parameters
    ----------
    group_path : str or Path
        Path of the zarr group holding the array.
    component : str, optional
        Array name within the group (default ``"0"``).
    shard : bool or tuple of int, optional
        Sharding request; see :func:`_shard_for`. ``False`` is a no-op.
    progress : bool, optional
        Show a dask progress bar for the rewrite.

    Returns
    -------
    str
        The path of the array that was rewritten.
    """
    target = f"{group_path}/{component}"
    if not shard:
        return target
    if is_remote(group_path):
        raise ValueError(
            f"reshard_level swaps directories by rename, which a remote store "
            f"({group_path}) cannot do; reshard a local copy instead"
        )
    if _group_zarr_format(group_path) == 2:
        logger.warning(
            "%s is a zarr v2 store (NGFF %s), which has no sharding codec; "
            "leaving it unsharded.",
            target,
            _NGFF_VERSION,
        )
        return target

    grp = _open_group(group_path)
    attrs = dict(grp[component].attrs)

    # Written beside the original, then swapped: rewriting in place would
    # mean reading an array that is being overwritten underneath.
    tmp = f"{component}__resharding"
    src = da.from_zarr(str(group_path), component=component)
    _to_zarr_level(src, str(group_path), tmp, shard, progress)

    # Swap by renames only, deleting the original last: removing it first
    # left a window in which a crash or a scheduler kill lost the level
    # outright. Now the data exists under one of the two names throughout.
    old, new = Path(group_path) / component, Path(group_path) / tmp
    backup = Path(group_path) / f"{component}__pre_reshard"
    shutil.rmtree(backup, ignore_errors=True)
    old.rename(backup)
    new.rename(old)
    shutil.rmtree(backup)

    grp = _open_group(group_path)
    grp[component].attrs.update(attrs)
    logger.info("resharded %s (%d attr(s) carried over)", target, len(attrs))
    return target

read_pixel_size

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

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

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
level int

Pyramid level (default 0, full resolution). Anything segmented or measured at a coarser level must use that level's voxel size.

0

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}
>>> read_pixel_size("scan.zarr", level=1)
{'z': 0.2, 'y': 0.2, 'x': 0.2}
Source code in src/patchworks/plugins/ome_zarr.py
def read_pixel_size(store: Union[str, Path], level: int = 0) -> PixelSize:
    """Physical voxel size recorded in an OME-ZARR's metadata for *level*.

    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.
    level : int, optional
        Pyramid level (default 0, full resolution). Anything segmented or
        measured at a coarser level must use that level's voxel size.

    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}
    >>> read_pixel_size("scan.zarr", level=1)  # doctest: +SKIP
    {'z': 0.2, 'y': 0.2, 'x': 0.2}
    """
    return _read_zarr_calibration(store, "", level=level)

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 currently being written.

Inside a write call this reflects that call's ngff_version=; outside one it is the version an "auto" write would produce.

Source code in src/patchworks/plugins/ome_zarr.py
def ngff_version() -> str:
    """NGFF version currently being written.

    Inside a write call this reflects that call's ``ngff_version=``;
    outside one it is the version an ``"auto"`` write would produce.
    """
    return _resolve_ngff_version(_ngff_target.get())

Every writer takes an ngff_version= keyword: "auto" (default) follows the installed zarr (0.5, since patchworks requires zarr-python 3) and "0.4" pins the older, zarr-v2 layout. Writing into an existing store always follows that store's format, so a label pyramid added later can never disagree with the image it sits in. NGFF 0.6 is released but not written: RFC-5 replaces axes with coordinateSystems and requires input/output on every coordinate transformation, and no reader supports it yet.

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.
    """
    version = _ngff_version_for(group)
    if _NGFF_ZARR_FORMAT[version] == 2:
        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"] = version
    merged.update(entries)
    group.attrs["ome"] = merged