spacr.napari_bridge

Workflow inputs and outputs

Napari Bridge

Send the matching image/labels to napari and import the revised labels back with field identity preserved.

Open: Make Masks → Napari Bridge.

Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.

Inputs

  • Microscope images — Source image folder; original files, supported vendor files or imported TIFFs.

  • Label masks — masks/ when retained, or explicitly saved image/mask pairs. Intermediate masks may be removed by cleanup.

Outputs

  • Label masks — masks/ when retained, or explicitly saved image/mask pairs. Intermediate masks may be removed by cleanup.

API reference.

Module tutorial.

A18 — hand a field to napari, take the corrected mask back.

spaCR has its own brush now (spacr.curation, spacr.qt.layer_viewer, spacr.qt.curation_tool): a world-space brush over a labels layer, with track curation beside it and an append-only ledger under both. This module is not a replacement for any of that. It exists because a great many people already have napari muscle memory — the fill tool, the polygon, the keybindings, their own plugins — and spaCR has no business insisting they learn a second brush to fix four cells.

So: a mask goes out to napari, the user corrects it there, and it comes back.

Round-trip fidelity is the whole feature

A bridge that returns approximately the mask is worse than no bridge, so the rules are stated rather than assumed, and each is a test:

  • Label values survive exactly. Label 41 comes back as 41 and not as 1, not renumbered, not relabelled by connectivity. to_spacr_mask() casts back to spaCR’s uint16 mask convention (spacr.mask_io.save_mask()) and refuses a label that would not fit rather than letting it wrap — np.uint16(70000) is 4464, silently, and a silently renamed cell is the worst thing this module could do.

  • Orientation survives exactly. A napari 2-D layer’s array axes are (row, column), which is numpy’s order and spaCR’s order, so the correct amount of transposing is none. That sounds too obvious to test until you meet a viewer that displays (x, y) and someone “fixes” it with a .T that is invisible on the square test image everybody uses.

  • The shape may not change. napari’s brush cannot change an array’s shape, but a caller handing back the wrong layer can, and that is a mistake worth a refusal rather than a resize.

Corrections are recorded, the same way as spaCR’s own

Every write-back appends to the artefact’s spacr.curation.CurationLog — the same sidecar the brush writes, in the same append-only form. That is the rule spacr.curation exists for, and it does not stop applying because the editing happened in another window: a hand-edited mask that looks exactly like a segmented one is a reproducibility hole no matter which program did the painting. spacr.curation.is_curated() answers “was this touched?” for a napari correction exactly as it does for a brush stroke.

napari is optional, and is never imported at module scope

napari is declared in the napari extra, never in the core dependencies, and every import of it here is inside the function that needs it. Two separate reasons, both load-bearing:

  • it pulls a second Qt stack, and this module is imported by a settings panel inside spaCR’s own PySide6 application;

  • the missing-dependency path must print pip install "spacr[napari]", not a traceback from six frames inside somebody’s import machinery. require_napari() is that path, following spacr.qt._QT_MISSING_MESSAGE and spacr.ome_zarr.require_zarr().

Everything except require_napari(), open_in_napari() and run_event_loop() works with no napari installed at all, which is also how the fidelity tests run: layer_specs() and labels_from_viewer() speak to a duck-typed viewer, so the conversion either side of napari is exercised for real without one.

Attributes

Exceptions

MaskFidelityError

A mask came back in a shape this module will not silently accept.

NapariExtraMissing

napari is not installed.

Classes

CorrectionResult

What one round trip changed, and where it was recorded.

MaskHandoff

One field on its way to napari, and the mask's way home.

Functions

add_to_viewer(→ Tuple[Any, ...])

Add the field's layers to viewer. Returns the layers it made.

correct_mask(→ CorrectionResult)

The whole round trip: open in napari, wait, take the mask back.

labels_from_viewer(→ numpy.ndarray)

Take the corrected labels back out of a viewer.

layer_specs(→ Tuple[Dict[str, Any], ...])

What napari should be asked to add, as plain dictionaries.

load_handoff() → MaskHandoff)

Read a field's mask, and optionally its image, ready for napari.

missing_napari_message(→ str)

The install instruction, naming the module that was actually missing.

napari_available(→ bool)

Whether napari can be imported. Never raises.

open_in_napari(→ Any)

Open the field in napari and return the viewer.

read_image(→ numpy.ndarray)

Read an image file for display beside the mask.

require_napari(→ Any)

Import and return napari, or raise a message worth reading.

run_event_loop(→ None)

Block until the napari window is closed. Headless callers only.

to_spacr_mask(→ numpy.ndarray)

Convert an array back to spaCR's mask convention, or refuse.

write_back(→ CorrectionResult)

Write a corrected mask the way spaCR writes masks, and record it.

Module Contents

exception spacr.napari_bridge.MaskFidelityError[source]

Bases: ValueError

A mask came back in a shape this module will not silently accept.

Raised rather than corrected. Every case it covers — a label too large for uint16, a negative label, a changed array shape, fractional values — is one where “doing something sensible” means quietly writing a different mask than the user drew.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.napari_bridge.NapariExtraMissing[source]

