spacr.qt.widgets.measure_preview

Interactive Measure crop preview with a pipeline-compatible settings panel.

The compact card is intentionally image-first. Its Crop settings… dialog contains the Measure settings that can be evaluated on one merged array: general mask/channel controls, object-crop output controls, measurement filters, and preview-only display controls. With propagation enabled, every pipeline setting is copied to the main Measure form as it changes.

Cell crops are grouped by three independent companion-object dimensions: nucleated/unnucleated, infected/uninfected, and with/without organelles. This keeps all cells visible while making it explicit which categories would be retained by the current filter settings.

Classes

CropSettingsDialog

Tabbed live settings dialog for MeasurePreviewPanel.

MeasurePreviewPanel

Preview Measure crops and propagate a faithful run configuration.

Functions

annotate_crops(→ None)

Tag each crop with its phenotype category and whether filters keep it.

compute_crops(→ Dict[str, Any])

Crop the objects out of data and categorise them. Worker-safe.

load_merged_array(→ Dict[str, Any])

Read one merged (H, W, C) array. No Qt, so it runs on a worker.

resolve_merged_source(path[, rng])

A concrete merged .npy from whatever the user gave us.

Module Contents

class spacr.qt.widgets.measure_preview.CropSettingsDialog(panel: MeasurePreviewPanel)[source]

Bases: PySide6.QtWidgets.QDialog

Tabbed live settings dialog for MeasurePreviewPanel.

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 crop-settings dialog over one preview panel.

Parameters:

panel – the preview these settings belong to.

closeEvent(event)[source]

Remember the dialog’s geometry before it goes.

Parameters:

event – the Qt close event.

refresh_organelle_slots() → None[source]

Show one organelle slot per slot the run declares.

The rows are HIDDEN, not removed: the widgets keep their values, so lowering the count and raising it again finds the old answers still there – the same promise spacr.settings._set_organelle_defaults makes for the settings themselves.

IT LAYS OUT AS WELL AS GATING. The panel builds a slot’s controls when the count reaches it rather than building all 702 up front, so a count that rises while this dialog is open arrives as controls with no rows; _adopt_new_slot_controls puts them in the block before the gate below decides which are shown.

An unset count shows every slot that exists, which is what this dialog did before the count reached it: better to offer a field too many than to hide one a run is using.

class spacr.qt.widgets.measure_preview.MeasurePreviewPanel(parent=None, *, threaded: bool = True)[source]

Bases: spacr.qt.widgets.preview_contract.LivePreviewContract, PySide6.QtWidgets.QWidget

Preview Measure crops and propagate a faithful run configuration.

A live view like the other three, and since the shared contract (LivePreviewContract) it wears their vocabulary: the same Run preview button, the same Cancel beside it, and a sentence on the status line whenever it cannot preview. It used to be alone in every one of those columns — its button said “Refresh crops”, nothing could be cancelled, and a press with no array loaded did nothing and said nothing.

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: its controls, its grid and its drop target.

Parameters:

parent – parent widget.

apply_settings(settings: dict) → None[source]

Seed the panel from the main Measure settings dict.

The inverse of settings_for_propagation(), and tested as one. This panel had no apply_settings at all, so the crop preview – opened to decide whether a crop size will cut the cell in half, or which stain lands in which colour – always answered for its own defaults rather than for the run about to happen.

Every field is copied independently: a settings file carrying one unusable value must not cost the panel every field after it.

Parameters:

settings – the Measure settings dict, or None; keys that are absent or None leave their controls unchanged, and a value that cannot be applied is skipped.

closeEvent(event)[source]

Stop any preview work before going away.

Parameters:

event – the Qt close event.

current_params() → dict[source]

The parameters the preview is using right now.

Returns:

the parameters as a plain dict.

display_channel() → int | None[source]

Channel index the crops are rendered from, or None for all.

dragEnterEvent(event)[source]

Accept a drag carrying something this preview can measure.

Parameters:

event – the Qt drag event.

dragMoveEvent(event)[source]

Keep accepting while droppable input stays over the panel.

Parameters:

event – the Qt drag event.

dropEvent(event)[source]

Take the dropped input and preview it.

Parameters:

event – the Qt drop event.

load_array(path: str) → bool[source]

Synchronously read and install one merged array.

The sibling of LivePreviewPanel.load_image: for programmatic callers and tests. The GUI uses load_array_async().

Parameters:

path – merged .npy file, run folder or merged/ folder, as load_merged_array() accepts; a read error is shown in the status line and returns False.

load_array_async(path: str, *, enumerate_sets: bool = True) → bool[source]

Read path on a worker, then install it on the GUI thread.

Every GUI entry point – the drop handler, the Choose-array dialog and the FOV dropdown – comes through here. A 17 MB merged array is not a cheap read, and the crop pass that follows it is far worse.

Parameters:

path – merged .npy file, run folder or merged/ folder, as load_merged_array() accepts; an empty value submits nothing.

Returns:

True when a job was submitted.

open_crop_settings() → None[source]

Open the crop-settings dialog for this preview.

propagate_settings() → None[source]

Push the tuned settings to the run, if anything is listening.

refresh() → None[source]

Re-crop the loaded array and redraw the grid.

Dispatches: the crop pass runs on a worker and the grid is rebuilt when it lands. Every knob in the Crop settings dialog is wired to this, so it used to freeze the window for 1441 ms per spinbox step on a 1024x1024x8 array. Re-cropping supersedes by token, so dragging a spinbox through ten values draws the last one rather than all ten.

Says why when it cannot: returning in silence left the button doing nothing with nothing on the status line, which is the one thing no live view may do.

