spacr.qt.prerun¶
Show segmentation QC results and measured diameters before a run starts.
The module shows two results the pipeline computes that would otherwise only be printed to a terminal or left in a module that no screen calls:
The segmentation verdict.
spacr.seg_qcscores every mask the moment it is written and files<plate>/qc/segmentation_qc_<object>.csv. Measure then spends hours cropping and measuring those masks without anyone having read it. The banner this module puts on the Measure screen reads that card back and says what it says, naming the plate, the wells and the likely cause — a verdict a user can act on, not a count of failures.The diameter.
spacr.diametermeasures characteristic object size from a handful of the user’s own fields, without loading Cellpose or torch.diameteris the single most consequential Cellpose 4 setting spaCR exposes —CellposeModel.eval(diameter=...)rescales every image by30/diameterso objects land near the sizecpsamworks at — and it is the one users guess at. The panel this module builds replaces the guess with a measurement, per object type, and shows how many objects it measured so the estimate can be judged. It opens in a popup of its own from the Measure diameters… button in Mask generation’s Model zoo popup, next to the choice of model it is the other half of, rather than on the main screen.
Neither one blocks anything. The banner is advisory: it never touches the
Run button, never disables it and never intercepts the click, because the user
may have reasons to measure a plate that failed QC. BLOCKS_RUN is
False, tests/qt/test_prerun.py asserts it against the real screen, and
there is no code path here that could change it.
Cost¶
The banner reads a verdict; it does not compute one. Opening a plate’s
masks takes seconds to minutes, which is too slow for every visit to the
screen. So spacr.seg_qc.read_digest() parses the CSVs the mask run
already wrote, dates each one against its mask stack, and reports a card
older than its masks as out of date instead of using it. Only the
Score the masks now button scores anything, only when pressed, and it does
it on a worker thread and writes the card so the next open is cheap again.
Neither half of that runs on the GUI thread any more. The read moved onto a
worker on 2026-09-04, when a single os.path.exists under a sleeping
autofs mount was measured not returning for twenty seconds while
install_qc_banner was doing it inside MainWindow._build_screen. See
SegQCBanner.refresh().
Installation goes through the seams that already exist rather than through the
shared screen: spacr.qt.app.APP_FACTORIES, consulted by
MainWindow._build_screen, and spacr.qt.theme.register_widget_qss()
for the colours. AppScreen is untouched. A factory already registered
for one of these keys — spacr.qt.chaining registers one for every
module that declares ports — is kept and delegated to, so a screen keeps the
strip it already had, in either registration order.
Classes¶
The diameter estimate in a popup of its own, opened from the Model zoo. |
|
A measured Cellpose |
|
The segmentation verdict, on the Measure screen, before Measure runs. |
Functions¶
|
Build the diameter popup for |
|
The Mask generation screen |
|
Install the segmentation-QC banner when |
|
Put a QC switch on |
|
The banner installed on |
|
Install the banner on the Measure screen. Idempotent. |
|
Undo |
Module Contents¶
- class spacr.qt.prerun.DiameterDialog(screen: PySide6.QtWidgets.QWidget, *, estimator=None, parent: PySide6.QtWidgets.QWidget | None = None)[source]¶
Bases:
PySide6.QtWidgets.QDialogThe diameter estimate in a popup of its own, opened from the Model zoo.
Holds the whole
DiameterPanel– the measure button, one row per object type with its evidence and note, Use per row and Use all – in a popup opened from the Model zoo popup instead of on the main Mask generation screen. The panel writes into the settings of the screen that opened the zoo.Being a
QDialogis what dresses it:spacr.qt.widgets.glassgives every dialog the translucent card, the rounded corners and the travelling rim, and it recognises one by its type. The way out is Close, red, at the bottom right, like every other spaCR dialog.- Parameters:
screen – the Mask generation screen the estimates are for.
estimator – diameter estimator, for tests.
parent – the window that opened it, normally the Model zoo popup.
Build the popup around a fresh
DiameterPanel.- Parameters:
screen – the screen whose settings the estimates are for.
estimator – how to measure the diameters.
parent – parent widget.
- class spacr.qt.prerun.DiameterPanel(screen: PySide6.QtWidgets.QWidget, *, estimator=None, parent=None)[source]¶
Bases:
_JobMixin,PySide6.QtWidgets.QFrameA measured Cellpose
diameter, per object type, from the user’s own fields.CellposeModel.eval(diameter=...)under Cellpose 4 rescales the input by30/diameterso objects land near the sizecpsamworks at, which makes a two-fold error in this one number a two-fold error in every mask, count and measurement downstream.spacr.diametermeasures it from a handful of sampled fields — no Cellpose, no torch — and this panel is where that measurement reaches the settings form.Every row carries its evidence: the value, the 10th-90th percentile range, how many objects it was pooled from, how many fields contributed, how it was measured and how much to trust it.
It lives in a
DiameterDialog, opened from the Model zoo popup, so the last measurement is kept on the screen it was made for and shown again the next time the popup opens, without measuring again.- Parameters:
screen – the
AppScreenit belongs to.estimator – what to call to estimate, for tests. Defaults to
spacr.diameter.estimate_diameters().parent – parent widget; ownership only.
Build the diameter estimator’s panel.
- Parameters:
screen – the screen this panel advises.
estimator – how to measure the diameters.
parent – parent widget.
- apply(object_type: str) bool[source]¶
Write one proposal into its
<object>_diameterfield.Only ever called from the row’s own button — nothing here writes a setting on its own, and an unusable estimate (NaN, by construction in
spacr.diameter.DiameterEstimate) is never written at all.- Parameters:
object_type –
'cell','nucleus'or'pathogen'.- Returns:
True when the value reached the widget.
- class spacr.qt.prerun.SegQCBanner(screen: PySide6.QtWidgets.QWidget, *, reader=None, threaded: bool = True, parent=None)[source]¶
Bases:
_JobMixin,PySide6.QtWidgets.QFrameThe segmentation verdict, on the Measure screen, before Measure runs.
Reads
<plate>/qc/segmentation_qc_<object>.csv— the cardspacr.seg_qcwrote when the masks were made — and shows what it says: the plate, the wells, the likely cause and what to do. It scores nothing on its own; the Score the masks now button is the only path in this class that opens a mask, and it exists because “no card” and “a card older than the masks” are both answers a user should be able to fix from here.It never prevents Measure from running. See
BLOCKS_RUN.- Parameters:
screen – the
AppScreenit belongs to.reader – what to call to read a digest, for tests. Defaults to
spacr.seg_qc.read_digest().threaded –
Falseruns the read inline instead of on a worker, emitting the same signals in the same order, so a test can drive the banner synchronously without the behaviour diverging. The interface never builds one that way – seerefresh().parent – parent widget; ownership only.
Build the segmentation-QC banner for one screen.
- Parameters:
screen – the screen this banner reports on.
reader – how to read the QC scores.
threaded – whether the read runs on a worker.
parent – parent widget.
- refresh() None[source]¶
Ask for the verdict and redraw when it lands. Never scores a mask.
The work is split in two so that no filesystem call runs on the GUI thread. What stays here is widget state — the
srcfield, and whether it names anything — and it is free. What moved into_refresh_job()is every filesystem call:find_scorecardswalking the user’s plate folders, oneos.statper card, andread_digestopening and parsing the CSVs. All of it is I/O on a path the user supplied.Measured on one workstation:
os.path.existson a path under/nas_mnt— anautofsmount whose share was asleep — had not returned after twenty seconds, because the stat is what triggers the automount. This method used to do that work inline, andinstall_qc_bannerused to call it insideMainWindow._build_screen, so it was the whole interface frozen on every Measure open. It left no traceback, because a stalled event loop is not a crash; it was reported as “opening map barcodes crashes spacr”, plus hover flicker and glimpses of other screens.The banner keeps whatever it last drew while a read is in flight. There is deliberately no
spacr.qt.path_probegate in front of the job: the probe answersisdiroptimistically-cheap but the scan has to happen off the GUI thread regardless, so a gate would only add a first-visit blank for no protection this does not give.Every call increments
_refresh_gen, including a call that finds the field empty. That is what makes a read cancellable without being interruptible: nothing can stop the worker mid-stat, but its answer is checked against the question before it is painted and dropped when the source has moved on. Without it, clearing src hid the banner and the read still in flight put the old plate’s verdict straight back on screen under no name at all.
- schedule_refresh() None[source]¶
Schedule
refresh()after the typing debounce delay (450 ms).What screen construction calls instead of
refresh(). Even the asynchronous refresh builds aQThread, and there is no reason to pay for one insideMainWindow._build_screen: the banner is advisory, and 450 ms later is soon enough for advice.
- spacr.qt.prerun.diameter_dialog(screen, *, estimator=None, parent: PySide6.QtWidgets.QWidget | None = None) DiameterDialog | None[source]¶
Build the diameter popup for
screen, not yet shown.- Parameters:
screen – the Mask generation screen.
estimator – diameter estimator, for tests.
parent – the window that opens it.
- Returns:
the dialog, or None when the screen has no diameter setting or the popup could not be built. Never raises, so the Model zoo still opens when this popup cannot be built.
- spacr.qt.prerun.diameter_screen_of(widget) PySide6.QtWidgets.QWidget | None[source]¶
The Mask generation screen
widgetsits in, or None.Walks the parents, so the Model zoo popup finds the screen whichever control opened it – the per-object model cell or a
*_model_namefield’s button – without either of them passing it along. A screen with no<object>_diametersetting has nothing to write an estimate into, and does not count.- Parameters:
widget – any widget, or None.
- spacr.qt.prerun.install(screen) None[source]¶
Install the segmentation-QC banner when
screenis Measure’s.Mask generation’s diameter panel is no longer installed on its screen: it opens from the Model zoo popup instead, see
diameter_dialog().
- spacr.qt.prerun.install_qc_banner(screen, *, reader=None, threaded: bool = True) SegQCBanner | None[source]¶
Put a QC switch on
screen’s action row that opens the verdict.The switch sits left of the 3D and Time switches; the banner lives in its popup, so the Run row keeps its place whatever the verdict says.
- Parameters:
screen – an
AppScreen.reader – digest reader, for tests.
threaded –
Falseto read inline, for tests.
- Returns:
the banner, or None when this screen cannot carry one. Never raises: if the banner cannot be installed, the screen opens without it.
- spacr.qt.prerun.qc_banner(screen) SegQCBanner | None[source]¶
The banner installed on
screen, or None.- Parameters:
screen – the screen widget whose
_seg_qc_bannerattribute is read.
- spacr.qt.prerun.register() bool[source]¶
Install the banner on the Measure screen. Idempotent.
Called by
spacr.qt.register_self_registering_modules()afterapp.pyhas finished importing and before the first window is built.- Returns:
True when anything was registered.
- spacr.qt.prerun.unregister() int[source]¶
Undo
register(), restoring whatever factory was displaced.- Returns:
how many keys were handed back.