Bases: ImportError

napari is not installed.

An ImportError subclass, so a caller already guarding with except ImportError keeps working and the actionable message — not a traceback from inside somebody else’s import machinery — is what reaches the user.

Initialize self. See help(type(self)) for accurate signature.

class spacr.napari_bridge.CorrectionResult[source]

What one round trip changed, and where it was recorded.

Parameters:
  • mask_path – the file that was written, or would have been.

  • mask – the corrected mask, in spaCR’s convention.

  • changed_pixels – how many elements differ from what was there.

  • added – labels present now that were not before.

  • removed – labels that were there and are gone.

  • altered – labels present in both whose pixels moved.

  • log_path – the curation ledger that was appended to, or "".

  • edit – the spacr.curation.CurationEdit that was appended.

  • written – whether the mask file was actually rewritten. False for a round trip that changed nothing — see write_back().

__bool__() → bool[source]

True when the mask came back different.

describe() → str[source]

One line, for a status bar and for the screen’s log.

property touched: Tuple[int, ...][source]

Every label the correction affected, sorted.

class spacr.napari_bridge.MaskHandoff[source]

One field on its way to napari, and the mask’s way home.

Parameters:
  • mask – the label array, uint16, exactly as it is on disk.

  • mask_path – where it came from, and where a correction is written back. Also what the curation ledger is named after.

  • image – the image to show under it, or None.

  • image_path – where that came from.

  • name – the labels layer’s name in napari, and what is looked for on the way back.

  • scale – per-axis world scale handed to napari, when the field is calibrated. Empty means one world unit per pixel.

describe() → str[source]

One line for a status bar.

property curated: bool[source]

Whether this mask already carries a curation ledger with edits.

property labels: Tuple[int, ...][source]

Every non-zero label present, sorted.

spacr.napari_bridge.add_to_viewer(viewer: Any, handoff: MaskHandoff) → Tuple[Any, ...][source]

Add the field’s layers to viewer. Returns the layers it made.

viewer is duck-typed on purpose: anything with add_image and add_labels will do, which is what lets the round trip be tested for real without napari installed.

Parameters:
  • viewer – a napari.Viewer, or anything shaped like one.

  • handoff – the field.

spacr.napari_bridge.correct_mask(mask_path: Any, image_path: Any = '', *, viewer: Any = None, block: bool = True, write: bool = True, name: str = LABELS_LAYER_NAME) → CorrectionResult[source]

The whole round trip: open in napari, wait, take the mask back.

For a script or a notebook. Not for use from inside spaCR’s own Qt application with block=True — see run_event_loop(); the GUI screen calls open_in_napari() and labels_from_viewer() separately, driven by the user.

Parameters:
  • mask_path – the mask to correct. The corrected one is written back here, and the ledger beside it.

  • image_path – the image to show under it.

  • viewer – an existing viewer, instead of opening one.

  • block – run the napari event loop and return when the window closes. False returns as soon as the viewer is open, which is only useful when the caller drives the loop itself.

  • write – passed to write_back().

  • name – the labels layer’s name, on the way out and back.

Returns:

a CorrectionResult.

Raises:

NapariExtraMissing – when napari is not installed.

spacr.napari_bridge.labels_from_viewer(viewer: Any, *, name: str = LABELS_LAYER_NAME) → numpy.ndarray[source]

Take the corrected labels back out of a viewer.

Parameters:
  • viewer – a napari.Viewer, or anything whose layers are iterable and carry name and data.

  • name – which layer to take. Falls back to the only labels-shaped layer when the name is not found, because a user who renamed the layer has not thereby thrown their work away.

Returns:

the layer’s array, converted with to_spacr_mask().

Raises:

MaskFidelityError – when there is no such layer, or when what came back is not a mask spaCR can write.

spacr.napari_bridge.layer_specs(handoff: MaskHandoff) → Tuple[Dict[str, Any], ...][source]

What napari should be asked to add, as plain dictionaries.

Separated from the call that adds them so the contents of the handoff — which array, under which name, at which scale, with no axis reordering — can be asserted with no napari installed. Each dict carries a "kind" naming the add_* method it belongs to; everything else is keyword arguments for it.

Parameters:

handoff – the field.

Returns:

the image spec (when there is an image) then the labels spec.

spacr.napari_bridge.load_handoff(mask_path: Any, image_path: Any = '', *, name: str = LABELS_LAYER_NAME, scale: Sequence[float] = ()) → MaskHandoff[source]

Read a field’s mask, and optionally its image, ready for napari.

The mask is read with spacr.mask_io.load_mask(), which is spaCR’s own reader and already probes .tif / .tiff / .npy for a bare stem. Reading it any other way here would be the first place the round trip could start losing.

Parameters:
  • mask_path – the label mask.

  • image_path – the image to show under it. Optional.

  • name – the labels layer’s name.

  • scale – per-axis world scale.

Returns:

a MaskHandoff.

Raises:

FileNotFoundError – when the mask is not there.

spacr.napari_bridge.missing_napari_message(module: str = 'napari') → str[source]

