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.

API reference.

Module tutorial.

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

AnnotateScreen

Main Qt widget for the annotate app.

Functions

badge_colors(→ Tuple[str, str])

(fill, glyph) for the corner badge a judged or judgeable crop wears.

current_ring_color(→ str)

Colour of the "this is the tile you are on" ring.

grid_that_fits(→ Tuple[int, int])

(rows, cols) of tile_w x tile_h tiles that fit a room.

key_token(→ Optional[str])

Normalise key (Qt code, key name or character) to an action token.

on_dark_theme(→ bool)

Is the app currently showing the dark theme?

resting_border_color(→ str)

The thin gray line every unlabelled crop carries.

tile_palette(→ Dict[str, str])

Palette for the theme the app is actually showing right now.

verdict_contradicts(→ bool)

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.QWidget

Main Qt widget for the annotate app.

Parameters:

parent – parent widget.

Build the annotation grid, its controls and its shortcuts.

Parameters:

parent – parent widget.

active_jobs() → int[source]

How many population counts are still winding down.

clear_object_request() → None[source]

Unpin a routed subset and go back to the ordinary population.

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(), and Leave clears the hover.

handle_key(key, text: str = '') → bool[source]

Run the annotate keybinding for key.

key may 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().

is_busy() → bool[source]

True while a population count has not delivered its result.

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 request names, 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.reason becomes 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.

retranslate_dynamic_content(language: str | None = None) → None[source]

Re-render the captions this screen composes from live state.

property current_slot: int[source]

The one tile wearing the white ring — mouse and keyboard agree.

property focus_slot: int[source]

Index of the crop the keyboard currently acts on.

property hover_slot: int | None[source]

Index of the tile the cursor is inside, or None.

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.

fg is 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 colour label_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) of tile_w x tile_h tiles that fit a room.

Pure arithmetic, so a page size can be asserted without a window. margin comes off every edge of the width x height room, gap sits 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 None for 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 PALETTE at 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, -c or 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. destroyed carries the dying QObject and closeEvent passes 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