Skip to content

napari viewer plugin

Open an OME-ZARR image and overlay the labels produced by tile_process as a napari Labels layer in a single call. Requires the optional napari extra (pip install "patchworks[napari]").

patchworks.plugins.napari.view_in_napari(image: Union[da.Array, str, Path], labels: Union[da.Array, str, Path, None] = None, *, channel: int | None = None, labels_component: str = 'labels', image_name: str = 'image', labels_name: str = 'labels', glasbey: bool = True, show: bool = True, **add_image_kwargs: Any)

Open image in napari and overlay labels as a Labels layer.

Parameters:

Name Type Description Default
image (Array, str or Path)

OME-ZARR store (multi-scale aware), any bioio-readable file, or an in-memory array.

required
labels (Array, str, Path or None)

Label array to overlay. A plain .zarr store written by tile_process is read from its labels_component; an OME-ZARR pyramid is shown multi-scale. None (default) auto-loads every label image stored inside the OME-ZARR under labels/<name>/ — the place tile_process writes them by default — each as its own Labels layer. (Falls back to image-only if there are none.)

None
channel int or None

Channel to display from the image. None (default) shows every channel — the segmentation channel doesn't need to match what you view. Pass an int to show just that one.

None
labels_component str

Array name inside a plain-zarr label store (default "labels", matching tile_process's output_component).

'labels'
image_name str

Layer names shown in napari.

'image'
labels_name str

Layer names shown in napari.

'image'
glasbey bool

Colour the labels with a glasbey palette (many distinct, high-contrast colours, tuned to read on the dark canvas) instead of napari's default. Default True. Needs the glasbey package (ships with patchworks[napari]).

True
show bool

Start the napari event loop (blocking). Set False in scripts/tests that manage the loop themselves.

True
**add_image_kwargs Any

Extra keyword arguments forwarded to viewer.add_image (e.g. colormap, contrast_limits).

{}

Returns:

Type Description
Viewer

The viewer instance (useful when show=False).

Notes

In 3-D view (the cube icon, or viewer.dims.ndisplay = 3), napari always shows the coarsest pyramid level — there is no automatic zoom-based switching in 3-D, only in 2-D. To pin a specific resolution (needs napari>=0.7.1)::

viewer.layers["labels"].locked_data_level = 0   # full resolution
viewer.layers["labels"].locked_data_level = None  # back to coarsest

A widget for this is also in the layer controls panel in napari>=0.7.1.

Examples:

>>> view_in_napari("scan.zarr")  # auto-loads scan.zarr/labels/*
Source code in src/patchworks/plugins/napari.py
def view_in_napari(
    image: Union[da.Array, str, Path],
    labels: Union[da.Array, str, Path, None] = None,
    *,
    channel: int | None = None,
    labels_component: str = "labels",
    image_name: str = "image",
    labels_name: str = "labels",
    glasbey: bool = True,
    show: bool = True,
    **add_image_kwargs: Any,
):
    """Open *image* in napari and overlay *labels* as a Labels layer.

    Parameters
    ----------
    image : da.Array, str or Path
        OME-ZARR store (multi-scale aware), any bioio-readable file, or an
        in-memory array.
    labels : da.Array, str, Path or None
        Label array to overlay. A plain ``.zarr`` store written by
        ``tile_process`` is read from its ``labels_component``; an OME-ZARR
        pyramid is shown multi-scale. ``None`` (default) **auto-loads** every
        label image stored inside the OME-ZARR under ``labels/<name>/`` — the
        place ``tile_process`` writes them by default — each as its own Labels
        layer. (Falls back to image-only if there are none.)
    channel : int or None, optional
        Channel to display from the image. ``None`` (default) shows every
        channel — the segmentation channel doesn't need to match what you
        view. Pass an int to show just that one.
    labels_component : str, optional
        Array name inside a plain-zarr label store (default ``"labels"``,
        matching ``tile_process``'s ``output_component``).
    image_name, labels_name : str, optional
        Layer names shown in napari.
    glasbey : bool, optional
        Colour the labels with a glasbey palette (many distinct, high-contrast
        colours, tuned to read on the dark canvas) instead of napari's default.
        Default ``True``. Needs the ``glasbey`` package (ships with
        ``patchworks[napari]``).
    show : bool, optional
        Start the napari event loop (blocking). Set ``False`` in scripts/tests
        that manage the loop themselves.
    **add_image_kwargs
        Extra keyword arguments forwarded to ``viewer.add_image``
        (e.g. ``colormap``, ``contrast_limits``).

    Returns
    -------
    napari.Viewer
        The viewer instance (useful when ``show=False``).

    Notes
    -----
    In 3-D view (the cube icon, or ``viewer.dims.ndisplay = 3``), napari
    always shows the **coarsest** pyramid level — there is no automatic
    zoom-based switching in 3-D, only in 2-D. To pin a specific resolution
    (needs ``napari>=0.7.1``)::

        viewer.layers["labels"].locked_data_level = 0   # full resolution
        viewer.layers["labels"].locked_data_level = None  # back to coarsest

    A widget for this is also in the layer controls panel in napari>=0.7.1.

    Examples
    --------
    >>> view_in_napari("scan.zarr")  # auto-loads scan.zarr/labels/*  # doctest: +SKIP
    """
    napari = _require_napari()

    img = _resolve_image(image, channel)
    img_ndim = img[0].ndim if isinstance(img, list) else img.ndim
    img_scale, img_units = (
        _pyramid_calibration(image, img_ndim)
        if _is_zarr(image)
        else (None, None)
    )
    viewer = napari.Viewer()
    viewer.add_image(
        img,
        name=image_name,
        multiscale=isinstance(img, list),
        scale=img_scale,
        units=img_units,
        **add_image_kwargs,
    )

    label_kwargs: dict[str, Any] = {}
    if glasbey:
        try:
            import glasbey as _glasbey
            import numpy as np
            from napari.utils.colormaps import CyclicLabelColormap

            # glasbey palette (biased lighter so colours read on the dark
            # canvas), wrapped in a CyclicLabelColormap so each label value
            # cycles through a distinct colour. Passing the raw palette list
            # makes napari map large label IDs past the end -> one flat colour.
            palette = _glasbey.create_palette(256, lightness_bounds=(40, 100))
            colors = np.array(
                [
                    [
                        int(h[1:3], 16) / 255,
                        int(h[3:5], 16) / 255,
                        int(h[5:7], 16) / 255,
                        1.0,
                    ]
                    for h in palette
                ]
            )
            label_kwargs["colormap"] = CyclicLabelColormap(colors=colors)
        except ImportError:
            logger.warning(
                "glasbey not installed; using napari's default label colours "
                "(pip install glasbey, or it ships with patchworks[napari])."
            )

    if labels is not None:
        lab = _resolve_labels(labels, labels_component)
        lab_ndim = lab[0].ndim if isinstance(lab, list) else lab.ndim
        lab_scale, lab_units = (
            _pyramid_calibration(labels, lab_ndim)
            if _is_zarr(labels)
            else (None, None)
        )
        metadata = _label_hint(labels) if _is_zarr(labels) else {}
        viewer.add_labels(
            lab,
            name=labels_name,
            multiscale=isinstance(lab, list),
            scale=lab_scale,
            units=lab_units,
            metadata=metadata,
            **label_kwargs,
        )
    elif _is_zarr(image):
        # No labels given → auto-overlay every label image stored inside the
        # OME-ZARR under labels/<name>/ (the default place tile_process writes
        # them), each as its own multi-scale Labels layer. Kept as a list (not
        # unwrapped to a single array) even for one level, so napari always
        # treats it as multiscale — required for 3D resolution switching, see
        # https://napari.org/stable/gallery/add_multiscale_volume.html
        for name in _inner_label_names(image):
            store = f"{image}/labels/{name}"
            levels = _multiscale_levels(store, None)
            lab = [lvl.astype("int32") for lvl in levels]
            lab_scale, lab_units = _pyramid_calibration(store, lab[0].ndim)
            viewer.add_labels(
                lab,
                name=name,
                multiscale=True,
                scale=lab_scale,
                units=lab_units,
                metadata=_label_hint(store),
                **label_kwargs,
            )
            logger.info("auto-loaded labels/%s from %s", name, image)

    if show:
        napari.run()
    return viewer