spacr.qt.prerun¶
What spaCR already knows about your data, on screen before you press Run.
Two things the pipeline computes, has always computed, and nobody has ever seen — because both of them print to a terminal that has since scrolled, or live in a module nothing 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 turns the guess into a measurement, per object type, and shows how many objects it measured so the number can be disbelieved. 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 by construction: it
never touches the Run button, never disables it, never intercepts the click.
A plate that failed QC is still a plate its owner may have every reason to
measure, and a quality report that stops people is a quality report they
switch off. 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 costs seconds to minutes, and a screen that pays that on every visit is
a screen nobody keeps. 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 rather than believing 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 –
cheap is not the same as free, and only free may run where the frames are
painted. 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 installing this
never costs a screen 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.
Asked for by a user: “in mask generation the, diamiter calculation should be a button in the model zoo button popup window not on the main screen. pressing the button should bring another pupup with all the information in the current container.” So this holds the whole
DiameterPanel– the measure button, one row per object type with its evidence and note, Use per row and Use all – and the panel still 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. A proposal without those is just a different guess.
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: closing a window is not a reason to measure twice.- 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 has no opinion about whether Measure should run. 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.
SPLIT IN TWO, and the split is the fix for a frozen application. 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 BUMPS
_refresh_gen, the one that finds the field empty included. 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]¶
Ask for a refresh a beat from now, on the same debounce as typing.
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: a Model zoo that opens without this button’s popup is better than one that does not open.
- 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: a screen that opens without the banner is the old behaviour, and that is always better than a screen that does not open.
- 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.