spacr.qt.widgets.live_preview¶
Live-preview segmentation widget — v2.
Interactive Cellpose tuning surface for the Mask app screen. It provides:
Zoomable canvases (Ctrl+scroll, in sync). Both the original and the mask overlay live in a shared
QGraphicsViewpair — pan and zoom on one and the other tracks pixel-for-pixel.Hover tooltip. Move the cursor over the original and a pinned status line shows the pixel intensity for every channel plus, when present, the object label at that position from the last segmenta- tion. Same tooltip regardless of which view holds the cursor.
Normalise toggle. Optional 2–98 % percentile stretch (per channel for RGB) so raw low-contrast tiles are legible.
Model-aware options. Every model shows the full segmentation set. Cellpose-SAM does not ignore
flow_threshold,cellprobordiameter— seeDIAMETER_TOOLTIPfor the measurement that killed that belief.Outline colour + thickness. Chosen from the toolbar; effect is live once a mask exists.
color (random)assigns a stable categorical colour to every object label so touching masks remain distinguishable.Multi-object segmentation. An “object type” combo picks between
cell,nucleus, andcell + nucleus. In cell+nucleus mode the panel runs two Cellpose passes and overlays both masks in distinct colours.The model the RUN will use, and it says which. The panel reads the same setting the pipeline reads –
pathogen_modeloverpathogen_model_namefor pathogens,<object>_model_nameotherwise (_model_keys_for()) – offers the model zoo beside the combo, and names the model that produced the masks on the status line. A checkpoint that is not on this machine previews with cpsam AND SAYS SO rather than stalling or substituting in silence.Pre / Post filters. When the object type is
cell(or the combined mode) the panel routes pre / post-processing settings from the Mask app (cell_min_size,cell_max_size,remove_background_cell, background intensity, …) through the segmentation. Users toggle these on/off with dedicated “Pre” / “Post” clickable labels sitting next to “Run preview” in the same visual style as the LP / AI toggles.
The whole file stays safe to import without cellpose — every cellpose call is lazy-imported inside the worker thread.
Attributes¶
Where each bound control of a Cellpose compartment lives in |
|
How many of this session's masks the comparison panel can lay over one |
Classes¶
Interactive segmentation preview — Mask app only. |
|
Modal dialog that surfaces every live-preview setting. |
|
Everything the worker needs to run one segmentation pass. |
Functions¶
|
Return the first supported image at or below |
|
Read path into a nonempty (H, W) or (H, W, C) numeric array. |
|
Max-project a field's planes, the way the ingest already does. |
|
Discover, enumerate and decode one preview source. Data in, data out. |
|
Convert an (H, W) or (H, W, C) array to a |
|
The settings ROLE an object-dropdown caption stands for. |
|
The dropdown caption for organelle slot |
|
Legacy single-mask overlay retained for older imports. |
|
Return an RGB uint8 view of |
|
Return one vivid random RGB triple for the |
|
Colours |
Module Contents¶
- class spacr.qt.widgets.live_preview.LivePreviewPanel(parent=None, *, threaded: bool = True, module: str = '')[source]¶
Bases:
spacr.qt.widgets.preview_contract.LivePreviewContract,PySide6.QtWidgets.QWidgetInteractive segmentation preview — Mask app only.
The reference implementation of
LivePreviewContract: the other three live views wear the same run button, the same cancel button and the same words as this one.- Parameters:
parent – parent widget.
threaded – whether the panel’s jobs run off the GUI thread. False runs each one inline, emitting the same signals in the same order, so a test can drive the panel synchronously without the behaviour diverging.
Build the preview panel and arm it to accept dropped images.
- Parameters:
parent – parent widget, or
None.threaded – load images on a worker thread. Loads go through
JobRunnerrather than a hand-rolled thread because that is what registers the job with the process-wide run registry, which is the only thing the activity spinner watches.
- apply_settings(settings: dict)[source]¶
Seed the panel from a module’s settings, and cache the whole dict for the Pre / Post routes to read from.
This is the inverse of
settings_for_propagation()and is tested as one — the defect it was written for is that the two spoke different vocabularies. The panel emittedcell_diameterand read backdiameter, which Mask does not declare, so a Mask screen seeded here kept the panel’s own hardcoded 30 px, 0.4 flow and 0.0 probability whilecell_channelandnucleus_channelDID land — the preview visibly changed and looked seeded, having silently dropped exactly the three settings it is opened to check.Every field is copied independently. A single unusable value used to abort the whole copy through the shared
except, so one junk diameter also cost the flow threshold, the channels and the model.A retired
{object}_min_area-family bound insettingsis folded intoobject_filtersfirst, as a Mask run folds it, so the preview judges what the run judges.- Parameters:
settings – the module’s settings dict (
Noneis treated as empty); a copy is kept for the Pre and Post routes.
- cancel_preview() bool[source]¶
Cancel PSF work cooperatively and discard any native inference result.
- closeEvent(event)[source]¶
Cancel a load in progress rather than let it outlive the panel.
- Parameters:
event – the close event; passed to the base class after
shutdown().
- comparable_masks() List[Dict[str, Any]][source]¶
The session’s masks that fit the field on screen, oldest first.
- comparison_layers()[source]¶
What the comparison popup lists, top of the stack first.
The masks, newest on top at half opacity, then the field as it is shown at full opacity underneath, then each channel on its own, unticked, for a mask that is better judged against one plane.
- display_channel() int | None[source]¶
Channel index the canvases show, or
Nonefor all channels.The captions are translated —
All channelsreadsAlla kanaleron a Swedish screen — so what the shared reader is given is the entry as written, kept in the item’s data.
- dragEnterEvent(event)[source]¶
Accept the drag only if it carries a supported image file.
- Parameters:
event – the drag-enter event; its MIME data is checked for a local file URL with a supported image extension.
- dragMoveEvent(event)[source]¶
Keep accepting while droppable input stays over the panel.
- Parameters:
event – the Qt drag event.
- dropEvent(event)[source]¶
Load the dropped image into the preview.
- Parameters:
event – the drop event; the first local file URL with a supported image extension is loaded asynchronously, and the drop is ignored when there is none.
- load_image(path)[source]¶
Synchronously load one image.
Intended for explicit programmatic calls and tests, and for those only. Every GUI path — the drop handler, the FOV dropdown and the Choose-image dialog — goes through
load_source_async(), so that neither the decode nor the folder enumeration behind_refresh_source_selectorscan block the application thread. Three of them used to call this instead, which is what the docstring already claimed was not happening.- Parameters:
path – the image file to show; with MIP on, its field’s stack is max-projected instead. A failure is reported in the status line and gives
False.
- load_source_async(source, *, enumerate_sets: bool = True, display_plane: int | None = None) bool[source]¶
Discover and decode a file/folder source on a worker thread.
New requests supersede older ones by token. An old decoder is allowed to finish safely, but its result is ignored.
- Parameters:
source – direct supported image or directory containing images.
enumerate_sets –
Falsereuses the sampler’s cached listing instead of re-scanning. Seeload_source_payload().display_plane – channel plane selected by the table, if any.
- Returns:
Truewhen a worker was started.
- open_live_settings()[source]¶
Open (or focus) the Live Settings modal.
The dialog rehomes every hidden state widget into its form so the user’s edits go straight into
self._*— nothing to sync. On close, widgets are re-parented back toself(hidden again) so state persists across opens.
- open_mask_comparison() bool[source]¶
Ask what to compare, then draw it in the third panel.
- Returns:
whether a comparison was drawn.
- propagate_settings() None[source]¶
Send the current live settings to the main panel (if a callback is registered). Called on any live-settings change while the dialog’s Propagate toggle is on.
- refresh_model_choices() None[source]¶
Re-read the Cellpose model list and add anything new.
spacr.settings.cellpose_model_choicesonly reads the API when Cellpose is already imported, because importing it costs ~2.5 s and this panel is built while a page is being laid out. That means the first build usually gets the shipped fallback — so ask again every time the panel is shown. After the first segmentation Cellpose is loaded and a checkpoint the user registered appears here.Additive on purpose: the current selection is never disturbed, and an entry is never removed, so a value the user picked cannot vanish under them because a probe came back thinner.
- regroup_the_folder() bool[source]¶
Group the loaded folder again, by the naming the form names now.
The table is grouped when a folder is loaded, with the
metadata_typeandcustom_regexthe Mask form held at that moment. Loading first and choosing the naming second left every file under one column until something else reloaded the folder. The screen calls this when either setting changes. The folder’s file names are read off the GUI thread, and nothing is decoded.- Returns:
Truewhen a regrouping was started,Falsewhen no image is loaded.
- resizeEvent(event)[source]¶
Re-elide the path when the panel changes width.
- Parameters:
event – the resize event; passed to the base class, and the new width is read back from the widget itself.
- retranslate_dynamic_content(language: str) None[source]¶
Record the language used for subsequently generated panel content.
Channel choices are rebuilt when a source folder is enumerated, which may occur after the standard translation pass. Storing the language applied to the widget tree ensures that regenerated choices use the current display language even before the preference is persisted.
The view dropdown is re-rendered here as well. The generic pass only rewrites a caption whose translation differs from its source or that has a hand-written row, so “Cell probability”, translated from the generated catalog, would otherwise stay Swedish after a switch back to English.
- Parameters:
language – Language code currently applied to the panel.
- run_preview()[source]¶
Segment the loaded image off the GUI thread.
The guard, the refusals and the busy state are the shared ones — see
LivePreviewContract.
- set_propagate_callback(cb) None[source]¶
Register a callback(dict) used to push tuned live settings back to the main settings panel (wired by the AppScreen).
- Parameters:
cb – callable given a dict of setting key to value when the tuned settings are propagated, or
None.
- settings_for_propagation() dict[source]¶
Map the live-preview widget values to main-panel settings keys.
THE MODEL IS WRITTEN BACK TO THE KEY IT WAS READ FROM. Propagation used to write
model_nameand<primary>_model_nameonly, and for pathogens the run reads neither first:pathogen_modeloverrides both when it is set. A user seeded from apathogen_modelcheckpoint, switched the live model, and propagated, and the run went on using the checkpoint – the same preview/run disagreement as before, pointing the other way.Only when the settings the panel holds ALREADY set that key. Writing it otherwise would newly switch the override on for a user who never asked for it, and
pathogen_modelis validated harder than the name key (spacr.validatestops a run on a path that is not there).
- showEvent(event)[source]¶
Refresh the model list whenever the panel comes back on screen.
- Parameters:
event – the show event; passed to the base class and otherwise not read.
- show_comparison(layers) bool[source]¶
Draw
layers(bottom first) in the third panel.Nothing ticked puts the panel away again.
- Parameters:
layers – the
Layerstack, bottom first, ascomposite()takes it.- Returns:
whether a picture was drawn.
- shutdown() None[source]¶
Abandon any load in flight and leave no QThread behind.
Called from
closeEvent(), and safe to call directly when a screen is torn down without one.
- class spacr.qt.widgets.live_preview.LiveSettingsDialog(panel: LivePreviewPanel)[source]¶
Bases:
PySide6.QtWidgets.QDialogModal dialog that surfaces every live-preview setting.
Re-parents the panel’s hidden state widgets into a QFormLayout so edits go straight into the panel’s canonical fields — nothing to sync manually. On close, widgets are returned to the panel hidden so their values persist across opens.
- Rows shown (per the user’s spec):
Normalisation upper + lower percentile
Outline colour
Outline thickness
Model
Flow threshold
Cell probability
Object type
Object channel (cell / nucleus depending on selection)
Pre (bool)
Post (bool)
- Parameters:
panel – the preview panel this dialog edits. It is also the dialog’s PARENT, and the widgets the dialog lays out belong to the panel rather than to it – the dialog only knows which rows they sit on, which is what lets a morphology change re-gate them.
Build the dialog around the panel’s own controls.
The controls are the panel’s and are re-parented in here for the lifetime of the dialog, so their values survive it being closed and reopened. The panel is told which dialog is open, so a morphology change can re-gate the rows – the widgets live on the panel, but it is the dialog that knows which row each sits on.
- Parameters:
panel – the live-preview panel whose controls this edits.
- close()[source]¶
Return panel controls even when a never-shown dialog is closed.
Qt skips
done()for a hidden dialog’s successful close, though the dialog can still be destroyed afterward. A visible dialog reachesdonethrough its normal close event; its release guard keeps this path idempotent.- Returns:
whether Qt accepted the close request.
- done(result)[source]¶
Return borrowed controls and detach subscriptions on every exit.
Qt destroys a dialog’s children with it, so anything of the panel’s still parented under this dialog when it goes would go with it. The controls belong to the panel and retain their values after Close, Escape, accept, reject and explicit completion. Restoring them in
donecovers exits that do not deliver a close event. A completed dialog cannot move controls out of a replacement dialog.- Parameters:
result – dialog result passed to Qt after controls return to the panel’s hidden store.
- refresh_visibility()[source]¶
Grey out settings that don’t apply to the current selection.
- Rules (mirroring the pipeline’s own relevance):
Nothing in the Segmentation group greys out for the model. Cellpose 4 ships one set of weights and all three knobs (diameter / flow / cell-prob) still reach it — see
DIAMETER_TOOLTIPfor the measurement.The object type decides which channel spinners are live: the cell channel greys out for a nucleus-only object and vice-versa.
Pre-processing knobs (normalise + its two percentiles) are only relevant when the Pre step is enabled.
Overlay / post knobs (outline colour + thickness) are only relevant when the Post step is enabled.
- class spacr.qt.widgets.live_preview.PreviewRequest[source]¶
Everything the worker needs to run one segmentation pass.
Kept as a plain dataclass so tests can construct it directly; the panel builds one from its widget state on each Run.
- Parameters:
image – the field to segment, an array of shape (H, W) or (H, W, C); each object type’s channel index selects its plane.
source_path – the file the field was loaded from, read for pixel size and objective metadata when the PSF calibration is left unset; empty falls back to the settings’
srcand then to the defaults.
- spacr.qt.widgets.live_preview.first_supported_image(source: pathlib.Path) pathlib.Path | None[source]¶
Return the first supported image at or below
source.Direct image files are returned unchanged. Directory traversal stops as soon as the first sorted match is found instead of materialising and sorting every image in a potentially enormous plate. Files whose names start with a dot are skipped:
._<name>.tif, the sidecar macOS writes on exFAT and network volumes, sorts before every image and holds none.- Parameters:
source – image path or directory to inspect.
- Returns:
the first supported image, or
None.
- spacr.qt.widgets.live_preview.load_preview_image(path: pathlib.Path) numpy.ndarray[source]¶
Read path into a nonempty (H, W) or (H, W, C) numeric array.
Tifffile preserves TIFF bit depth; PNG/JPEG use PIL. NumPy
.npystacks are memory mapped without loading pickled objects. RaisesFileNotFoundErrorif the path is bad.- Parameters:
path – image file path (
strorPath);.tif/.tiffselects tifffile and.npyselects NumPy.
- spacr.qt.widgets.live_preview.load_preview_mip(paths) numpy.ndarray[source]¶
Max-project a field’s planes, the way the ingest already does.
io._rename_and_organize_image_filesreduces every z-stack tonp.maxover its planes, per field and per channel, before anything reachesstack/. This is the preview’s copy of that, so what the user is looking at is what masking will actually run on.Planes are folded one at a time rather than stacked: a 60-plane field at 2048x2048 uint16 is 500 MB as one array and 8 MB folded.
- Parameters:
paths – plane paths in acquisition order; one path is returned unchanged, so a flat 2-D field costs nothing.
- Raises:
FileNotFoundError – if no path can be read.
- spacr.qt.widgets.live_preview.load_source_payload(source, max_sets: int = DEFAULT_MAX_SETS, enumerate_sets: bool = True, *, project: bool = False, known_sets=()) Dict[str, Any][source]¶
Discover, enumerate and decode one preview source. Data in, data out.
This is the whole of a preview load, written so it touches no widget and no Qt object and can therefore be handed straight to
spacr.qt.job_runner.JobRunner. It used to be therunmethod of a hand-rolledQThreadthat emitted two signals, which kept the panel’s sampler warm by orderingenumeratedbeforeloaded; returning both halves in one dict gets the same ordering for free, because the caller adopts the enumeration and installs the image in a single GUI-thread call.The enumeration reads file names only. Decoding reads the selected image, plus its channel’s z-planes when projection is requested.
- Parameters:
source – image file or directory to load a preview from.
max_sets – cap for the sample drawn when
sourceis a directory.enumerate_sets –
Falseskips the folder scan entirely. The FOV dropdown hands out a path from a set the sampler already produced, so re-scanning for it would burn a full pass over a 98 000-file plate to rediscover what is already cached.project – project the selected channel’s z-stack on this worker.
known_sets – cached image sets used when enumeration is skipped.
- Returns:
{path, array, directory, sets, channels, error}.setsisNonewhen no enumeration was done or it failed, which the caller reads as “leave the sampler alone”.
- spacr.qt.widgets.live_preview.numpy_to_qpixmap(arr: numpy.ndarray, normalise: bool = True, lo_pct: float = 2.0, hi_pct: float = 98.0) PySide6.QtGui.QPixmap[source]¶
Convert an (H, W) or (H, W, C) array to a
QPixmap.The result is always RGB888, so the caller cannot hand Qt a buffer whose real row length disagrees with the
w * 3stride below. Channel counts other than three are reconciled here — extra channels are dropped, missing ones are filled with black — because a mismatch madeQImagereadh * w * 3bytes out of a buffer that only heldh * w.- Parameters:
arr – image array of shape (H, W) or (H, W, C); a non-uint8 array is scaled to 8 bits first, by percentile when
normaliseis true.
- spacr.qt.widgets.live_preview.object_role(label: str) str[source]¶
The settings ROLE an object-dropdown caption stands for.
organelleis slot 1, whose role has the same name;organelle 2isorganelleb, which is the prefix its settings keys actually carry. The dropdown counts because that is what the main panel counts, and the roles use letters because a digit cannot start a Python identifier.- Parameters:
label – an object-dropdown caption such as
"cell","organelle"or"organelle 2"; anything not starting withorganelleis returned unchanged.
- spacr.qt.widgets.live_preview.organelle_label(number: int) str[source]¶
The dropdown caption for organelle slot
number.Slot 1 stays plain
organelle: one organelle is the ordinary case, and numbering it “organelle 1” would relabel every existing screen to say something new about a run that has not changed.- Parameters:
number – the organelle slot, counting from 1; converted to
int.
- spacr.qt.widgets.live_preview.overlay_mask(image: numpy.ndarray, mask: numpy.ndarray) numpy.ndarray[source]¶
Legacy single-mask overlay retained for older imports.
- Parameters:
image – source image of shape (H, W) or (H, W, C).
mask – label image the same height and width as
image; its object boundaries are drawn in the cell outline colour.
- spacr.qt.widgets.live_preview.overlay_masks(image: numpy.ndarray, masks: Dict[str, numpy.ndarray], outline_rgb: Tuple[int, int, int] | None = None, outline_thickness: int = 1, normalise: bool = True, lo_pct: float = 2.0, hi_pct: float = 98.0, random_outline: bool = False, outline_colors: Dict[str, Tuple[int, int, int]] | None = None, primaries: str = 'rgb') numpy.ndarray[source]¶
Return an RGB uint8 view of
imagewith every mask’s boundary drawn in the object’s colour (oroutline_rgbwhen supplied).- Parameters:
image – (H, W) or (H, W, C) source image.
masks –
{object_type: label_array}— one entry per object type currently visible on the panel.outline_rgb – overrides the per-object colour when the user picks a global outline colour from the toolbar.
outline_thickness – number of pixels the boundary is dilated by (1 = crisp, 3 = highlighter). Tops out at 5.
normalise – forwarded to
_to_uint8().random_outline – assign every positive object label a vivid, stable categorical colour. This takes precedence over
outline_rgband corresponds tocolor (random)in Mask Live.outline_colors – per-compartment colour overrides used when no global
outline_rgbis given. This is how the panel’sautomode reaches the renderer: it holds one random colour per compartment for the current run. Falls back toOBJECT_COLORSfor anything it does not name.primaries – one of
spacr.crops.DISPLAY_PRIMARIES. Applied to the IMAGE ONLY, before a single outline is drawn.
WHY THE ORDER MATTERS, and it is the whole reason this parameter is here rather than in
numpy_to_qpixmap(). The primaries are a channel-to-colour mapping and only channels belong in it. An outline is not a channel – it is a colour the user chose, or a categorical label – so putting it through the same matrix would answer a request for a red outline with a yellow one. Recolour the image, then draw on top.
- spacr.qt.widgets.live_preview.random_outline_colour(rng: random.Random | None = None, palette: Sequence[Tuple[int, int, int]] | None = None) Tuple[int, int, int][source]¶
Return one vivid random RGB triple for the
autooutline mode.Hue is uniform over the full circle while saturation and value stay high, so the colour is always legible on top of a micrograph — a uniform draw in RGB would regularly produce muddy near-grey outlines nobody can see.
- Parameters:
rng – optional generator, for reproducible tests.
palette – draw from these instead of the hue circle. This is how
safe_outline_palette()reaches theautomode.
- Returns:
(r, g, b)in 0..255.
- spacr.qt.widgets.live_preview.safe_outline_palette() List[Tuple[int, int, int]] | None[source]¶
Colours
automay draw from, orNonewhen any colour will do.A random hue is right for a sighted user and exactly wrong for a colour-blind one: uniform over the circle, it will sooner or later hand two adjacent compartments a pair that user cannot tell apart, and the outlines are the one thing on the screen whose whole job is to be told apart. When a colour-vision mode is set,
autodraws from the Okabe-Ito set instead – eight colours chosen to stay distinct under all three deficiencies.- Returns:
RGB triples, or
Nonewhen the preference isoff.
- spacr.qt.widgets.live_preview.BOUND_ROWS[source]¶
Where each bound control of a Cellpose compartment lives in
object_filters. Mask’s{object}_min_areafamily was retired on 2026-09-25; for cell, nucleus and pathogen these four controls read and write the object’sareaandintensity_meanrows instead. The organelle slots keep their own settings.
- spacr.qt.widgets.live_preview.SESSION_MASK_LIMIT = 8[source]¶
How many of this session’s masks the comparison panel can lay over one another: the last eight, oldest dropped first. Each is a full-size label array, and a panel that kept every run of an afternoon would hold them all in memory for a popup that lists eight comfortably.
Nested helpers¶
- LivePreviewPanel._build_compartment_widgets._organelle_widget(kind, spin_args)¶
The control an organelle setting needs, by its kind.
spacr/qt/widgets/live_preview.py:4071
- LivePreviewPanel._build_compartment_widgets._spin(kind, spin_args)¶
One spin box of the right kind for this setting.
spacr/qt/widgets/live_preview.py:4011
- LivePreviewPanel.apply_settings._seed(widget, keys, cast)¶
Write the first present, usable value of
keys.A SPIN BOX CLAMPS WHAT IT CANNOT HOLD, silently. The flow threshold runs -1 to 3 here while Mask ships 100 – “accept everything Cellpose proposes” – so seeding wrote 3, and propagating then handed 3 back as if the user had chosen it. The value that did not fit is remembered so propagation can return it untouched; see
_unclamped().spacr/qt/widgets/live_preview.py:3674
- _apply_size_filter._num(key, default)¶
One size-filter setting as a number, or the default.
spacr/qt/widgets/live_preview.py:1284
- _segment_multi._prepared(ch_idx: int) np.ndarray¶
One channel’s plane after the PSF or enhancement chain, once.
spacr/qt/widgets/live_preview.py:1090
- _unmix_preview_field.load_control(path)¶
Read controls cooperatively and reject incompatible channel layouts.
spacr/qt/widgets/live_preview.py:1012