spacr.qt.screens.image_scatter¶
Workflow inputs and outputs¶
Image Scatter¶
Plot selected measurement columns with object-crop inspection.
Open: Image UMAP → Image Scatter.
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.
Outputs
Projection and clusters — Image UMAP/PCA coordinate tables, selected clusters and figures for the loaded measurement data.
Figures and table exports — The output location chosen by the tool; exports describe the selected data and filters.
V3 — a scatter plot where every point is the cell it stands for.
A measurement scatter is thousands of dots, and the question anybody actually has about it is what does that one look like. Without an answer, an outlier is a coordinate: you can see that something at (0.9, 12.4) is unusual and you cannot tell whether it is a real phenotype, a debris fragment or two cells segmented as one. That judgement is the whole reason to look at a plot of images rather than a plot of numbers, and it needs the image.
So: hover shows the crop, click opens it. The click goes through
spacr.qt.linked_selection.open_objects(), so this screen does not import
the annotation grid and the grid grows no method for it.
Not stuttering is a design constraint, not an optimisation¶
A hover handler runs sixty-plus times a second and a PNG decode costs
milliseconds, so the obvious implementation — decode the crop under the cursor
in mouseMoveEvent — produces a plot that lags behind the mouse and reads
as broken. Three things keep it honest, and all three are load-bearing:
The point cloud is painted once into a pixmap and blitted. Hover repaints a ring, never ninety thousand dots.
Crop paths are resolved for the whole plot in one database pass at load time (
spacr.qt.crop_thumbs.crop_paths_for_keys()), so hover is a dict lookup rather than a query.Decoding is behind a short debounce and an LRU (
spacr.qt.crop_thumbs.CropThumbnails). Sweeping the cursor across a cluster decodes what the cursor stopped on, not everything it crossed; a crop already in the cache appears with no delay at all.
Classes¶
A measurement scatter with the crop under the cursor beside it. |
|
Points in data space, painted once and blitted. |
Functions¶
|
Every table in |
|
Read one measurement table, ready to plot. Runs on a worker thread. |
|
Build the screen. The one constructor every caller goes through. |
|
Columns worth putting on an axis, in the frame's own order. |
Module Contents¶
- class spacr.qt.screens.image_scatter.ImageScatterScreen(parent=None, *, threaded: bool = True)[source]¶
Bases:
spacr.qt.linked_selection.LinkedView,PySide6.QtWidgets.QWidgetA measurement scatter with the crop under the cursor beside it.
- Parameters:
threaded –
Falseloads inline, so a test drives the screen without a worker thread and gets the same signals in the same order.parent – parent widget; ownership only.
Build the screen, join the shared selection and arm its drop zone.
- Parameters:
parent – parent widget, or
None.threaded – run reads on a worker thread. Set
Falsein tests so a load finishes before it returns.
- closeEvent(event) None[source]¶
Stop background work before going away.
- Parameters:
event – the Qt close event.
- key_at(index: int) str[source]¶
The object key of point
index, or"".- Parameters:
index – the point’s position in the plotted arrays; out-of-range gives
"".
- on_linked_filter_changed(data_filter) None[source]¶
The population narrowed: say so. A filter hides, so re-plot.
Applied to the plotted frame rather than to the source, so clearing the filter widens it back without another database read.
- Parameters:
data_filter – the shared filter; its
describe()text is shown in the status line before the plotted frame is re-plotted.
- on_linked_selection_changed(selection) None[source]¶
Ring the points another view selected.
- Parameters:
selection – the shared selection whose points are ringed.
- open_point(index: int) Any[source]¶
Route point
index’s object to whatever shows crops.- Parameters:
index – the point’s position in the plotted arrays.
- Returns:
what the opener returned, or
Nonewhen there is no key or nowhere to open it.
- path_at(index: int) str[source]¶
The crop path of point
index, or"".- Parameters:
index – the point’s position in the plotted arrays; out-of-range gives
"".
- set_database(path: str) bool[source]¶
Point the screen at
pathand list its tables.The supported way for a caller to seed the source. Image Scatter is opened from Image UMAP as one of the views of the same objects, and a view that made the user retype the database the screen beside it is already reading would be a second module rather than a second view.
An empty path is ignored rather than clearing the box: “no path known” must not throw away a path the user typed.
- Parameters:
path – path to the SQLite measurement database; stripped, and ignored when empty.
- Returns:
True when the path was taken and the table listing started.
- set_frame(frame: pandas.DataFrame, *, keys: Sequence[str] | None = None, paths: Dict[str, str] | None = None, x: str = '', y: str = '', note: str = '') None[source]¶
Plot
frame. The seam a test — or another screen — goes through.- Parameters:
frame – measurement rows to display in the scatter plot.
keys – one object key per row. Derived from the frame when it carries
spacr.selection.OBJECT_KEY_COLUMNSand omitted. Without keys the plot still draws, but a click cannot open anything and says so rather than doing nothing.paths –
{key: crop path}, resolved once by the caller.
- class spacr.qt.screens.image_scatter.ScatterCanvas(parent=None)[source]¶
Bases:
PySide6.QtWidgets.QFramePoints in data space, painted once and blitted.
Kept separate from the screen so the hit-testing and the caching can be tested without a database, and so a second consumer (a facetted view, a comparison grid) can reuse it.
- Parameters:
parent – parent widget.
Create an empty scatter canvas with mouse tracking on.
- Parameters:
parent – parent widget, or
None.
- index_at(x: float, y: float) int[source]¶
Nearest plotted point within
HIT_RADIUS, or-1.One vectorised pass. A spatial index would be faster asymptotically and is not worth the invalidation rules: at 200 000 points this is well under a millisecond, and the row cap in
load_scatter_frame()keeps it there.- Parameters:
x – horizontal widget coordinate, in pixels.
y – vertical widget coordinate, in pixels.
- leaveEvent(event) None[source]¶
Clear the hover when the pointer leaves the canvas.
OTHERWISE THE THUMBNAIL STICKS: the last hovered point stays drawn over a canvas the pointer has left, which reads as a selection.
- Parameters:
event – the Qt leave event.
- mouseMoveEvent(event) None[source]¶
Track which point is under the pointer, for the thumbnail.
- Parameters:
event – the Qt mouse event.
- mousePressEvent(event) None[source]¶
Select the point under the pointer.
- Parameters:
event – the Qt mouse event.
- paintEvent(event) None[source]¶
Draw the points and the hovered thumbnail.
- Parameters:
event – the Qt paint event.
- point_position(index: int) Tuple[float, float] | None[source]¶
Widget coordinates of point
index, orNoneif not drawn.- Parameters:
index – the point’s position in the plotted arrays.
- resizeEvent(event) None[source]¶
Re-lay the points for the new size.
- Parameters:
event – the Qt resize event.
- set_points(x: Sequence[float], y: Sequence[float], *, x_label: str = '', y_label: str = '') int[source]¶
Plot
xagainsty. Returns how many points are plottable.Rows with a non-finite coordinate are kept in the arrays (so indices still line up with the caller’s frame) but never drawn and never hit — dropping them would silently renumber every point and make a click open the wrong object.
- Parameters:
x – horizontal values, one per row, flattened to a float array; non-finite values are kept but not drawn.
y – vertical values, one per
x; a length mismatch raisesValueError.
- set_selected(mask: Sequence[bool]) None[source]¶
Ring these points. A selection highlights; it never hides.
- Parameters:
mask – one boolean per point; a mask of the wrong length clears the selection.
- property plottable: numpy.ndarray[source]¶
Mask of the points with a finite coordinate pair.
- spacr.qt.screens.image_scatter.list_tables(db_path: str) List[str][source]¶
Every table in
db_path, sorted. Worker-thread safe.
- spacr.qt.screens.image_scatter.load_scatter_frame(db_path: str, table: str, *, limit: int = 200000) pandas.DataFrame[source]¶
Read one measurement table, ready to plot. Runs on a worker thread.
Deliberately a module-level function taking strings: it must never touch a widget, and the surest way to guarantee that is for it not to have one.
- Parameters:
db_path – path to the SQLite measurements database.
table – measurement table to read from the database.
limit – a hard row cap. A million-row table plotted at three pixels a point is a solid rectangle, so the cap costs no information and keeps a mis-click on the wrong table from being a two-minute freeze.
- Raises:
FileNotFoundError – no such database — said plainly, because an empty plot from a wrong path looks exactly like an empty table.
- spacr.qt.screens.image_scatter.make_image_scatter_screen(**_kwargs) ImageScatterScreen[source]¶
Build the screen. The one constructor every caller goes through.
- spacr.qt.screens.image_scatter.numeric_columns(frame: pandas.DataFrame) List[str][source]¶
Columns worth putting on an axis, in the frame’s own order.