spacr.curation_queue¶
Build, order and record resumable curation sessions over a folder of fields.
This module provides the session workflow of the external curation tool inside spaCR: which fields are waiting, which order to offer them in, how many to offer before the session ends, and what was already decided about each one. The masks themselves are edited in Make Masks.
It holds no pixels and draws nothing. Everything below is ordinary Python
over a folder and a CSV, so it is tested without a display, for example by
tests/test_a_curation_session_resumes_where_it_stopped.py. The module
imports no Qt, needs no display, and imports numpy only inside the
functions that read a draft mask from disk.
Three layouts, all accepted¶
Assuming a single layout can load a folder of unreviewed work as empty, so all three layouts are accepted and the one present is detected:
nested <folder>/*.tif + <folder>/masks/*.tif
sibling <folder>/images/*.tif + <folder>/masks/*.tif
seg <folder>/*_seg.npy (Cellpose pickles, the external tool)
An existing curation set opens without conversion, so no files have to be
moved before work starts. The editor also reads all three layouts and saves
each mask where it was read (see spacr.qt.mask_engine).
The layout is not guessed. A folder that matches none of the three, or that
matches two of them, raises LayoutError, because an empty queue
would report the folder as finished.
Three reviewed states, not two¶
REVIEWED is done, skip and recropped. skip records
that a field cannot be curated and is kept distinct from done; without
it, the same unusable field would be offered again every session. recropped is what the external tool writes
when a bundle has been split into children: the parent is out of the queue
and its pieces are in it.
The record lives at <folder>/curate_status.csv, inside the queue folder
rather than beside it, so it syncs with the images and a session resumes on
the other machine. A row about a stem that is not in this folder is kept on
rewrite for exactly that reason: the curator may be resuming somewhere the
rest of the set has not arrived yet, and dropping those rows would hand back
work already done. A folder with no record of its own but with the external
tool’s <parent>/<name>_status.csv beside it resumes from that one and
writes back to it (status_path()), so a set curated half in each tool
keeps one record.
Ordering¶
The order decides which fields a session with a limit offers first. The
easy, prob, value and name orders, and the constants inside
them, are the external tool’s, chosen on a 500-bundle curation set;
uncertain reads spaCR’s own curate_uncertainty.csv.
order_items() describes each one.
Where this knowingly differs from the external tool¶
Five places, each because this module must serve three layouts and a test suite rather than one folder of Cellpose pickles:
Objects are counted as distinct non-zero labels, not
masks.max(). Seecount_draft_objects()for the two cases where those differ.The median object diameter is computed directly rather than through
skimage.measure.regionprops, so ordering a queue needs numpy alone. It is the same number, and a test pins the equality.An unknown
orderraises instead of silently becomingvalue.nameorder sorts by stem rather than by file name, so the order does not change when one field is a PNG and the next a TIFF.Status rows are written sorted by stem, atomically. The file syncs between machines; a stable order keeps the diff to the rows that changed, and the rename keeps a half-written resume record off the disk.
The nested and sibling layouts also offer an image whose mask is missing,
rather than pairing strictly as the external tool’s bundle builder does: a
field with no draft is still work, drawn from scratch, and the easy
order places it last.
Exceptions¶
A curation queue could not be built, read or written. |
|
The folder does not unambiguously hold one of the three layouts. |
|
|
Classes¶
A bounded session of curation work over one folder. |
|
One field waiting to be curated. |
|
What was found in the folder, and where. |
|
What is left to do in a curation session. |
|
One line of |
Functions¶
|
Open a folder as a bounded curation session. |
|
Return the number of objects in a field's draft mask. |
|
Work out which of the three layouts |
|
Every field in |
|
Return the object count and median object diameter of a field's draft. |
|
Where the external curation tool keeps one of a queue's files. |
|
Return whether a status state takes its stem out of the queue. |
|
Draft object counts for |
|
Read the queue's scores file, when there is one. |
|
Record one decision and write the file, keeping every other row. |
|
Median circle-equivalent diameter of the objects in a label image. |
|
Order a queue, with the external tool's semantics. |
|
Return the fields that have no reviewed state. |
|
Read the resume record. |
|
Return |
|
Return the ordering that will be used and the fallback notice, if any. |
|
Where this queue's probabilities are read from, if anywhere. |
|
Where this queue's resume record is read from and written to. |
|
Count a folder's fields by review state. |
|
Sort key for |
|
Write the resume record, atomically and in full. |
Module Contents¶
- exception spacr.curation_queue.CurationQueueError[source]¶
Bases:
spacr.errors.SpacrErrorA curation queue could not be built, read or written.
Subclass of
spacr.errors.SpacrError, so a caller that catches spaCR’s own errors catches these too.Initialize self. See help(type(self)) for accurate signature.
- exception spacr.curation_queue.LayoutError[source]¶
Bases:
CurationQueueErrorThe folder does not unambiguously hold one of the three layouts.
Raised rather than returning an empty queue, which would report a folder of unreviewed fields as finished.
Initialize self. See help(type(self)) for accurate signature.
- exception spacr.curation_queue.StatusFileError[source]¶
Bases:
CurationQueueErrorcurate_status.csvexists but cannot be read as a resume record.Initialize self. See help(type(self)) for accurate signature.
- class spacr.curation_queue.CurationQueue[source]¶
A bounded session of curation work over one folder.
- Variables:
layout – what was found, and where.
items – the fields to offer this session — unreviewed, ordered, and cut to
limit.status – every status row read, including rows about stems that are not in this folder.
order – the ordering that was asked for.
effective_order – the ordering actually used, which differs from
orderwhenprob,easyoruncertainfell back tovaluefor want of scores.limit – the cap that was applied, or
None.summary – counts over the whole folder, not over this session.
notices – warnings for the curator before the first field – a
prob,easyoruncertainorder with no scores, or with fields the scores do not name, and a resume record that is the external tool’s file beside the folder, which every save then writes to. Printed when the queue is built and shown by the editor with it, so that a changed ordering or a save outside the opened folder is reported.exclude_verdicts – whether fields with a Keep or Discard verdict were dropped from this session; the editor starts with its “Hide judged fields” switch on when this is true.
excluded_by_verdict – number of otherwise waiting fields dropped because they have a verdict. Zero unless
exclude_verdictsis true.
- property all_items: Tuple[QueueItem, ...][source]¶
Every field in the folder, reviewed ones included.
- property folder: pathlib.Path[source]¶
The queue folder.
- class spacr.curation_queue.QueueItem[source]¶
One field waiting to be curated.
- Variables:
stem – the identity of the field, and the key of its status row.
layout – which of
LAYOUTSthis item came from.image – the image file, when the layout has one on disk.
Nonefor asegbundle that carries its own image, unless a display image of the same stem sits beside it.mask – the draft mask file, or
Nonewhen no draft exists yet.bundle – the
_seg.npypickle, for theseglayout only.
- property draft: pathlib.Path | None[source]¶
The file holding this item’s draft labels, or
None.- Returns:
the bundle for a
segitem, the mask file for the other two layouts, andNonewhen there is no draft — which means drawing from scratch, not an error.
- class spacr.curation_queue.QueueLayout[source]¶
What was found in the folder, and where.
- Variables:
kind – one of
LAYOUTS.folder – the queue folder, which is also where the status file goes.
images_dir – where the images are, when they are on disk.
masks_dir – where masks are read from and written back to.
items – every field found, sorted by stem.
- mask_destination(stem: str) pathlib.Path[source]¶
Where a curated mask for
stemis written.The
seglayout writes back into the bundle it was read from, as the external tool does; the other two write<masks_dir>/<stem>.tif, which is whatspacr.qt.mask_engine.mask_save_path()already produces for the nested layout.- Parameters:
stem – the field’s stem.
- Returns:
the path a curated mask belongs at.
- Raises:
CurationQueueError – when
stemis not in this queue.
- property status_path: pathlib.Path[source]¶
where this queue’s resume record is read and written:
<folder>/curate_status.csv, or the external curation tool’s<parent>/<name>_status.csvbeside the folder when that is the record the folder has.status_path()decides.- Type:
returns
- class spacr.curation_queue.QueueSummary[source]¶
What is left to do in a curation session.
- Variables:
total – fields in the folder.
done – curated.
skip – refused — cannot be curated, and must not be offered again.
recropped – split into children, which are in the queue instead.
remaining – fields with no reviewed state.
unknown – status rows about stems that are not in this folder. Kept, never dropped, and reported here rather than silently: on a second machine they are usually work already done elsewhere.
- class spacr.curation_queue.StatusRow[source]¶
One line of
curate_status.csv.- Variables:
stem – which field this is about.
state –
done,skip,recropped, or anything else, which counts as pending. Normalised to lower case with the surrounding whitespace stripped, so a row typed by hand asDonestill ends that stem’s turn.n_objects – how many objects the field had when it was reviewed, or
Nonewhen the file did not say.updated – ISO-8601 timestamp, as written.
- spacr.curation_queue.build_queue(folder: PathLike, order: str = DEFAULT_ORDER, limit: int | None = None, probs=None, counts=None, announce: Callable[[str], None] | None = None, include_reviewed: bool = False, min_objects: int = MIN_OBJECTS_FOR_VALUE, min_diameter: float = MIN_DIAMETER_FOR_VALUE, cache_counts: bool = True, exclude_verdicts: bool = False) CurationQueue[source]¶
Open a folder as a bounded curation session.
In order: detect the layout, read the resume record, drop everything already reviewed, order what is left, and only then apply the limit — so
--limit 20means the first twenty fields of the ordered pending list, not the first twenty found.Probabilities are read only for the orders that use them, so
nameandvalueread no scores file at all;uncertainreadscurate_uncertainty.csvinstead. Forprobandeasya missing scores file is reported: it falls back tovaluewith a notice naming both places it looked; a scores file that leaves some waiting fields unscored says how many; and where the scores came from is printed. Those warnings, and the one that says progress is being written back to the external tool’s record beside the folder, are kept on the queue asCurationQueue.noticesfor the editor to show.- Parameters:
folder – the queue folder.
order – one of
ORDERS;DEFAULT_ORDERby default.limit – how many fields this session offers, or
Nonefor all.probs – probabilities to use instead of reading
curate_scores.csv.counts – draft counts to use instead of reading the drafts.
announce – where notices go; defaults to
print().include_reviewed – offer reviewed fields too. Off by default; this is what “resume” means.
min_objects – passed to
value_key().min_diameter – passed to
value_key().cache_counts – whether
easymay write its count cache.exclude_verdicts – also drop every field with a Keep or Discard verdict in
csv/keep_discard.csv(read from the queue folder and from the folder the editor opens, which differ in thesiblinglayout), in addition to the done and skip rows. The verdicts stay in the file; the number dropped isCurationQueue.excluded_by_verdict.
- Returns:
the
CurationQueue.- Raises:
LayoutError – when the folder is not one unambiguous layout.
StatusFileError – when the status file cannot be read.
ValueError – when
orderis not one ofORDERS, orlimitis negative.
- spacr.curation_queue.count_draft_objects(item: QueueItem) int[source]¶
Return the number of objects in a field’s draft mask.
Counted as distinct non-zero labels rather than as
masks.max(), which is what the external tool uses. The two agree on a fresh Cellpose pickle, whose ids run 1..N, and disagree in the two places that matter here: a label TIFF that has had objects deleted has gaps in its ids, somax()overcounts; and a 0/255 binary mask, which the nested and sibling layouts can hold, would report 255 objects.- Parameters:
item – the field to count.
- Returns:
the number of objects, and
0for a draft that is missing or unreadable — as the external tool’s count cache also does, since counts drive ordering only.
- spacr.curation_queue.detect_layout(folder: PathLike) QueueLayout[source]¶
Work out which of the three layouts
folderholds, and read it.- Parameters:
folder – the queue folder.
- Returns:
the
QueueLayout, with every field found in it.- Raises:
LayoutError – when
folderis not a directory, matches none of the three layouts, or matches more than one of them.CurationQueueError – when two files in it share a stem.
Both cases raise instead of guessing: choosing between two layouts loads only part of the folder, and assuming one that is not there loads none of it and reports the queue as finished.
- spacr.curation_queue.discover_items(folder: PathLike) Tuple[QueueItem, ...][source]¶
Every field in
folder, whatever layout it is in.- Parameters:
folder – the queue folder.
- Returns:
the items, sorted by stem.
- Raises:
LayoutError – as
detect_layout()does.
- spacr.curation_queue.draft_stats(item: QueueItem) Tuple[int, float][source]¶
Return the object count and median object diameter of a field’s draft.
- Parameters:
item – the field to measure.
- Returns:
(n_objects, median_diameter)for its draft, and(0, 0.0)when the field has no draft.- Raises:
Exception – whatever reading an unreadable draft raises. The
valueorder catches that and sorts the field last.
- spacr.curation_queue.external_record(folder: PathLike, suffix: str) pathlib.Path[source]¶
Where the external curation tool keeps one of a queue’s files.
- Parameters:
folder – the queue folder.
suffix –
EXTERNAL_STATUS_SUFFIXorEXTERNAL_SCORES_SUFFIX.
- Returns:
<parent>/<name><suffix>, beside the queue folder rather than in it. The folder is made absolute first, without touching the disk, so.has a name to put in front of the suffix.
- spacr.curation_queue.is_reviewed(state: str | None) bool[source]¶
Return whether a status state takes its stem out of the queue.
- Parameters:
state – a state string, or
None.- Returns:
whether that state takes a stem out of the queue.
done,skipandrecroppedall do, and they stay distinct:skipmeans “cannot be curated” and is recorded separately fromdone.
- spacr.curation_queue.load_draft_counts(folder: PathLike, items: Sequence[QueueItem], write_cache: bool = True, announce: Callable[[str], None] | None = None) Dict[str, int][source]¶
Draft object counts for
items, cached incurate_drafts.csv.Reading three hundred drafts at every launch is slow, so counts are cached by stem and only stems missing from the cache are read. Counts affect ordering only: a stale one mis-sorts a field but loses no work.
An unreadable cache is therefore recounted from the drafts on disk instead of raising. A file truncated by a crash mid-write, for example, is not valid UTF-8.
- Parameters:
folder – the queue folder.
items – the fields to count.
write_cache – whether to write the cache back. A failure to write is announced, never raised — a read-only folder must still open.
announce – where progress goes; defaults to
print().
- Returns:
{stem: n_objects}for every item given.
- spacr.curation_queue.load_probabilities(folder: PathLike) Dict[str, float][source]¶
Read the queue’s scores file, when there is one.
- Parameters:
folder – the queue folder.
- Returns:
{stem: probability}, and{}when there is no scores file (seescores_path()), in which caseprobandeasyfall back tovaluewith a notice.- Raises:
CurationQueueError – when
curate_scores.csvis present but has no stem column or no recognised probability column. A missing scores file is not an error.
- spacr.curation_queue.mark_state(folder: PathLike, stem: str, state: str, n_objects: int | None = None, updated: str | None = None) Dict[str, StatusRow][source]¶
Record one decision and write the file, keeping every other row.
This is the one call a curation session makes per field, and it is the call where rows about absent stems would otherwise be lost.
- Parameters:
folder – the queue folder.
stem – the field being decided.
state – the state to record.
n_objects – how many objects it had, if known.
updated – the timestamp to record; defaults to now.
- Returns:
the rows as they now stand on disk.
- Raises:
StatusFileError – when the file cannot be read or written.
- spacr.curation_queue.median_equivalent_diameter(labels) float[source]¶
Median circle-equivalent diameter of the objects in a label image.
This is
skimage.measure.regionprops’sequivalent_diameter_areafor 2-D input, computed directly assqrt(4 * area / pi)so that ordering a queue needs numpy and not scikit-image. The equality is pinned by a test rather than asserted here.- Parameters:
labels – a 2-D integer label image.
- Returns:
the median diameter in pixels, or
0.0when there are no objects.
- spacr.curation_queue.order_items(items: Iterable[QueueItem], order: str = DEFAULT_ORDER, probs=None, counts=None, announce: Callable[[str], None] | None = None, min_objects: int = MIN_OBJECTS_FOR_VALUE, min_diameter: float = MIN_DIAMETER_FOR_VALUE, scores_hint: PathLike | None = None) List[QueueItem][source]¶
Order a queue, with the external tool’s semantics.
nameBy stem, and nothing else is read — not the probabilities, not the drafts. By stem rather than by file name so that the order does not change when one field is a PNG and the next a TIFF.
probMost likely first: the key is
-probs.get(stem, -1.0). An unscored field yields+1.0, larger than any real negated probability, so it sorts last rather than being treated as probability zero.easy(0 if n > 0 else 1, -probs.get(stem, -1.0), -n). A populated draft is accept-or-light-edit; an empty draft means drawing from scratch, so empty ones go last regardless of probability. Within each group, most likely first, then most objects.valueBy
value_key(): how much draft there is to correct.uncertainMost uncertain segmentation first, the key being
-probs.get(stem, -1.0)withprobscarrying each field’s uncertainty fromcurate_uncertainty.csv. An unscored field sorts last.
Ties are broken by stem, because the list is sorted by stem before any of this and Python’s sort is stable.
- Parameters:
items – the fields to order.
order – one of
ORDERS.probs –
{stem: probability}, or a callable returning one, orNonefor none available; foruncertain,{stem: uncertainty}.counts –
{stem: n_objects}, or a callable returning one. WhenNone,easyreads the drafts itself.announce – where the fallback notice goes; defaults to
print(), so it lands on stdout as the external tool’s does.min_objects – passed to
value_key().min_diameter – passed to
value_key().scores_hint – the scores file named in the fallback notice.
- Returns:
a new list, ordered.
- Raises:
ValueError – when
orderis not one ofORDERS.
- spacr.curation_queue.pending_items(items: Iterable[QueueItem], status: Mapping[str, StatusRow]) List[QueueItem][source]¶
Return the fields that have no reviewed state.
- Parameters:
items – fields in the folder.
status – the rows read from the status file.
- Returns:
the items with no reviewed state, in the order given.
- spacr.curation_queue.read_status(folder: PathLike) Dict[str, StatusRow][source]¶
Read the resume record.
- Parameters:
folder – the queue folder.
- Returns:
{stem: StatusRow}, empty when no status file exists yet. Rows about stems that are not in the folder are included; they are the reasonwrite_status()writes back everything it is given.- Raises:
StatusFileError – when the file exists but has no
stemcolumn, which means it is not a status file at all.
- spacr.curation_queue.record_state(rows: Mapping[str, StatusRow], stem: str, state: str, n_objects: int | None = None, updated: str | None = None) Dict[str, StatusRow][source]¶
Return
rowswith one stem’s decision recorded.Pure: the mapping passed in is not modified, so a caller can decide whether the change reaches the disk.
- Parameters:
rows – the rows read so far.
stem – the field being decided.
state –
done,skip,recropped, or any other string, which leaves the stem pending.n_objects – how many objects it had, if known.
updated – the timestamp to record; defaults to now, to the second.
- Returns:
a new mapping including the recorded row.
- spacr.curation_queue.resolve_order(order: str, probabilities: Mapping[str, float], scores_hint: PathLike | None = None) Tuple[str, str | None][source]¶
Return the ordering that will be used and the fallback notice, if any.
probandeasyneed probabilities anduncertainneeds uncertainty scores. Without them they fall back tovalueand the fallback is announced, so the curator is told that the requested order is not the one in use.- Parameters:
order – the ordering asked for, one of
ORDERS.probabilities – the probabilities available, possibly empty.
scores_hint – the scores file that would have supplied them, named in the message.
- Returns:
(effective_order, announcement), the announcement beingNonewhen nothing changed.- Raises:
ValueError – when
orderis not one ofORDERS, so a misspelled order does not become a different ordering.
- spacr.curation_queue.scores_path(folder: PathLike) pathlib.Path | None[source]¶
Where this queue’s probabilities are read from, if anywhere.
<folder>/curate_scores.csvfirst; failing that, the external curation tool’s<parent>/<name>_scores.csvwhen its header has astemcolumn and a probability column, which is how the plaque project’s scores are written. A file beside the folder that does not look like scores is not a scores file and is passed over rather than allowed to stop the session.- Parameters:
folder – the queue folder.
- Returns:
the scores file, or
Nonewhen there is none.
- spacr.curation_queue.status_path(folder: PathLike) pathlib.Path[source]¶
Where this queue’s resume record is read from and written to.
<folder>/curate_status.csv, inside the queue folder so it travels with the images when the set is synced between machines – unless the folder has none and the external curation tool’s record,<parent>/<name>_status.csv, sits beside it withstemandstatecolumns. That record is then the one read and written back in place: the sets that tool curated keep their progress there and nothing inside the folder, and starting a second file would reopen every field already done or skipped as undone while the other tool went on reading the first.- Parameters:
folder – the queue folder.
- Returns:
the path of the resume record, which need not exist yet.
- spacr.curation_queue.summarize(items: Iterable[QueueItem], status: Mapping[str, StatusRow]) QueueSummary[source]¶
Count a folder’s fields by review state.
- Parameters:
items – every field in the folder.
status – the rows read from the status file.
- Returns:
the
QueueSummary. Counts are over fields that are actually here, sodone + skip + recropped + remaining == total; rows about stems that are not here are reported separately asunknownrather than subtracted from what is left to do.
- spacr.curation_queue.value_key(item: QueueItem, min_objects: int = MIN_OBJECTS_FOR_VALUE, min_diameter: float = MIN_DIAMETER_FOR_VALUE) Tuple[int, float, int][source]¶
Sort key for
valueorder: how much draft there is to correct.Exactly the external tool’s
_value:n == 0 -> (2, 0, 0) nothing to correct, last unreadable -> (3, 0, 0) dead last otherwise -> (0 if rich else 1, -diameter, -n)
- Parameters:
item – the field to rank.
min_objects – objects a “rich” draft needs. Plaque-specific.
min_diameter – median diameter a “rich” draft needs, in pixels. Plaque-specific.
- Returns:
the sort key.
- spacr.curation_queue.write_status(folder: PathLike, rows: Mapping[str, StatusRow]) pathlib.Path[source]¶
Write the resume record, atomically and in full.
Every row given is written, including rows about stems that are not in this folder: the curator may be resuming on a machine where part of the set has not arrived, and dropping those rows offers work back that was already finished somewhere else.
Rows are written sorted by stem. The file syncs between machines, and a stable order keeps a sync diff to the rows that actually changed.
- Parameters:
folder – the queue folder.
rows –
{stem: StatusRow}, asread_status()returns.
- Returns:
the path written.
- Raises:
StatusFileError – when the file cannot be written.