The install instruction, naming the module that was actually missing.

spacr.napari_bridge.napari_available() → bool[source]

Whether napari can be imported. Never raises.

For a screen that wants to grey a button out rather than let the user press it and read a paragraph.

spacr.napari_bridge.open_in_napari(handoff: MaskHandoff, *, viewer: Any = None, title: str = '') → Any[source]

Open the field in napari and return the viewer.

Does not start an event loop: see run_event_loop() for why that is a separate decision.

Parameters:
  • handoff – the field.

  • viewer – an existing viewer to add to, instead of making one.

  • title – the window title. Defaults to the mask’s filename.

Returns:

the viewer.

Raises:

NapariExtraMissing – when napari is not installed.

spacr.napari_bridge.read_image(path: Any) → numpy.ndarray[source]

Read an image file for display beside the mask.

TIFFs go through tifffile so a 16-bit field keeps its bit depth, .npy through numpy, everything else through Pillow — the same three branches, in the same order, as spacr.qt.widgets.live_preview.load_preview_image(), written here because this module must not import anything from spacr.qt.

Parameters:

path – the file.

Returns:

the array exactly as stored. No rescaling and no reordering: napari is being handed the data, not a picture of it.

Raises:

FileNotFoundError – when there is nothing there.

spacr.napari_bridge.require_napari() → Any[source]

Import and return napari, or raise a message worth reading.

The only place this module imports napari. See the module docstring for why that matters more here than in most optional-dependency code.

Returns:

the imported napari module.

Raises:

NapariExtraMissing – when the extra is not installed.

spacr.napari_bridge.run_event_loop() → None[source]

Block until the napari window is closed. Headless callers only.

Deliberately its own function rather than a flag on open_in_napari(), because whether it may be called depends on where the caller is, and getting it wrong is not a small mistake:

  • from a script or a notebook it is what makes correct_mask mean “correct it, then take it back”;

  • from inside spaCR’s own Qt application it must never be called — there is already a running QApplication event loop, and starting a second one nests them. The GUI screen therefore opens the viewer and lets the user press “Take the mask back” when they are done, which is also the friendlier interaction.

Raises:

NapariExtraMissing – when napari is not installed.

spacr.napari_bridge.to_spacr_mask(data: Any) → numpy.ndarray[source]

Convert an array back to spaCR’s mask convention, or refuse.

The fidelity guarantee, in one function. It casts to MASK_DTYPE and does nothing else: no transpose, no flip, no relabelling, no renumbering. A napari 2-D layer’s axes are (row, column), the same order numpy and spaCR use, so any reordering here would be an invented one.

Parameters:

data – whatever came out of the labels layer.

Returns:

a uint16 array with the same shape and the same label values.

Raises:

MaskFidelityError – for a negative label, a fractional value, a label that does not fit in uint16, or an array that is not 2-D or 3-D. Each is refused rather than repaired, because every repair would silently change which pixel belongs to which cell.

spacr.napari_bridge.write_back(mask_path: Any, corrected: Any, *, original: numpy.ndarray | None = None, source: str = LOG_SOURCE, extra: Mapping[str, Any] | None = None, write: bool = True) → CorrectionResult[source]

Write a corrected mask the way spaCR writes masks, and record it.

Two halves, and both are the point.

The mask goes through spacr.mask_io.save_mask(), which is the one place spaCR decides what a mask file is — uint16, LZW-compressed TIFF by default, .npy when the path or SPACR_MASK_FORMAT says so. A corrected mask written any other way would be a mask the rest of the pipeline reads differently from the one segmentation produced.

The correction goes into the artefact’s spacr.curation.CurationLog, appended, never rewritten, so spacr.curation.is_curated() tells a curated dataset from a raw one whether the editing happened in spaCR’s brush or in napari.

A round trip that changed nothing writes nothing and records nothing. That is the same rule spacr.curation.MaskCuration.end_stroke() applies to a stroke that moved no pixels: a ledger padded with no-op entries is one nobody reads, and rewriting the file would move its mtime and make every downstream artifact look stale for no reason.

Parameters:
  • mask_path – where the mask lives. The ledger is written beside it.

  • corrected – the corrected labels, from napari or anywhere.

  • original – what it was before. Read from mask_path when not given, so the diff is against what is actually on disk.

  • source – what to stamp a new ledger with. An existing ledger keeps its own; the tool that made each edit is recorded on the edit.

  • extra – anything else to record on the ledger entry.

  • write – False computes and records nothing, returning the diff only — for a preview.

Returns:

a CorrectionResult.

Raises:

MaskFidelityError – when the mask cannot be written faithfully, or when its shape changed.

spacr.napari_bridge.NAPARI_MISSING_MESSAGE = Multiline-String[source]
Show Value
"""Correcting a mask in napari needs the optional `napari` extra, which is not
installed in this environment (missing module: {module}).

Install it with:

    python -m pip install "spacr[napari]"

You do not need it to correct masks: spaCR's own Curate screen has a brush, a
label picker and track curation, and records the same ledger. This bridge is
for people who would rather work in napari."""