spacr.qt.screens.model_compare¶
Model Compare — two Cellpose models, the same three fields, one table.
Picking a segmentation model in spaCR means running a plate, looking at montages, changing one number and running it again. This screen makes the question small enough to answer in a minute: choose a folder, take three fields, configure two models, press Compare. The masks come back side by side over the same image with the object counts, the ARI and the split/merge attribution underneath.
Layout:
┌──────────────────────────────────────────────────────────────────────┐
│ /data/plate1/1 [Choose folder…] [Load] │
│ Fields [3] [Compare] │
├───────────────────────────────┬──────────────────────────────────────┤
│ Model A │ Model B │
│ model [cpsam ] │ model [cpsam ] │
│ diameter [30.0] │ diameter [60.0] │
│ flow / cellprob / min size … │ … │
├───────────────────────────────┴──────────────────────────────────────┤
│ ! B: diam_mean=17 is ignored — use diameter=, that one rescales. │
├──────────────────────────────────────────────────────────────────────┤
│ parameter A B reaches the model? │
│ diameter 30.0 60.0 yes ← varied │
│ diam_mean – 17 no ← ignored by Cellpose 4 │
├──────────────────────────────────────────────────────────────────────┤
│ field A obj B obj Δ ARI matched splits merges only B … │
├───────────────────────────────┬──────────────────────────────────────┤
│ [ image + A masks ] │ [ image + B masks ] │
└───────────────────────────────┴──────────────────────────────────────┘
Design notes:
The resolved-parameter table is not decoration. Cellpose 4 accepts
model_type,diam_mean,nchan,channelsandrescaleand then ignores every one of them, and it resolves all the pre-SAM model names tocpsam. A comparison that differs only in one of those reports “no difference”, which reads as “the models are equivalent” rather than “you changed nothing”. So the screen shows what actually reached each model, marks what was dropped, and says so in a banner above the numbers.Off the GUI thread. Loading fields and segmentation can both be slow on a plate or NAS path, so they go through
spacr.qt.bridge.make_thread()like every other spaCR job. Tests passthreaded=False, which runs the same code inline.No modal dialogs on any error path. A folder with no images, a model that will not load, a field of the wrong shape — all of it lands in the inline status label. A QMessageBox hangs a headless run.
The preview colours by correspondence, not by label id. Label 3 in A has nothing to do with label 3 in B, so a shared random palette would invite exactly the wrong comparison. Objects that have a partner in the other mask are drawn in one colour and objects that do not in another, which is the comparison rather than a decoration of it.
Neither model is called correct. The screen renders
spacr.model_compare’s directional wording as-is.
Classes¶
Compare two segmentation models on the same handful of fields. |
Functions¶
|
Blend a mask over its image, colouring by correspondence. |
|
Parse a |
|
Reduce any field to a uint8 grayscale of |
Module Contents¶
- class spacr.qt.screens.model_compare.ModelCompareScreen(parent=None, threaded: bool = True)[source]¶
Bases:
PySide6.QtWidgets.QWidgetCompare two segmentation models on the same handful of fields.
- Parameters:
parent – Qt parent.
threaded – run the segmentation on a worker thread (the default). Tests pass
Falsefor deterministic, synchronous behaviour.
- Variables:
last_error – text of the most recent failure,
""when the last operation succeeded. Errors are only ever reported here and in the inline status label — never in a modal dialog.
Build the screen and arm its drop zone.
The panel stylesheet is applied here rather than relied on from the launch sheet:
app.pyimports this module inside the branch that builds the screen, long after that sheet was generated, so the block is not in the sheet that is live and the panels would open bare.- Parameters:
parent – parent widget, or
None.threaded – segment on a worker thread. Set
Falsein tests socomparefinishes before it returns.
- closeEvent(event)[source]¶
Let every in-flight comparison thread finish before the widget dies.
- Parameters:
event – the close event; it is passed on to the base class after running threads are asked to quit and waited on for up to 5 s each.
- compare() bool[source]¶
Segment every loaded field with both models and fill the tables.
- Returns:
for the synchronous path, whether a report was produced; for the threaded path, True once the job has started.
- configure(model_a: str = '', model_b: str = '', folder: str = '', n_fields: int = 0) bool[source]¶
Preload the screen with two models and a folder.
The public route for another screen to hand this one a comparison — the Model Zoo’s “compare the two selected” uses it. A caller reaching into
_panel_a.model_editwould break the moment either panel is restructured.Every argument is optional; an empty one leaves that control alone.
n_fieldsis applied BEFORE the folder so the reload the folder triggers already uses the right count, rather than loading N fields and immediately reloading with a different N.- Parameters:
model_a – model name or checkpoint path for the left panel.
model_b – same, for the right panel.
folder – directory of fields to compare on.
n_fields – how many fields; 0 keeps the current value.
- Returns:
what
set_source()returned, or True when no folder was given.
- is_busy() bool[source]¶
Whether anything is still running.
What the window asks before closing.
- Returns:
True while work is outstanding.
- model_configs()[source]¶
(config_a, config_b)as currently configured.- Raises:
ValueError – when either
extraline does not parse.
- preview_sizes()[source]¶
(a, b)preview sizes on screen —(0, 0)for an empty panel.The size the picture OCCUPIES, not the pixel count it was drawn with: on a HiDPI screen the panel is rendered at twice the density and the two answers differ by that factor.
- report() spacr.model_compare.ComparisonReport | None[source]¶
The most recent
ComparisonReport.
- select_field(row: int) bool[source]¶
Draw field
row’s two masks side by side over the same image.- Parameters:
row – index into the per-field table.
- Returns:
True when both panels rendered.
- set_segment_fn(fn: Callable | None) None[source]¶
Override the segmentation backend.
The Model Zoo (and every test in this file) hands in its own callable rather than loading Cellpose; None restores
spacr.model_compare.segment_with_cellpose().- Parameters:
fn –
fn(images, config) -> masks, or None.
- set_source(folder: str) bool[source]¶
Load the first N fields out of
folderwithout blocking Qt.Every failure here is a normal state — a mistyped path, a folder of CSVs, an empty plate — so it lands in the status label and returns False. This never raises and never opens a dialog.
- Parameters:
folder – a directory of
.tif/.png/.npy/.npzfields.- Returns:
with
threaded=False, True when at least one field loaded; otherwise True once the load job starts.
- spacr.qt.screens.model_compare.compose_overlay(image: numpy.ndarray | None, mask: Any, matched: Sequence[int] | None = None, alpha: float = 0.45) numpy.ndarray | None[source]¶
Blend a mask over its image, colouring by correspondence.
Objects listed in
matched(they have a partner in the other model’s mask) are drawn in one colour and the rest in another. Colouring by label id would be worse than useless here: label 3 in A has nothing to do with label 3 in B, so a shared palette invites the eye to compare things that are not the same object.- Parameters:
image – the field, or None for a black backdrop.
mask – a 2-D label image.
matched – labels of
maskthat have a partner; None colours everything as unmatched.alpha – overlay strength.
- Returns:
a uint8
(H, W, 3)RGB array, or None when the mask is not a 2-D label image.
- spacr.qt.screens.model_compare.parse_extra(text: str) Dict[str, Any][source]¶
Parse a
key=value, key=valueline into eval keyword arguments.Numbers and booleans are coerced so
diam_mean=17arrives as a number rather than the string"17"— the report compares values, and"17"and17would look like a difference where there is none.- Parameters:
text – the raw line; blank returns
{}.- Returns:
the parsed mapping.
- Raises:
ValueError – on a fragment with no
=in it, naming the fragment.
- spacr.qt.screens.model_compare.to_display_gray(image: numpy.ndarray | None, shape) numpy.ndarray[source]¶
Reduce any field to a uint8 grayscale of
shape, for a backdrop.Multi-channel fields are collapsed to their max projection rather than a mean: a nucleus channel averaged with an empty channel is a dim nucleus. Contrast is a 1-99.9 percentile stretch, the same window
spacr.qt.mask_engine.normalize_uint16()uses. A missing or wrong-shaped image comes back black rather than raising — the masks are what the screen is really showing.- Parameters:
image – the field, or None.
shape –
(height, width)the mask expects.
- Returns:
a uint8 array of
shape.
Nested helpers¶
- ModelCompareScreen._run_job._job(payload: Dict[str, Any]) None¶
Call the wrapped function, stashing its result in the payload.
spacr/qt/screens/model_compare.py:969
- ModelCompareScreen.compare._job() mc.ComparisonReport¶
Run both models over the same fields. Off the GUI thread.
spacr/qt/screens/model_compare.py:730
- ModelCompareScreen.set_source._job()¶
Load the comparison fields. Off the GUI thread.
spacr/qt/screens/model_compare.py:655