spacr.qt.layer_viewer¶
Workflow inputs and outputs¶
Layer Viewer¶
Inspect aligned image, label and point/ROI layers for the selected field.
Open: QC → Layer Viewer.
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
Figures and table exports — The output location chosen by the tool; exports describe the selected data and filters.
The Qt view over spacr.layers — a napari-style layer viewer.
Everything that decides what a pixel ends up being lives in
spacr.layers, which knows nothing about Qt. This module is the part
that can only exist with a display: a canvas that paints the model’s composite,
a layer list that reorders it, and the controls that set the properties the
model already knows how to honour.
The split is not tidiness. Five later features build on the layer model — ROI
shapes read by Measure, a counting points layer, a label brush with timelapse
track curation, orthogonal views with dimension sliders, and a synchronised
comparison grid — and every one of them is a change to the model with a small
widget on top. Putting the compositing rules in a paintEvent would make
all five untestable without a running QApplication.
Why this canvas paints rather than reusing _ZoomView¶
spacr.qt.widgets.live_preview._ZoomView is the right answer when the
thing being shown is a finished pixmap: it scales what it was given. Here the
zoom is a property of the world, not of the picture — zooming in means
spacr.layers.Canvas.zoomed() and a fresh render at the new resolution,
which is how the labels layer stays crisp at 8× and how the orthogonal-view
item gets its slice sliders for free. So the canvas owns a
Canvas and paints; the array→pixmap step still goes
through numpy_to_qpixmap(), and file
loading through load_preview_image(),
rather than growing a third copy of either.
Clicking an object¶
Clicking a labels layer picks the object under the cursor, and — if the layer
was told which field it segments — publishes it through
spacr.qt.linked_selection, so the same cell lights up in the UMAP, the
plate view and the annotation grid. Double-clicking asks for it to be opened.
Classes¶
Something that borrows the canvas's mouse: an ROI pen, a counter. |
|
Paints a |
|
The stack as a list, top layer first — the order the user sees it in. |
|
Canvas, layer list and per-layer controls over one |
Functions¶
|
Build the viewer as an app screen. The |
|
Register the screens built on this viewer's world. Idempotent. |
|
Put the viewer in the app registry, through the public seam. Idempotent. |
|
Build a stack from an image file and/or a label-mask file. |
Module Contents¶
- class spacr.qt.layer_viewer.CanvasTool[source]¶
Something that borrows the canvas’s mouse: an ROI pen, a counter.
Two features need a click to mean something other than “select the object under the cursor” — drawing a polygon vertex (
spacr.qt.roi_tool) and dropping a counted marker (spacr.qt.counting_tool) — and both need it in world coordinates, not widget pixels, so that a point placed at 8× zoom lands where the same point placed at 1× does.Every handler is given the world position the canvas has already resolved and returns
Truewhen it consumed the event. ReturningFalse(the default for every method here) leaves the canvas doing exactly what it did before tools existed, which is what makes attaching one reversible.Subclass and override what you need; the base class is inert, so a tool that only wants clicks does not have to implement four no-ops.
- double_click(view: LayerCanvas, world: Dict[str, float], event: Any) bool[source]¶
A double click at
world— how a polygon is closed.- Parameters:
view – the
LayerCanvasthe tool is attached to.world – world position already resolved from the cursor, as an axis-name to coordinate dict that includes the pinned depth axes.
event – the Qt mouse event, passed through unchanged.
- Returns:
Trueto consume the double click; the base returnsFalse.
- key(view: LayerCanvas, event: Any) bool[source]¶
A key was pressed while the canvas had focus.
- Parameters:
view – the
LayerCanvasthe tool is attached to.event – the Qt key event, passed through unchanged.
- Returns:
Trueto consume the key; the base returnsFalseso the canvas’s default handling runs.
- move(view: LayerCanvas, world: Dict[str, float], event: Any) bool[source]¶
The cursor moved to
worldwith no drag in progress.- Parameters:
view – the
LayerCanvasthe tool is attached to.world – world position already resolved from the cursor, as an axis-name to coordinate dict that includes the pinned depth axes.
event – the Qt mouse event, passed through unchanged.
- Returns:
Trueto consume the move; the base returnsFalse.
- press(view: LayerCanvas, world: Dict[str, float], event: Any) bool[source]¶
A mouse button went down at
world. Return True to consume it.- Parameters:
view – the
LayerCanvasthe tool is attached to.world – world position already resolved from the cursor, as an axis-name to coordinate dict that includes the pinned depth axes.
event – the Qt mouse event, passed through unchanged; a tool can read its button and modifiers.
- release(view: LayerCanvas, world: Dict[str, float], event: Any) bool[source]¶
The mouse button came up at
world— the end of a drag.What turns a drag into ONE action. A brush stroke is dozens of
move()calls and exactly one thing the user did, so undo has to take back the stroke rather than the last few pixels of it; without a release the tool cannot tell where one stroke ends and the next begins. Inert by default, like the rest of this class, so a tool that only wants clicks is unaffected.- Parameters:
view – the
LayerCanvasthe tool is attached to.world – world position already resolved from the cursor, as an axis-name to coordinate dict that includes the pinned depth axes.
event – the Qt mouse event, passed through unchanged.
- class spacr.qt.layer_viewer.LayerCanvas(stack: spacr.layers.LayerStack | None = None, parent=None)[source]¶
Bases:
PySide6.QtWidgets.QFramePaints a
LayerStackthrough a world window.Wheel zooms about the cursor, dragging pans, and every change re-renders at the widget’s own resolution rather than scaling a stale pixmap.
- Parameters:
stack – the layers to paint.
Nonebuilds an EMPTY stack rather than leaving the canvas without one, so every later call has something to act on instead of guarding for None.parent – parent widget.
Build the canvas over one layer stack.
- Parameters:
stack – the layers to paint.
parent – parent widget.
- keyPressEvent(event) None[source]¶
Offer the key to the active tool before the default handling.
- Parameters:
event – the Qt key event.
- mouseDoubleClickEvent(event) None[source]¶
Hand the double-click to the active tool.
- Parameters:
event – the Qt mouse event.
- mouseMoveEvent(event) None[source]¶
Continue a pan, or hand the move to the active tool.
- Parameters:
event – the Qt mouse event.
- mousePressEvent(event) None[source]¶
Begin a pan, or hand the press to the active tool.
- Parameters:
event – the Qt mouse event.
- mouseReleaseEvent(event) None[source]¶
End a pan, or hand the release to the active tool.
- Parameters:
event – the Qt mouse event.
- paintEvent(event) None[source]¶
Draw every visible layer through the current world window.
- Parameters:
event – the Qt paint event.
- set_plane(axes: Sequence[str], depth: Dict[str, float] | None = None) None[source]¶
Look at a different plane — the seam the orthogonal-view item uses.
- Parameters:
axes – world-axis names for the canvas rows and columns, in that order; only the first two items are used.
- set_stack(stack: spacr.layers.LayerStack) None[source]¶
Show a different stack, unsubscribing from the old one.
The unsubscribe matters: the model holds listeners by strong reference, so a canvas that swapped stacks without letting go would keep repainting for a stack nobody is looking at.
- Parameters:
stack – the
LayerStackto display; the canvas subscribes to its layer changes.
- set_tool(tool: CanvasTool | None) CanvasTool | None[source]¶
Attach a tool (or
Noneto go back to picking); returns the old one.Keyboard focus is granted only while a tool is attached: a tool typically wants Escape and Backspace, and a canvas that grabbed focus the rest of the time would swallow the arrow keys the surrounding screen uses.
- Parameters:
tool – the
CanvasToolto receive mouse and key events, orNone. A different tool already attached is detached first.
- wheelEvent(event) None[source]¶
Zoom the world window about the pointer.
ABOUT THE POINTER, not the centre, so zooming toward something keeps it under the cursor – which is the only way to navigate a large field without losing the thing you were looking at.
- Parameters:
event – the Qt wheel event.
- property canvas: spacr.layers.Canvas | None[source]¶
The world window being shown, or
Nonefor an empty stack.
- property stack: spacr.layers.LayerStack[source]¶
The layer stack this canvas paints.
- Returns:
the stack.
- property tool: CanvasTool | None[source]¶
The
CanvasToolcurrently borrowing the mouse, if any.
- class spacr.qt.layer_viewer.LayerListWidget(stack: spacr.layers.LayerStack, parent=None)[source]¶
Bases:
PySide6.QtWidgets.QListWidgetThe stack as a list, top layer first — the order the user sees it in.
Reversed on purpose: the model’s index 0 is the bottom layer (a stack of acetates), while a list reads top-down, so the topmost row is the layer nearest the viewer. Every drag and every button here is translated back into a model index, so the model stays the single source of order.
- Parameters:
stack – the layers to list. Required here, unlike the canvas: this widget IS the stack’s presentation and has nothing to show without one.
parent – parent widget.
Build the list over one layer stack.
- Parameters:
stack – the layers to list.
parent – parent widget.
- class spacr.qt.layer_viewer.LayerViewer(stack: spacr.layers.LayerStack | None = None, parent=None)[source]¶
Bases:
spacr.qt.linked_selection.LinkedView,PySide6.QtWidgets.QWidgetCanvas, layer list and per-layer controls over one
LayerStack.- Parameters:
stack – the layers to show. Handed to the canvas and the list, so the three share one stack rather than three copies of it.
parent – parent widget.
Build the viewer: canvas, layer list and per-layer controls.
- Parameters:
stack – the layers to show, or None for an empty one.
parent – parent widget.
- add_image_file(path) spacr.layers.ImageLayer | None[source]¶
Load
pathas an image layer. Returns it, orNoneon failure.- Parameters:
path – image file read through
stack_from_paths(); a load error is logged and shown in the status line.
- add_labels_file(path, field: spacr.layers.FieldKey | None = None) spacr.layers.LabelsLayer | None[source]¶
Load
pathas a labels layer.- Parameters:
path – label-mask file read through
stack_from_paths(); returnsNoneafter logging and showing a load error.
- closeEvent(event) None[source]¶
Unlink the selection and unsubscribe from the stack before going.
BOTH, and both matter: a linked view outliving its window is a broadcast to a dead widget, and a live subscription on the stack is a callback into one. Either is a crash rather than a leak.
- Parameters:
event – the Qt close event.
- on_linked_selection_changed(selection) None[source]¶
Highlight the object another view selected, when we hold it.
- Parameters:
selection – the published
Selection; only a selection of exactly one key is acted on, and that key is matched against each labels layer’s object keys.
- property stack: spacr.layers.LayerStack[source]¶
The layer stack this viewer shows.
- Returns:
the stack.
- spacr.qt.layer_viewer.make_layer_viewer_screen(**_kwargs) LayerViewer[source]¶
Build the viewer as an app screen. The
factory=forregister_app().
- spacr.qt.layer_viewer.register_companion_apps() tuple[source]¶
Register the screens built on this viewer’s world. Idempotent.
spacr.qt.appholds the one import-time table of self-registering modules (_SELF_REGISTERING_APPS) and callsregister_layer_viewer_app()out of it. The screens inCOMPANION_APPSgrew out of this module: they either borrow the layer world directly (aCanvasToolonLayerCanvas) or join the same linked-selection contract this viewer joined. Registering them from here rather than giving each one a row inapp.pykeeps the chain one hop long and written down in a single tuple — the next screen adds a line to it, not a mechanism.One companion’s failure costs that companion and nothing else, the same posture
app.pytakes towards its own table and towards plugins: an optional screen must never stop the window opening.- Returns:
the module names that registered without raising.
- spacr.qt.layer_viewer.register_layer_viewer_app(*, section: str | None = None, stage: str | None = None, key: str = LAYER_VIEWER_APP_KEY)[source]¶
Put the viewer in the app registry, through the public seam. Idempotent.
Called at import from the bottom of
spacr.qt.app, which is the only place a registration is visible to everybody — see_SELF_REGISTERING_APPSthere for why it cannot be called at the top of this module.The row itself – the key, the name, the blurb, the section, the “no headless run” sentence, the API doc link and the nine translations of the display name – is declared in
spacr.qt.app_catalog.spacr.qt.app.register_app()distributes those into the four tables each used to need a hand-edit in, and this function’s whole job is to name which row. That is what lets the app be registered without importing this module at all: the launch reads the table, and the screen is imported when somebody opens it.- Returns:
the registry row that was added, or
Nonewhen the key was already registered. Safe to call twice: this module is reachable from three import paths and a duplicate key would otherwise raise.
- spacr.qt.layer_viewer.stack_from_paths(image_path=None, labels_path=None, *, spacing: spacr.layers.Spacing | None = None, field: spacr.layers.FieldKey | None = None) spacr.layers.LayerStack[source]¶
Build a stack from an image file and/or a label-mask file.
Loading goes through
spacr.qt.widgets.live_preview.load_preview_image(), which already knows to read TIFFs withtifffileso a 16-bit field keeps its bit depth, and channel order throughspacr.qt.widgets.timelapse_preview.frame_channel(), which resolves channels-first against channels-last the same way the timelapse preview does. Neither is re-implemented here.- Parameters:
spacing – the world spacing both layers share. Defaults to one world unit per pixel — correct for a single field, and the thing to override the moment a z-stack or a µm-calibrated mosaic is involved.