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, 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, or any image format readable by bioio (CZI, LIF, ND2, OME-TIFF, …). 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
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'
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,
    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, or any image format readable by bioio (CZI, LIF, ND2, OME-TIFF, …).
    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.
    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'
    """
    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)
    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-")
    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.

Returns:

Type Description
str

The path to the updated group.

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.

    Returns
    -------
    str
        The path to the updated group.
    """
    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 = root.attrs.get("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) -> 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.

Returns:

Type Description
str

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

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,
) -> 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.

    Returns
    -------
    str
        Path to the written label group (``image_store/labels/<name>``).
    """
    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,
    )

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

Returns:

Type Description
str

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

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,
) -> 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).

    Returns
    -------
    str
        Path to the label group (``image_store/labels/<name>``).
    """
    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")
    grp.attrs["image-label"] = {"version": _NGFF_VERSION}

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