spacr.qt.annotate_engine¶
Pure-Python backend for the Qt annotate screen.
The image-processing pipeline (normalize / channel-filter / outline / colored border) and the SQLite-backed page fetch + background save worker are all Tk-free. The Qt screen wraps this with a QWidget UI.
Semantics mirror spacr.gui_elements.AnnotateApp so annotations made in
either GUI are read/written the same way from the same
measurements/measurements.db.
Exceptions¶
Raised when a requested cancellation stops outline generation. |
Classes¶
Every knob the Annotate screen exposes, packed into one dataclass. |
|
Runs in a daemon thread; consumes {png_path: annotation} batches |
Functions¶
|
Return |
|
Turn a path list into the batch |
Measured records for decoded outline arrays retained between draws. |
|
|
Return sorted list of (class_value, count) for annotated rows. |
|
Null every value in |
|
Return the number of |
|
Evict one decoded array selected by the global memory policy. |
|
Return all object-filter bounds in their disabled state. |
|
Add |
|
Return ALL (png_path, annotation) rows matching every one of the |
|
Read one page of (png_path, annotation) rows in insertion order. |
|
Parse a filter bound, returning |
|
Zero out channels not present in |
|
Return the settings key for a channel and measurement pair. |
|
Return the page-aligned offset of the last annotated row, or None. |
|
Drop every cached mask. For tests, and for a caller changing plates. |
|
png_paths surviving a chain of |
|
Map an annotation value to a hex border color. |
|
Open one object crop PNG as an 8-bit RGB image in display order. |
|
The distinct values of one png_list metadata column, sorted. |
|
Normalise object-filter bounds and migrate legacy size limits. |
|
Normalize the given PIL image per-channel using percentile stretch. |
|
Overlay per-channel object outlines on |
|
Turn an image-type expression into a SQL fragment and its parameters. |
|
png_paths satisfying EVERY |
|
png_paths whose |
Module Contents¶
- exception spacr.qt.annotate_engine.OutlineCancelled[source]¶
Bases:
ExceptionRaised when a requested cancellation stops outline generation.
Cellpose model construction and inference cannot be interrupted safely. Cancellation is therefore checked between native calls, and this exception unwinds the current page of crops.
Initialize self. See help(type(self)) for accurate signature.
- class spacr.qt.annotate_engine.AnnotateSettings[source]¶
Every knob the Annotate screen exposes, packed into one dataclass.
Sensible defaults let callers instantiate
AnnotateSettings()and override just the handful of fields they care about.
- class spacr.qt.annotate_engine.SaveWorker(db_path: str, annotation_column: str, *, table: str = DEFAULT_PNG_TABLE)[source]¶
Runs in a daemon thread; consumes {png_path: annotation} batches from a Queue and commits them to the DB in coalesced transactions.
Prepare an idle worker; call
start()to spawn its thread.- Parameters:
db_path – path to the SQLite
measurements.db.annotation_column – column in
tableto write into.table – the crop table being annotated. A generated set lands under
png_list_2and onwards, and a writer pointed at the wrong one would put a user’s labels on somebody else’s rows.
- stop(wait: bool = True) None[source]¶
Drain queued writes and stop the writer.
A bounded five-second join used to let the screen disappear while the daemon thread still owned a live SQLite connection. That is unsafe at application shutdown: CPython can finalize the sqlite extension while the thread is still inside it. SQLite already bounds lock waits with its 30-second connection timeout, so a requested blocking stop waits for the thread to close its cursor and connection completely.
- submit(batch: dict, column: str | None = None) None[source]¶
Enqueue a copy of the batch for saving.
- Parameters:
batch –
{png_path: value};Noneclears.column – the column to write; the annotation column when omitted. The Annotate screen’s judgements go to the
<column>_verdictcolumn through this same writer rather than through a second connection: one writer, one queue, one order. A batch for another column that cannot be written is kept in_failed_extraunder its column, apart from_failed_batch, so the annotation batch keeps the shape every reader of it expects.
- spacr.qt.annotate_engine.add_colored_border(img: PIL.Image.Image, width: int, color: str) PIL.Image.Image[source]¶
Return
imgwith an inset colored border ofwidthpx.Kept for parity with the Tk
AnnotateApp(and for callers that want a bordered image out of the pipeline). The Qt grid does NOT use it: its tiles paint their borders in_Thumbnail.paintEventso recolouring one costs a repaint instead of a rebuilt pixmap.- Parameters:
img – image to frame; it is pasted unchanged into an RGB canvas.
width – border thickness in pixels on each side, so the result is
2 * widthlarger in both dimensions.color – PIL colour for the border, e.g. a name or
"#rrggbb".
- spacr.qt.annotate_engine.annotation_batch(paths: Iterable[str], value: int | None) Dict[str, int | None][source]¶
Turn a path list into the batch
SaveWorker.submit()takes.Trivial, and it exists so every auto-annotation source ends at the same call.
Noneclears, exactly as it does for a keystroke.- Parameters:
paths – png_paths to label.
value – the class number, or None to clear.
- Returns:
{png_path: value}.
- spacr.qt.annotate_engine.cache_budget_entries()[source]¶
Measured records for decoded outline arrays retained between draws.
- spacr.qt.annotate_engine.class_counts(db_path: str, annotation_column: str, *, table: str = DEFAULT_PNG_TABLE) List[Tuple[int, int]][source]¶
Return sorted list of (class_value, count) for annotated rows.
- Parameters:
db_path – path to the SQLite measurement database; a missing file gives an empty list.
annotation_column – integer annotation column in
table. Values at or abovespacr.suggest.SUGGESTION_OFFSET(model suggestions) and NULLs are not counted.
- spacr.qt.annotate_engine.clear_column(db_path: str, annotation_column: str, *, table: str = DEFAULT_PNG_TABLE) None[source]¶
Null every value in
annotation_columnofpng_list.- Parameters:
db_path – path to
measurements.db; missing files are ignored.annotation_column – column to reset.
- spacr.qt.annotate_engine.count_rows(db_path: str, image_type: str | None = None, *, table: str = DEFAULT_PNG_TABLE) int[source]¶
Return the number of
png_listrows, optionally filtered byimage_type.- Parameters:
db_path – path to
measurements.db; missing files count as 0.image_type – optional substring to filter
png_pathon.
- spacr.qt.annotate_engine.drop_cache_budget_entry(record_key) bool[source]¶
Evict one decoded array selected by the global memory policy.
- Parameters:
record_key –
(kind, key)pair fromcache_budget_entries();kindis"mask","edge"or"model"(which releases the cached Cellpose outline model instead of an array).- Returns:
whether anything was evicted.
- spacr.qt.annotate_engine.empty_object_filters() Dict[str, Tuple[float | None, float | None]][source]¶
Return all object-filter bounds in their disabled state.
- spacr.qt.annotate_engine.ensure_annotation_column(db_path: str, column: str, *, table: str = DEFAULT_PNG_TABLE) None[source]¶
Add
columnINTEGER totableif missing and index png_path.- Parameters:
db_path – path to the SQLite measurement database; nothing happens if the file does not exist.
column – name of the annotation column to create; an empty name does nothing.
- spacr.qt.annotate_engine.fetch_filtered_paths(db_path: str, annotation_column: str, measurements: List[str], thresholds: List[float], directions: List[str], image_type: str | None = None, *, table: str = DEFAULT_PNG_TABLE) List[Tuple[str, int | None]][source]¶
Return ALL (png_path, annotation) rows matching every one of the measurement/threshold/direction triples.
Rows come from a merge of png_list with the measurement tables (via spacr.io._read_and_join_tables) — same code path as the Tk app — filtered with the same bound SQL image-type expression as normal browsing. NOT, AND, OR, parentheses and SQLite LIKE semantics apply equally when measurement thresholds are enabled. Invalid expressions raise ValueError. Callers paginate the returned list themselves.
- Parameters:
db_path – path to the SQLite measurement database; a missing file gives an empty list.
annotation_column – annotation column returned beside each path; filled with
Noneif the joined tables lack it.measurements – measurement column names to threshold; an empty list gives an empty result. Columns the tables lack are skipped.
thresholds – one cut per measurement, or a single value applied to all of them; an empty list gives an empty result.
directions – one direction per measurement, or a single string or one-item list applied to all;
"higher"keeps rows above the cut,"lower"keeps rows below it, and any other value applies no cut. Mismatched lengths raiseValueError.
- spacr.qt.annotate_engine.fetch_page(db_path: str, annotation_column: str, offset: int, page_size: int, image_type: str | None = None, *, table: str = DEFAULT_PNG_TABLE) List[Tuple[str, int | None]][source]¶
Read one page of (png_path, annotation) rows in insertion order.
- Parameters:
db_path – path to the SQLite measurement database; a missing file gives an empty list.
annotation_column – annotation column read beside
png_path.offset – number of matching rows to skip (SQL
OFFSET).page_size – maximum number of rows to return (SQL
LIMIT).
- spacr.qt.annotate_engine.filter_bound(value) float | None[source]¶
Parse a filter bound, returning
Nonefor empty or invalid input.- Parameters:
value – user-entered bound, typically a number or numeric string;
None, a blank string, anythingfloat()rejects and NaN all giveNone.
- spacr.qt.annotate_engine.filter_channels_pil(img: PIL.Image.Image, channels: Iterable[str] | None = None) PIL.Image.Image[source]¶
Zero out channels not present in
channels(e.g. [‘r’,’g’]).- Parameters:
img – RGB PIL image; it must split into exactly three bands.
- spacr.qt.annotate_engine.filter_key(channel: str, measure: str) str[source]¶
Return the settings key for a channel and measurement pair.
- Parameters:
channel – channel name; stripped and lower-cased to form the prefix of
"<channel>_<measure>".measure – measurement name; stripped and lower-cased to form the suffix.
- spacr.qt.annotate_engine.find_last_annotated_offset(db_path: str, annotation_column: str, page_size: int, image_type: str | None = None, *, table: str = DEFAULT_PNG_TABLE) int | None[source]¶
Return the page-aligned offset of the last annotated row, or None.
- Parameters:
db_path – path to the SQLite measurement database; a missing file gives
None.annotation_column – annotation column scanned; any value other than NULL or
0counts as annotated.page_size – rows per page; the last annotated row’s index is rounded down to a multiple of it.
- spacr.qt.annotate_engine.forget_outline_masks() None[source]¶
Drop every cached mask. For tests, and for a caller changing plates.
- spacr.qt.annotate_engine.gate_paths(db_path: str, gates: Sequence[Any], *, table: str = DEFAULT_PNG_TABLE) List[str][source]¶
png_paths surviving a chain of
spacr.qt.widgets.gate_spec.Gate.The route the Gate Editor was missing. The gate maths is NOT reproduced here –
GateClauseevaluates the chain, exactly as it does when the same gates filter a plot, so a population gated on screen and a population annotated from it are the same population by construction.- Parameters:
db_path – the measurements database.
gates – the gate chain, outermost first.
- Returns:
matching png_path strings.
- spacr.qt.annotate_engine.label_to_hex(val: int | None, dark: bool = True) str | None[source]¶
Map an annotation value to a hex border color.
None / 0 / non-int -> None (no border). 1 -> blue, 2 -> red, 3+ -> golden-ratio hue rotation.
THE COLOURS DEPEND ON THE THEME, because contrast does. The original palette was tuned against a dark tile and measured 3.5-12.0 against #1e1e1e – all comfortable. Against a light tile (#f5f5f5) the SAME colours measure 1.28-4.34, with five of the first six below the 3.0 readability floor:
class 1 #3ea6ff 6.43 on dark 2.38 on light class 4 #55f2d8 11.97 on dark 1.28 on light
That is issue #6 – “labels do not appear with good contrast in the annotation app like they do on Linux machines” – and it is a theme difference rather than a platform one: macOS defaults to the light appearance far more often.
HUE IS PRESERVED so a class keeps its identity across themes; only saturation and value move, deepening the colour until it reads against a pale background.
- Parameters:
val – the annotation value.
dark – True for the dark theme’s palette, False for the light theme’s deepened one.
- spacr.qt.annotate_engine.load_crop_image(path: str, db_path: str | None = None, stored_channel_order: str = 'auto', display_order: str = 'rgb', display_primaries: str = 'rgb') PIL.Image.Image[source]¶
Open one object crop PNG as an 8-bit RGB image in display order.
spacr.crops.read_crop_png()resolves the stored format from the sidecar marker, database, or legacy fallback before applying the requested display order. Sixteen-bit single-channel images are narrowed consistently instead of being clipped by an RGB conversion.Two different questions are kept separate rather than combined into one control:
- stored_channel_order Physical channel order in the file, resolved
from its sidecar marker or database.
'auto'is recommended when metadata is available.- display_order Preferred on-screen order, independent of file
storage. Defaults to
'rgb'.
- Parameters:
path – the crop PNG.
db_path – optional
measurements.db, consulted when the crop folder carries no sidecar marker.display_order – one of
spacr.crops.DISPLAY_ORDERS. Applied AFTER the format is corrected, so the two never fight.
- Returns:
PIL
Imagein RGB mode.
- spacr.qt.annotate_engine.metadata_values(db_path: str, column: str, *, table: str = DEFAULT_PNG_TABLE) List[str][source]¶
The distinct values of one png_list metadata column, sorted.
Read from the database rather than guessed from a naming convention: plates are named by whoever ran them, and a picker offering rows A-H to someone whose plate is numbered is a picker they cannot use.
- Parameters:
db_path – the measurements database.
column – one of
METADATA_COLUMNS.
- Returns:
the distinct values, as strings, sorted; empty when the column or the database is missing.
- Raises:
ValueError – a column outside METADATA_COLUMNS, which would otherwise interpolate an arbitrary name into SQL.
- spacr.qt.annotate_engine.normalize_object_filters(object_filters: Mapping | None = None, object_size=None) Dict[str, Tuple[float | None, float | None]][source]¶
Normalise object-filter bounds and migrate legacy size limits.
Legacy
object_sizelimits are applied to the area filter for each colour channel, with non-positive legacy limits treated as disabled. Explicitobject_filtersvalues take precedence; invalid or empty explicit bounds are disabled, while zero remains a valid explicit bound.- Parameters:
object_filters –
{'r_area': (min, max), ...}; partial maps are accepted and unknown keys are ignored.object_size – Legacy
(min, max)area limits in pixels.
- Returns:
A new dictionary containing every supported key and a pair of floats or
None.
- spacr.qt.annotate_engine.normalize_pil(img: PIL.Image.Image, percentiles: Tuple[float, float] = (1.0, 99.0), normalize_channels: Iterable[str] | None = None) PIL.Image.Image[source]¶
Normalize the given PIL image per-channel using percentile stretch.
If
normalize_channelsis None or empty, the image is returned unchanged (aside from clipping to 8-bit range).- Parameters:
img – grayscale or RGB PIL image; pixel values are clipped to 0-255 before any stretch.
- spacr.qt.annotate_engine.outline_image(base_img: PIL.Image.Image, full_img: PIL.Image.Image, outline_channels: Iterable[str] | None = None, edge_sigma: float = 1.0, edge_thickness: float = 1.0, edge_transparency: float = 100.0, edge_image: bool = False, outline_threshold_factor: float = 1.0, object_size: Tuple[int, int] = (0, 0), outline_method: str = 'otsu', object_filters: Mapping | None = None, should_stop=None) PIL.Image.Image[source]¶
Overlay per-channel object outlines on
base_img.Mirrors AnnotateApp.outline_image (Tk) semantics: for every channel in
outline_channels, compute an Otsu-thresholded foreground mask on the corresponding channel offull_img, extract the boundary, optionally dilate it, then alpha-blend it over the channel inbase_imgwithedge_transparency/100opacity. Peak-normalized so thin edges stay visible.WHICH objects get an outline is decided per plane by
object_filters– an area window and a mean-intensity window for each of red, green and blue.object_sizeis the one-window-for-every-plane setting those replaced and is still honoured: it is migrated onto the three area rows bynormalize_object_filters(), so a caller that passes only it gets exactly what it always got.- Parameters:
base_img – RGB display image to receive the blended outlines. Its current channel filtering is preserved except where an outlined channel is deliberately blanked in outline-only mode.
full_img – unfiltered RGB image supplying the channel intensities used to detect objects, aligned pixel-for-pixel with
base_img.should_stop – optional callable asked before each channel’s Cellpose model construction and forward pass. When it answers True the work is abandoned by raising
OutlineCancelledrather than finishing a page nobody is waiting for;'otsu'outlines are fast enough that they are never interrupted mid-channel.
- spacr.qt.annotate_engine.parse_image_type(expression: str | None) Tuple[str, List[str]][source]¶
Turn an image-type expression into a SQL fragment and its parameters.
The filter used to be one substring matched with
LIKE %x%, which can only ever say what a path MUST contain. There was no way to ask for the complement – “the cells with no pathogen crop” – which is half of most comparisons (issue #7).The grammar is small and deliberately close to what someone would type:
pathogen contains “pathogen” !pathogen does NOT contain it NOT pathogen the same, spelled out cell AND nucleus contains both cell OR nucleus contains either cell AND NOT pathogen mixes them
AND binds tighter than OR, as everywhere else. Terms are matched case-insensitively, since
LIKEis already case-insensitive for ASCII in SQLite and a user typing “Pathogen” means the same thing.EVERY TERM IS A BOUND PARAMETER. Nothing the user types is interpolated into SQL, so a path fragment containing a quote is a path fragment and not an injection.
- Parameters:
expression – the user’s filter, or None/empty for “no filter”.
- Returns:
(sql, params)where sql is a bracketed boolean expression overpng_path, or("", [])when there is nothing to filter.- Raises:
ValueError – on an expression that cannot be read, naming what was wrong – an empty NOT, a dangling operator, unbalanced parentheses.
- spacr.qt.annotate_engine.paths_by_measurements(db_path: str, annotation_column: str, rules: Sequence[Mapping[str, Any]]) List[str][source]¶
png_paths satisfying EVERY
{column, threshold, direction}rule.Several measurements at once is the point: one threshold is a gate, not a population. The rules are ANDed, which is what
fetch_filtered_paths()already does for the settings-panel filter – reused here rather than re-derived, so the auto-annotator and the filter can never disagree about what a threshold means.- Parameters:
db_path – the measurements database.
annotation_column – the column being written (needed by the join).
rules – mappings with
column,thresholdanddirection('higher'or'lower').
- Returns:
matching png_path strings.
- Raises:
ValueError – a rule missing a field, or an unknown direction.
- spacr.qt.annotate_engine.paths_by_metadata(db_path: str, column: str, values: Sequence[str], *, table: str = DEFAULT_PNG_TABLE) List[str][source]¶
png_paths whose
columnis one ofvalues.- Parameters:
db_path – the measurements database.
column – one of
METADATA_COLUMNS.values – the values to select.
- Returns:
matching png_path strings.
- Raises:
ValueError – a column outside METADATA_COLUMNS.