Spinbox drags call refresh repeatedly in one event-loop turn, so the old grid is cleared once for the latest token and only its result is drawn.

run_preview() → None[source]

Re-crop on demand — the shared name for the shared action.

Every live view answers to run_preview; this panel’s own refresh() stays as the internal path the crop knobs drive, which supersedes rather than refusing.

sample_note() → str[source]

The sentence stating this preview is a sample of N of M sets.

set_organelle_count(count) → None[source]

How many organelle slots the crop settings should offer.

The same rule Mask and the Mask live preview follow: the run declares number_of_organelles, and every panel shows that many. Without it the crop settings offered a fixed four – so a one-organelle run had three mask-slice and three minimum-area fields for objects it does not have, and each of them propagates into the settings the run reads.

IT BUILDS THEM AS WELL AS SHOWING THEM. This used to change only what was SHOWN, over 702 slots’ worth of controls that _build_controls had already made – so the answer to “a run with no organelle” was 2,117 controls hidden behind a gate. The count is now what brings a slot’s controls into existence, which is why raising it is the only route that has to work.

Parameters:

count – number of organelle slots, clamped to 0-MAX_ORGANELLES; a value int() rejects is ignored.

set_propagate_callback(callback) → None[source]

Set what to call when the user pushes these settings to the run.

Parameters:

callback – called with the settings dict.

settings_for_propagation() → dict[source]

The settings this preview would hand to the real run.

What makes a preview worth doing: the numbers tuned here are the numbers the run uses, rather than something the user must retype.

Returns:

the settings dict.

shutdown() → None[source]

Abandon anything in flight and leave no QThread behind.

spacr.qt.widgets.measure_preview.annotate_crops(crops: List[Dict[str, Any]], data: numpy.ndarray | None, params: Dict[str, Any]) → None[source]

Tag each crop with its phenotype category and whether filters keep it.

params is a snapshot of the widget values taken on the GUI thread – see MeasurePreviewPanel._category_params(). Passing a snapshot rather than reading the widgets is what makes this safe to call from a worker.

Parameters:
  • crops – crop dicts from spacr.measure.crop_objects_from_array(), each with a label; updated in place with category, included and, for cells, phenotype.

  • data – the merged (H, W, C) array the crops came from, or None, in which case every crop is simply included.

  • params – widget snapshot with object, cell_dim, dims, minima and uninfected; only object == "cell" is categorised by phenotype.

spacr.qt.widgets.measure_preview.compute_crops(data: numpy.ndarray, crop_kwargs: Dict[str, Any], category_params: Dict[str, Any]) → Dict[str, Any][source]

Crop the objects out of data and categorise them. Worker-safe.

The whole of what refresh used to do inline, minus the drawing: QPixmap is a GUI object and building one off the GUI thread is undefined behaviour, so the pixmaps stay in _render_grid().

Parameters:
  • data – merged (H, W, C) array of image channels then mask slices.

  • crop_kwargs – keyword arguments for spacr.measure.crop_objects_from_array(), e.g. mask_dim; a crop failure is returned as error rather than raised.

  • category_params – widget snapshot passed to annotate_crops().

Returns:

{crops, error}.

spacr.qt.widgets.measure_preview.load_merged_array(path: str, enumerate_sets: bool = True) → Dict[str, Any][source]

Read one merged (H, W, C) array. No Qt, so it runs on a worker.

Also lists the sibling arrays, by name only, for the same reason live_preview.load_source_payload does: _refresh_source_selectors enumerates the folder on every load, and doing that on the GUI thread cost 124 ms of the 2469 ms freeze this replaced.

Parameters:
  • path – NumPy array file to open; a usable payload must decode to one merged (height, width, channels) array.

  • enumerate_sets – False reuses the sampler’s cached listing – the FOV dropdown hands out a path it already enumerated.

Returns:

{path, data, directory, sets, channels, error}. data is None whenever error is set or the file is not a merged array.

spacr.qt.widgets.measure_preview.resolve_merged_source(path, rng=None)[source]

A concrete merged .npy from whatever the user gave us.

THREE THINGS ARE A VALID ANSWER TO “where are the crops”, and only one of them used to be accepted:

  • a merged .npy – what the Choose dialog offered, and nothing else;

  • a RUN FOLDER, which is what src holds. A Measure run is pointed at the plate directory and finds merged/ itself, so the preview asking for a file meant hunting through a folder for one of fifty-two arrays whose names carry a well and a field and nothing about which is interesting;

  • a merged/ folder directly.

A field is picked at RANDOM rather than taking the first. Sorted first is always the same well and the same field, so a preview that always opens on E01 field 1 tells the user about one corner of one condition – and if that field happens to be clean, a crop size that cuts cells in half everywhere else looks fine.

No Qt and no widgets: this runs on the worker with the load it feeds.

Parameters:
  • path – a file, a run folder, or a merged folder.

  • rng – something with choice; defaults to random.

Returns:

a Path to one array, or None.

Nested helpers

MeasurePreviewPanel.apply_settings._set(fn, key, cast=None)

Apply one setting, skipping keys that are absent or None.

Absent and None are LEFT ALONE rather than applied as a default: the preview is showing what the run will do, and filling a gap here would show a value the run does not have.

spacr/qt/widgets/measure_preview.py:2017

_unmixed_crop_source.checkpoint()

Stop between controls and before expensive display processing.

spacr/qt/widgets/measure_preview.py:438

_unmixed_crop_source.load_control(filename)

Read each control without changing it or accepting a wrong layout.

spacr/qt/widgets/measure_preview.py:453