spacr.crop_loader¶
Choose a set of crops, then read them a page at a time.
This module selects crops from a plate and loads them as one pixel stack,
for example for spacr.qt.screens.embeddings.EmbeddingsScreen.set_crops().
It works in two steps so a GUI stays responsive between them.
Planning and loading are separate. plan_crops() asks the database or the folder which objects match and
reads no pixels at all: a COUNT and a SELECT of identifiers, or one
scandir. It is milliseconds on a plate of sixty thousand crops, so a
screen can run it on a click and tell the user what they asked for before
committing to reading any of it. load_crops() then reads the pixels in
pages, reporting after each one. A caller that runs the second half on a
worker thread gets a progress number per page and a stop that takes effect
within a page rather than at the end.
The input is either a measurements.db – where the crops are chosen by
object class, by plate, or by a predicate over png_list – or a folder of
crop PNGs. The pixels come from spacr.crops.resolve_crop_source(), which
decides whether to read pre-generated crops or cut them from
merged/*.npy, and a legacy folder is corrected on load as it is
elsewhere.
The output is one (objects, height, width, channels) uint8 array,
which is the layout spacr.embeddings.embed_array() documents and
spacr.qt.screens.embeddings.EmbeddingsScreen.set_crops() requires, plus
a JSON-serialisable record of what was actually read – how many matched,
how many were taken, the crop shape, and how many crops had to be conformed
to it.
The array can be passed to the Embeddings screen or to
spacr.embeddings.embed_array().
The number of crops is capped by default. Sixty thousand crops of 96x96x3
take 1.6 GB before embedding, which can exhaust memory on a machine that
could embed the first two thousand. CropQuery.limit therefore has a
non-zero default, and the plan states what was left out – “the first 2,000
of 61,433”.
Rows dropped for other reasons are counted separately from the cap. The
merged route drops every row it cannot cut; reporting that as a cap would
suggest raising a limit that was not reached. CropPlan.matched, CropPlan.selected and
CropPlan.count are three numbers, CropPlan.capped is the gap
between the first two and CropPlan.dropped the gap between the last
two, and they reach the panel as separate clauses.
An empty result is reported with a reason, not returned as an empty array.
A plan that matched nothing carries CropPlan.empty_reason, which
names the table, the class, the plate and the predicate that were asked for
and what the database does hold, for example when plate 9 is requested from
an eight-plate screen.
The predicate is SQL and the database is opened read-only. where is an
SQL fragment against the user’s own file. It is spliced into a WHERE
clause, so
the connection is opened with readonly=True – SQLite’s query_only,
which refuses every write at the engine – and a predicate carrying a
statement separator is rejected before it is sent.
Exceptions¶
Raised when a set of crops cannot be planned or loaded. |
Classes¶
Functions¶
|
Centre a crop in a |
|
Read every crop in |
|
Read one page of |
|
Return the object classes this database has crops for. |
|
Plan the crops that match |
|
Choose crops out of a |
|
Choose crops out of a folder of |
|
Every plate the crop table names, sorted. |
Module Contents¶
- exception spacr.crop_loader.CropLoadError[source]¶
Bases:
ValueErrorRaised when a set of crops cannot be planned or loaded.
The message is a sentence suitable for a status bar: every raise in this module names the path, the class or the predicate that produced it, since the calling screen shows this text as the explanation.
Initialize self. See help(type(self)) for accurate signature.
- class spacr.crop_loader.CropPlan[source]¶
What a query matched, before any pixel was read.
- Parameters:
query – the
CropQuerythis answers.rows – one opaque handle per crop, in the order they load – a row mapping for the database source, a path string for the folder one. Never longer than
query.limit.matched – how many crops the query matched in total, before the limit and before the merged route’s join. More than
selectedexactly when the cap bit.selected – how many rows the selection actually took, which is
min(matched, limit). Left at0it defaults tolen(rows), which is right for every plan that has no join to lose rows to.source_label – which pixel route was chosen and why, from
spacr.crops.CropSource.describe().empty_reason – the sentence to show when nothing matched, naming both what was asked and what is there. Empty when something did.
Three counts are kept because rows are removed at two stages, for different reasons.
matchedis what the query found,selectedis what the limit left of it, andcountis what survived the merged route’s join – which drops every row with no single object label ('omulti'/'onone') or no merged array recorded. Only the first gap is a cap, and only a cap is repaired by raising ‘At most’; raising a limit that was not reached returns the same rows.- __post_init__() None[source]¶
Default
selectedto the rows, for a plan built without a join.A plan whose rows came straight out of the selection – the folder route, and anything a test or a script builds by hand – selected exactly what it carries, and should not have to say so.
- describe() str[source]¶
Return a one-line status-bar description of the plan.
Says what was taken out of what, so a capped load reads as a deliberate subset rather than as the whole plate – and names a dropped row as a dropped row, in its own clause, so the two never arrive as one number.
- pages(page_size: int = 0) Tuple[Tuple[int, int], ...][source]¶
(start, stop)for each page, covering every row exactly once.- Parameters:
page_size – crops per page;
0takes the query’s own.
- property capped: bool[source]¶
Whether the limit kept crops out of this plan.
Against
selected, never againstcount: a merged-route load that lost rows to the join hasmatched > countwithout reaching the limit, and that is not a cap.
- class spacr.crop_loader.CropQuery[source]¶
Which crops to load, and how much of the answer to take.
- Parameters:
source –
CROP_SOURCE_DATABASEorCROP_SOURCE_FOLDER.path – the
measurements.db, or the folder of crop PNGs.object_type – which object class the crops are of –
'cell','nucleus','pathogen','cytoplasm'or an organelle role. It picks the object-id column inpng_listand the mask plane a streamed crop is cut by, so it is not cosmetic: asking for nuclei out of a cellpng_listyields nothing rather than yielding cells. Ignored by the folder source, which has no classes.plate – one plate, or
''for every plate.where – an SQL predicate over
png_list, or''. Spliced into theWHEREclause of a read-only connection; see the module docstring for why it is accepted at all.limit – the most crops to take,
0for no cap. The plan records what the cap left behind.page_size – crops read per page by
load_crops().prefer –
'png'to read pre-generated crops,'merged'to cut them frommerged/*.npy,''to letspacr.crops.resolve_crop_source()decide. Database source only.
- spacr.crop_loader.conform_crop(crop: numpy.ndarray, height: int, width: int) numpy.ndarray[source]¶
Centre a crop in a
heightxwidthframe, padding or trimming.The crop is padded, not resized: a stack has to be rectangular before a backbone sees it, and rescaling a crop changes the apparent size of the object in it, which is a phenotype. Zero-padding changes the background, which is not. It is also what
spacr.crop_source.crop_at()already does to a box that runs off the edge of a field, so two crops of the same object arrive the same way whichever route cut them.- Parameters:
crop –
(height, width, channels).height – the target first axis.
width – the target second axis.
- Returns:
cropitself when it already fits, so the common case copies nothing.
- spacr.crop_loader.load_crops(plan: CropPlan, *, progress: Callable[[int, int], None] | None = None, cancelled: Callable[[], bool] | None = None, record: MutableMapping[str, Any] | None = None) numpy.ndarray[source]¶
Read every crop in
plan, a page at a time.Nothing here touches a widget and nothing here is Qt:
progressandcancelledare plain callables, so a screen passes a signal emit and a flag and the same function drives a script or a test.- Parameters:
plan – from
plan_crops().progress – called
(done, total)after each page, on whatever thread this runs on. A caller on a worker thread must not touch a widget from it – emit a signal.cancelled – called before each page; a true answer stops the load and returns the pages already read, which is why the result can be shorter than
plan.count. Checked between pages, so a stop takes at most one page to take effect.record – filled in with what was read –
matched,selected,dropped,loaded,crop_shape,conformed,capped,source,stopped.matchedminusselectedis the cap;selectedminusloadedis rows that could not be cut, plus whatever a stop left unread. JSON-serialisable, so a caller can keep it beside the matrix.
- Returns:
(objects, height, width, channels), the layoutspacr.embeddings.embed_array()documents. Every crop is padded or trimmed to the first one’s frame; seeconform_crop().- Raises:
CropLoadError – the plan is empty, or a page could not be read.
- spacr.crop_loader.load_page(plan: CropPlan, start: int, stop: int, shape: Tuple[int, int, int] | None = None) Tuple[numpy.ndarray, int][source]¶
Read one page of
plan’s crops.- Parameters:
plan – from
plan_crops().start – first row of the page.
stop – one past its last row.
shape –
(height, width, channels)every crop must come back as.Nonetakes the first crop of the page as the template, which is howload_crops()fixes the stack’s shape from page one.
- Returns:
(page, conformed)– the(n, height, width, channels)array, and how many of its crops had to be padded or trimmed to fit.- Raises:
CropLoadError – a crop could not be read, or came back with a different number of channels than the page’s template – which is a real difference between two crops and is not silently padded away.
- spacr.crop_loader.object_classes(db_path: str, table: str = CROP_TABLE) Tuple[str, ...][source]¶
Return the object classes this database has crops for.
A class is offered only when its id column is present AND holds at least one value, because a
png_listwritten for cells carries an emptynucleus_idcolumn on some schemas and offering it would put a choice in the panel that can only ever return nothing.- Parameters:
db_path – the
measurements.db.table – the crop table;
'png_list'.
- Returns:
class names in
spacr.png_list.PNG_LIST_ID_COLUMNSorder. Empty when the database has no crop table.
- spacr.crop_loader.plan_crops(query: CropQuery) CropPlan[source]¶
Plan the crops that match
querywithout reading pixels.- Parameters:
query – what to select.
- Returns:
a
CropPlan. CheckCropPlan.is_emptybefore loading – an empty plan is not an error, and it carries an explanation inCropPlan.empty_reason.- Raises:
CropLoadError – an unknown source, or a path that is not there.
- spacr.crop_loader.plan_from_database(query: CropQuery) CropPlan[source]¶
Choose crops out of a
measurements.db, reading no pixels.The rows come back carrying
png_path– whatspacr.crops.PngCropSourcereads – and, throughspacr.png_list.crop_rows_from_png_list(),path_nameand an integerobject_label, which is whatspacr.crops.MergedCropSourceneeds. So the plan is good for whichever routespacr.crops.resolve_crop_source()picks, and the route is picked here, once, rather than per crop.- Parameters:
query – what to select.
query.sourceis not re-checked.- Returns:
a
CropPlan, possibly an empty one carryingCropPlan.empty_reason.- Raises:
CropLoadError – the database is missing, has no crop table, or refused the selection.
- spacr.crop_loader.plan_from_folder(query: CropQuery) CropPlan[source]¶
Choose crops out of a folder of
*.png, reading no pixels.- Parameters:
query – what to select. Only
pathandlimitapply: a folder records no class and no plate, and saying so inCropPlan.source_labelis better than filtering on a filename convention that no run guarantees.- Returns:
a
CropPlan, possibly an empty one carryingCropPlan.empty_reason.- Raises:
CropLoadError – no folder was chosen, or it is not a directory.
- spacr.crop_loader.plates(db_path: str, table: str = CROP_TABLE) Tuple[str, ...][source]¶
Every plate the crop table names, sorted.
- Parameters:
db_path – the
measurements.db.table – the crop table;
'png_list'.
- Returns:
plate identifiers as strings. Empty when the table has no
plateIDcolumn, which is how a database written before plate keys existed reads rather than an error.