spacr.qt.crop_thumbs

Decode object crops lazily and cache the most recent ones for hover previews.

A hover preview shows the crop under the cursor. A PNG decode takes a few milliseconds and a mouse sweep produces about a hundred hover events per second, so decoding on every event makes the plot lag behind the cursor.

Decoding therefore never happens on the paint path, and each crop is decoded once:

  • CropThumbnails.peek() answers from memory or not at all. It is what a hover handler calls, because the answer has to be instant or absent.

  • CropThumbnails.pixmap() decodes on a miss. A caller runs it behind a short debounce (or on a click), never once per mouse-move event.

  • The cache is keyed on (path, mtime, size, px), following spacr.crops, so a crop re-written by a re-run is re-read rather than served stale from the last session’s decode.

Why not QPixmap(path)

Crop PNGs are not ordinary images. Anything spaCR wrote before the BGR fix has the first stain in the blue channel, and a 16-bit single-channel crop opened with convert('RGB') is clipped to solid white by PIL. Both are corrected by spacr.qt.annotate_engine.load_crop_image(), which is therefore the only loader used here, so a hover preview shows the same colours as the annotation grid.

Classes

CropThumbnails

A bounded LRU of decoded crop thumbnails.

Functions

crop_paths_for_keys(→ Dict[str, str])

{object key: crop path}, resolved ONCE for a whole plot.

live_thumbnail_caches()

A snapshot of the decoded-thumbnail caches that still have owners.

resolve_crop_path(→ str)

The crop's path on this machine, re-anchored if the dataset moved.

Module Contents

class spacr.qt.crop_thumbs.CropThumbnails(db_path: str = '', *, size: int = DEFAULT_SIZE, capacity: int = DEFAULT_CAPACITY)[source]

A bounded LRU of decoded crop thumbnails.

Parameters:
  • db_path – the measurements.db the crops belong to. Used to re-anchor moved paths and to resolve the folder’s stored channel order; optional, because a folder of crops is a legitimate source too.

  • size – longest edge of the thumbnails, in pixels.

  • capacity – how many to keep.

Not a QObject, and holds no widget: a screen can own one, a test can drive one, and neither keeps the other alive.

Create the thumbnail cache.

Parameters:
  • db_path – measurements database the crops are read from.

  • size – longest edge of a decoded thumbnail, in pixels.

  • capacity – how many thumbnails to keep; the least recently used are dropped first.

__contains__(path: object) → bool[source]

Report whether a crop path is cached.

Parameters:

path – the crop path; coerced with str().

Returns:

True if it has an entry, including a cached failure.

__len__() → int[source]

Return how many entries the cache holds.

cache_budget_entries()[source]

(key, bytes, last use, in use) rows for the global sweep.

A QLabel/QGraphicsItem that is displaying a pixmap owns its own implicitly-shared Qt value. Removing this lookup entry cannot blank that control, so no thumbnail entry needs pinning here.

clear() → None[source]

Drop everything. For a new source, or a screen closing.

describe() → str[source]

One line of cache health, for a status bar.

drop_cache_budget_entry(key) → bool[source]

Evict one decoded thumbnail selected by the memory policy.

Parameters:

key – cache key of the entry to evict, an (abspath, mtime_ns, size, px) tuple; an unknown key is ignored and False is returned.

peek(path: str) → PySide6.QtGui.QPixmap | None[source]

The thumbnail if it is already decoded, else None. Never blocks.

The call a mouse-move handler makes. Returning None means “not yet”, not “there is no crop” — ask pixmap() for that, off the hover path.

Parameters:

path – path of the crop image; an empty value returns None.

pixmap(path: str) → PySide6.QtGui.QPixmap | None[source]

The thumbnail, decoding it if this is the first time.

Parameters:

path – path of the crop image to decode; an empty value returns None.

Returns:

the QPixmap, or None when the crop cannot be read. A failure is cached as None rather than raised: a missing file under the cursor must not throw out of a mouse handler, and it must not be retried sixty times a second either.

prime(path: str) → PySide6.QtGui.QPixmap | None[source]

Decode path now so a later peek() is instant.

The same as pixmap(); named separately because a debounce timer runs it to fill the cache, not a hover handler.

Parameters:

path – path of the crop image to decode and cache, as for pixmap().

spacr.qt.crop_thumbs.crop_paths_for_keys(db_path: str, keys) → Dict[str, str][source]

{object key: crop path}, resolved ONCE for a whole plot.

spacr.active_learning.crops_for_object_keys() scans the crop table per call — suitable for opening a subset, too slow to run once per hover event. This is the plot-time call: resolve every plotted point up front and let hover be a dict lookup.

That function keeps the caller’s order but drops keys it cannot resolve, so when everything resolves the answer is a zip and costs one scan. When some keys miss, the returned list is a subsequence and no longer lines up, so the range is bisected: each half is asked separately until a half is either fully resolved (zip it) or a single key (resolve it or record the miss). That is a handful of extra scans for a handful of missing crops, rather than one scan per key.

Keys with no crop are absent from the result rather than mapped to "", which would read as a crop path.

spacr.qt.crop_thumbs.live_thumbnail_caches()[source]

A snapshot of the decoded-thumbnail caches that still have owners.

spacr.qt.crop_thumbs.resolve_crop_path(path: str, db_path: str = '') → str[source]

The crop’s path on this machine, re-anchored if the dataset moved.

png_list stores absolute paths built at measure time, so a dataset copied to another disk resolves to nothing and every preview is blank. spacr.qt.screens.annotate._reanchor_png_path() already knows how to rebuild one from the /data/ segment under the database’s own root; this borrows it rather than growing a third copy of that rule, and falls back to the stored path when the Annotate screen is not importable (a headless test, a trimmed install).

Parameters:

path – crop path as stored in png_list; returned unchanged when it is empty or already exists as a file.

Nested helpers

crop_paths_for_keys.resolve(batch) → None

Resolve one batch of keys, bisecting when some are missing.

Never called with an empty batch: the caller refuses an empty key list and a bisection of two or more cannot produce an empty half.

spacr/qt/crop_thumbs.py:334