Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,18 @@ imaris = ["imaris-ims-file-reader"]
# napari enables the interactive viewer plugin.
# - napari >= 0.5: NumPy 2.0 compatible (older 0.4.x uses np.array(copy=False),
# which raises under NumPy 2 -> ValidationError on Viewer()).
# - numpy < 2.5: napari/numba don't support numpy 2.5 yet.
# - ipykernel < 7: napari-console requires ipykernel < 7.
# - lxml-html-clean: napari's notebook_display imports lxml.html.clean, split
# into a separate package in lxml >= 5.2 (else ImportError on Viewer()).
napari = ["napari[all]>=0.5.5", "lxml-html-clean"]
# - colorcet: glasbey label LUTs for view_in_napari.
napari = [
"napari[all]>=0.5.5",
"numpy<2.5",
"ipykernel<7",
"lxml-html-clean",
"colorcet",
]
# workflow runs the Snakemake pipeline (per-tile SLURM jobs across GPUs).
workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm"]
dev = ["pytest", "pytest-cov", "scikit-image", "psutil", "tqdm"]
Expand Down
58 changes: 56 additions & 2 deletions src/patchworks/plugins/napari.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,48 @@ def _resolve_labels(
return arr.astype("int32")


def _label_colormap(name: str | None):
"""Build a cyclic napari label colormap from a colorcet glasbey palette.

Parameters
----------
name : str or None
A ``colorcet`` palette attribute, e.g. ``"glasbey_dark"`` (glasbey on a
dark background). ``None`` falls back to napari's default label colours.

Returns
-------
napari.utils.colormaps.CyclicLabelColormap or None
The colormap to pass to ``add_labels``, or ``None`` for the default.
"""
if not name:
return None
try:
import colorcet
except ImportError:
logger.warning(
"colorcet not installed; using napari's default label colours "
"(pip install colorcet, or it ships with patchworks[napari])."
)
return None

palette = getattr(colorcet, name, None)
if palette is None:
logger.warning("colorcet has no palette %r; using default colours.", name)
return None

import numpy as np
from napari.utils.colormaps import CyclicLabelColormap

def _hex_to_rgba(h: str):
h = h.lstrip("#")
return (int(h[0:2], 16) / 255, int(h[2:4], 16) / 255,
int(h[4:6], 16) / 255, 1.0)

colors = np.array([_hex_to_rgba(c) for c in palette], dtype=float)
return CyclicLabelColormap(colors=colors)


def view_in_napari(
image: Union[da.Array, str, Path],
labels: Union[da.Array, str, Path, None] = None,
Expand All @@ -199,6 +241,7 @@ def view_in_napari(
labels_component: str = "labels",
image_name: str = "image",
labels_name: str = "labels",
label_colormap: str | None = "glasbey_dark",
show: bool = True,
**add_image_kwargs: Any,
):
Expand All @@ -223,6 +266,12 @@ def view_in_napari(
matching ``tile_process``'s ``output_component``).
image_name, labels_name : str, optional
Layer names shown in napari.
label_colormap : str or None, optional
``colorcet`` palette for the label LUT; default ``"glasbey_dark"``
(glasbey on a dark background — many distinct, high-contrast colours).
Any colorcet name works (e.g. ``"glasbey_light"``); ``None`` uses
napari's default label colours. Needs ``colorcet`` (ships with
``patchworks[napari]``).
show : bool, optional
Start the napari event loop (blocking). Set ``False`` in scripts/tests
that manage the loop themselves.
Expand Down Expand Up @@ -250,17 +299,22 @@ def view_in_napari(
**add_image_kwargs,
)

cmap = _label_colormap(label_colormap)
label_kwargs = {"colormap": cmap} if cmap is not None else {}

if labels is not None:
lab = _resolve_labels(labels, labels_component)
viewer.add_labels(lab, name=labels_name)
viewer.add_labels(lab, name=labels_name, **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.
for name in _inner_label_names(image):
levels = _multiscale_levels(f"{image}/labels/{name}", None)
lab = [lvl.astype("int32") for lvl in levels]
viewer.add_labels(lab if len(lab) > 1 else lab[0], name=name)
viewer.add_labels(
lab if len(lab) > 1 else lab[0], name=name, **label_kwargs
)
logger.info("auto-loaded labels/%s from %s", name, image)

if show:
Expand Down
Loading