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
        names = _inner_label_names(image)
        if not names:
            logger.warning(
                "%s has no label images under labels/, so nothing was "
                "overlaid. Pass labels=<path> explicitly if they live "
                "somewhere else.",
                image,
            )
        else:
            logger.info(
                "auto-loading %d label image(s) from %s/labels: %s",
                len(names),
                image,
                ", ".join(names),
            )
        loaded = 0
        for name in names:
            store = f"{image}/labels/{name}"
            # Guarded per label: without this, one bad label group raised
            # *after* the image had been added, so the viewer opened showing
            # the image alone and every remaining label was skipped -- which
            # looks exactly like "there were no labels".
            try:
                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,
                )
            except Exception:
                logger.exception(
                    "could not add labels/%s as a layer; skipping it and "
                    "continuing with the rest.",
                    name,
                )
                continue
            loaded += 1
            logger.info("auto-loaded labels/%s from %s", name, image)
        if names and not loaded:
            logger.error(
                "found %d label image(s) in %s/labels but none could be "
                "added -- see the errors above.",
                len(names),
                image,
            )

    if show:
        napari.run()
    return viewer