spacr.qt.screens.annotate¶
Workflow inputs and outputs¶
Annotate¶
Label representative objects in png_list before supervised training. Use separate annotations for different questions and keep training and evaluation groups independent.
Open: Home → Annotate.
Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.
Inputs
Measured objects — measurements/measurements.db; object tables depend on the enabled cell, nucleus, pathogen and organelle masks. Relevant tables, depending on the route:
cell,nucleus,pathogen,cytoplasm. Relevant columns, depending on the route:plateID,rowID,columnID,fieldID.Object crops — data/**/*_png when save_png is enabled; png_list indexes saved crops. Supported workflows can instead stream crops from merged arrays and masks. Relevant tables, depending on the route:
png_list. Relevant columns, depending on the route:png_path,prcfo.Reusable gates — Saved threshold/polygon gate definitions or a selected object set; apply a gate to the same feature definitions.
Outputs
Training annotations — A chosen annotation column in measurements/measurements.db, table png_list; labels belong to object identities. Relevant tables, depending on the route:
png_list. Relevant columns, depending on the route:prcfo.
Before this module
Measure: Save or stream crops with stable object identities.
External Masks: Keep the newly measured project and crop index together.
Import: Imported tables require explicit column and object-identity mappings.
Gate Editor: Apply compatible gates, then review candidate labels.
After this module
Classify: Keep labelled training and evaluation groups separate.
Annotator Agreement: Choose independent annotation columns for the same objects.
Display and annotate image crops in the Qt interface.
Displays a paginated grid of clickable image thumbnails backed by
png_list in measurements/measurements.db. Left-click assigns value 1,
right-click assigns value 2, and clicking the assigned value again clears it.
Annotations are persisted through
spacr.qt.annotate_engine.SaveWorker.
Where it sits in the workflow. Annotate is the third step of the
pipeline. It needs the crops Measure lists in png_list, and the labels it
writes into an annotation column of that table are what Classify trains on
when dataset_mode is annotation.
A keyboard-only rapid-annotation layer sits on top of the same write
path (see AnnotateScreen.handle_key()): 1–9 assign a class
and auto-advance to the next unlabelled crop, 0 clears, arrows /
hjkl move focus, Space/Backspace step without labelling,
u undoes and Enter commits the page.
Every crop is drawn as a rounded square (see _Thumbnail) with
two independent bands of colour, so its three states stay readable
together rather than overwriting each other:
resting — a thin gray ring hugging the image
classified — that same ring in the class colour (
label_to_hex(), the app’s one class→colour map)current — an extra white ring outside it on the single tile the next click or keystroke will hit
The cursor and the keyboard move the same current tile: entering a tile makes it current, and an arrow key moves it away. There is no second “hovered” highlight that could point somewhere else.
A PAGE IS WHAT FITS. The grid holds exactly the crops that fit
the room the crop pane gives it, at the crop size the settings ask for, and
it never scrolls: opening the console, folding a pane, changing the GUI
scale or resizing the window recomputes the page and pushes the rest to
the next one, the page counter follows, and the first crop on screen stays
where it was. See grid_that_fits() and
AnnotateScreen._refit_grid().
A SUGGESTION IS JUDGED, NOT RELABELLED. A crop Suggest proposed a
class for wears an amber ? badge beside its dashed ring. A click or Y
confirms it (it becomes an ordinary label and wears a green tick); a
right-click or N rejects it (it goes back to unanswered and wears a red
cross); U undoes either. The bar above the key legend says so, and
counts what the page and the column hold. Each judgement is written to the
<column>_verdict column as +c or -c, so it survives a restart,
and the next Suggest round is handed the rejections to train on. See
AnnotateScreen._judge() and spacr.suggest.verdict_column().
Shift + left click blows one crop up to fill the grid’s container, drawn in
front of the tiles rather than reflowing them (_ZoomOverlay); a
click beside it, or Escape, folds it back. It is an EXTRA gesture –
plain left click goes on labelling, because that is what this screen is
for, and the class keys still land while a crop is open.
Annotator Agreement is folded onto this screen’s masthead rather than
carrying a tile of its own: κ between annotation columns is a question
about the labels this screen writes, asked by the person who wrote them
while the crops are still in front of them. The button is that module’s
own icon, lit on hover in its maturity colour, and it opens the agreement
screen itself as a PAGE beside the grid rather than a window over it — a
window is the last resort for a fold, and nothing here needs one. See
FOLDED_APPS.
The Qt screen does not currently provide the UMAP window, Deep spaCR training launcher, or measurement-threshold filtering. A threshold can be entered in settings, but page queries do not apply it.
Classes¶
Main Qt widget for the annotate app. |
Functions¶
|
|
|
Colour of the "this is the tile you are on" ring. |
|
|
|
Normalise |
|
Is the app currently showing the dark theme? |
|
The thin gray line every unlabelled crop carries. |
|
Palette for the theme the app is actually showing right now. |
|
True when a recorded judgement no longer agrees with the label. |
Module Contents¶
- class spacr.qt.screens.annotate.AnnotateScreen(parent: PySide6.QtWidgets.QWidget | None = None)[source]¶
Bases:
PySide6.QtWidgets.QWidgetMain Qt widget for the annotate app.
- Parameters:
parent – parent widget.
Build the annotation grid, its controls and its shortcuts.
- Parameters:
parent – parent widget.
- closeEvent(event)[source]¶
Drain every native/Python worker before Qt destroys this screen.
- Parameters:
event – the close event, passed on to the base class after every worker has been stopped or drained.
- eventFilter(obj, event)[source]¶
Catch keys landing on the scroll area so arrows don’t just scroll.
Also catches the cursor leaving the grid as a whole. A tile’s own Leave normally clears the hover, but the cursor can quit the grid without one (window hidden, cursor warped), and a hover nobody is pointing at any more must not survive.
- Parameters:
obj – the watched object: the grid scroll area, its viewport or the grid holder. Resizes count only on the viewport, and mouse presses, moves and releases drive the selection band only on the grid holder.
event – the event; key presses go to
handle_key(), andLeaveclears the hover.
- handle_key(key, text: str = '') bool[source]¶
Run the annotate keybinding for
key.keymay be a Qt key code, a Qt key name ("Left") or a literal character ("1","h"). Returns True when the key is bound — unbound keys return False and are left for Qt’s default handling. This is the single entry point for the whole keyboard feature so it can be driven directly, without synthesising key events.- Parameters:
key – Qt key code, Qt key name or literal character, normalised with
key_token().
- keyPressEvent(event)[source]¶
Route keystrokes through
handle_key()before Qt’s default.- Parameters:
event – the key event; its key code and text are read, and it is accepted when the key is bound.
- note_console_error() None[source]¶
Reveal “File as issue” because something went wrong.
The button appears in this console when there is an issue, and is hidden until then rather than always present, because a permanently visible report button invites reports with no traceback attached, which are the ones nobody can act on.
Still gated on the user’s opt-in, the same gate the module screens use. Opting in reveals the ACTION; nothing is ever submitted in response to the failure itself.
- open_object_request(request)[source]¶
Show exactly the crops
requestnames, in its order.The one method this screen grows for the whole routing contract. A UMAP point, a confusion-matrix cell and anything added later all arrive here as an
ObjectRequest; none of them imports this module and this module grows no method per caller.The subset pins the grid: pagination, the uncertainty queue and the threshold filter all defer to it until it is cleared, because a request that quietly got replaced by the next queue rebuild would show the user a different population under the same heading. Opening a source, or
clear_object_request(), clears it.- Parameters:
request – the routed request.
request.reasonbecomes the line above the grid — a grid of twelve crops that does not say why reads as the whole screen.- Returns:
this screen, so the caller can raise or focus it.
- resizeEvent(event)[source]¶
Re-fit the thumbnail grid after resize activity settles.
- Parameters:
event – the resize event, passed to the base class; the grid is re-measured from the widget’s new size.
- spacr.qt.screens.annotate.badge_colors(state: str, dark: bool | None = None) Tuple[str, str][source]¶
(fill, glyph)for the corner badge a judged or judgeable crop wears.A suggestion is amber, a confirmed suggestion green and a rejected one red – the theme’s own warning, success and error colours, so the badge follows a theme change the way the rest of the chrome does. The glyph is drawn in the theme’s background colour, which is black on the dark theme and near-white on the light one; each of those fills was chosen against it and reads at better than 4.5:1 in both. None of the three is a class colour, so a badge is never read as a class.
- Parameters:
state – one of
JUDGEMENT_STATES.dark – which theme to answer for; the one on screen when None.
- Returns:
(fill, glyph)hex colours.
- spacr.qt.screens.annotate.current_ring_color() str[source]¶
Colour of the “this is the tile you are on” ring.
fgis pure white on the (default) dark theme — exactly the white border the feature asks for — and flips to the near-black foreground on the light theme, where white would vanish into the background. Either way it is a colourlabel_to_hex()can never produce (class 1 is blue, 2 red, 3+ are HSV rotations at saturation 0.65), so the current ring is never mistaken for a class.
- spacr.qt.screens.annotate.grid_that_fits(width: int, height: int, tile_w: int, tile_h: int, *, gap: int = 0, margin: int = 0) Tuple[int, int][source]¶
(rows, cols)oftile_wxtile_htiles that fit a room.Pure arithmetic, so a page size can be asserted without a window.
margincomes off every edge of thewidthxheightroom,gapsits between tiles and not after the last one, and there is always at least one row and one column: a room smaller than a tile shows one crop rather than none.- Parameters:
width – the room’s width, in the same pixels as the tiles.
height – the room’s height.
tile_w – one tile’s width, rings included.
tile_h – one tile’s height, rings included.
gap – the layout’s spacing between tiles.
margin – the layout’s margin on each edge.
- Returns:
(rows, cols).
- spacr.qt.screens.annotate.key_token(key, text: str = '') str | None[source]¶
Normalise
key(Qt code, key name or character) to an action token.Returns
Nonefor anything the annotate screen does not bind, so callers can fall through to the default Qt handling.- Parameters:
key – Qt key code (an int or int-like value), a key name such as
"Left", or a literal character such as"1".
- spacr.qt.screens.annotate.on_dark_theme() bool[source]¶
Is the app currently showing the dark theme?
The Annotate grid paints raw colours rather than being QSS-styled, so it resolves this itself – see
tile_palette(), which does the same for the tile chrome.
- spacr.qt.screens.annotate.resting_border_color() str[source]¶
The thin gray line every unlabelled crop carries.
- spacr.qt.screens.annotate.tile_palette() Dict[str, str][source]¶
Palette for the theme the app is actually showing right now.
The Annotate grid paints raw colours (it is not QSS-styled), so it has to resolve dark/light itself instead of importing the dark
PALETTEat module scope — a hard-coded gray is invisible on one of the two themes.
- spacr.qt.screens.annotate.verdict_contradicts(verdict: int | None, value: int | None) bool[source]¶
True when a recorded judgement no longer agrees with the label.
A confirmation of class c stands while the crop is labelled c; a rejection of class c stands until the crop is labelled c after all. Relabelling a confirmed crop, or labelling a rejected one as the class that was rejected, withdraws the judgement, so the verdict column never says something the annotation column contradicts.
- Parameters:
verdict –
+c,-cor None.value – the crop’s label, a suggestion or None.
- Returns:
True when the verdict should be withdrawn.
Nested helpers¶
- AnnotateScreen._build_ui._append_error_and_offer_the_report(text, *args, **kwargs)¶
Show the error as before, then offer to file it.
WRAPS rather than replaces, so the original behaviour is unchanged even if the offer fails – an error message must still appear when the thing that would report it is broken.
spacr/qt/screens/annotate.py:3911
- AnnotateScreen._choose_the_test_data._next(result=True, error='')¶
Start the next missing half, or open the plate after the last.
spacr/qt/screens/annotate.py:4043
- AnnotateScreen._choose_the_test_data._restore()¶
Give the button back, whether the download worked or not.
spacr/qt/screens/annotate.py:4035
- AnnotateScreen._end_blind.ask()¶
Confirm revealing crop origins and recording the unblind event.
spacr/qt/screens/annotate.py:5214
- AnnotateScreen._follow_path_probes.landed(path: str, answer: bool) None¶
Re-run the suggestion when the probe answers about its path.
- Parameters:
path – the path whose answer just changed. Every probe in the process arrives here, so anything but this screen’s own remembered source is ignored.
answer – what the probe now says. Only True is acted on – a folder that is gone leaves the placeholder standing.
- Returns:
nothing; the subtitle is the output.
spacr/qt/screens/annotate.py:3522
- AnnotateScreen._follow_path_probes.let_go(*_args) None¶
Withdraw this screen from the process-wide probe signal.
- Parameters:
_args – whatever the caller passes.
destroyedcarries the dying QObject andcloseEventpasses nothing, and this reads neither.- Returns:
nothing. Safe to call twice: the second call is a no-op rather than a disconnect of an absent slot.
spacr/qt/screens/annotate.py:3546
- AnnotateScreen._on_file_issue._send()¶
Post the approved report. Off the GUI thread; never raises.
spacr/qt/screens/annotate.py:4283
- AnnotateScreen._on_similar_result_plate.show_source_hits()¶
Rebuild the new plate’s grid before presenting its matches.
spacr/qt/screens/annotate.py:5085
- _SettingsDialog._load_the_example_data._done(result, error)¶
Restore the button whether the load worked or failed.
spacr/qt/screens/annotate.py:2691
- _SettingsDialog._load_the_streaming_example._done(result, error)¶
Restore the button whether the load worked or failed.
spacr/qt/screens/annotate.py:2725
- _blind_order.position(row)¶
Order known crops by the blind key and unseen crops by path hash.
spacr/qt/screens/annotate.py:2118
- _blind_scrub.token(match)¶
One token’s stand-in, keeping trailing punctuation outside it.
spacr/qt/screens/annotate.py:2201