spacr.qt.crop_thumbs

Decode object crops lazily, and keep the last few — the hover seam.

Showing the crop under the cursor is the difference between a scatter plot of ninety thousand dots and a scatter plot you can interrogate. It is also the easiest way to make a plot unusable: a PNG decode is a handful of milliseconds, a mouse sweep is a hundred hover events a second, and the two multiply into a plot that lags a quarter-second behind the cursor and feels broken.

So nothing here decodes on the paint path, and nothing decodes twice:

  • 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 door used here — a hover preview showing different colours from the annotation grid is worse than no hover preview, because both look plausible.

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 the intent at the call site is different — this is what a debounce timer runs, and reading it as “prime the cache” rather than “get the pixmap” is what stops it drifting back onto the hover path.

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 — right for opening a subset, ruinous 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 "" — “this object has no crop” and “this object’s crop is at nowhere” are different claims and only the first one is true.

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:337