spacr.ops_objects¶
One object list for a well, sewn across window seams and numbered once.
SEGMENT, SEW, NUMBER – the three steps between a composite and an object table. The compose step gives a mosaic that is built a window at a time and never materialised whole; this is what segmentation and numbering do on top of it:
SEGMENT ONCE, on the composite, window by window.
SEW THE LABELS ACROSS WINDOW SEAMS. An object straddling a boundary is one object, matched on its well-frame centroid.
NUMBER IN THE WELL FRAME, NOT PER WINDOW. Per-window numbering would give two windows an object #1 apiece.
THE SEAM PROBLEM IS ALREADY HALF-SOLVED BY THE COMPOSE STEP, and saying how
avoids
reinventing it. spacr.ops_compose.windows_over() tiles with an OVERLAP,
and its contract is that the overlap exceeds the largest object. So a nucleus
near a seam is not split between two windows – it is seen WHOLE by at least
one of them, and usually twice. The job here is therefore deduplication, not
reconstruction: find the observations that are the same nucleus and keep the
one that saw all of it.
THAT DISTINCTION IS THE WHOLE DESIGN. Reconstructing an object from two partial masks means deciding how to union pixels across a seam, which is fiddly and lossy. Choosing between two complete masks is neither.
WHICH LEAVES ONE FAILURE THAT MUST NOT BE SILENT: an object larger than the
overlap, which every window clips. number() refuses those rather than
emitting a fragment, because a fragment looks exactly like a small nucleus
and would be counted as one for the rest of the run.
THE IDS ARE A JOIN KEY, so they are assigned deterministically – raster order on the well-frame centroid – and not by iteration order over a dict or by whichever window happened to be segmented first. The contract is: “This id, and the centroid beside it, is the join key for every later phase.” Two runs over the same data must produce the same numbers or nothing downstream can be compared.
Exceptions¶
An object list that cannot be trusted, with the way out in the text. |
Classes¶
One nucleus, numbered for the whole well. The join key for sampling. |
|
One segmented object as ONE window saw it, in well-frame coordinates. |
Functions¶
|
One id per object, assigned in the well frame, deterministically. |
|
The |
|
Turn one window's label image into well-frame observations. |
|
Run a segmenter over each window and collect the observations. |
|
Group observations that are the same nucleus. |
|
One record per group no window saw whole, for a run report. |
Module Contents¶
- exception spacr.ops_objects.ObjectsError[source]¶
Bases:
ValueErrorAn object list that cannot be trusted, with the way out in the text.
Raised rather than returned empty for the same reason
spacr.ops_compose.ComposeErroris: every one of these is a sentence an operator can act on – “raise the window overlap above the largest nucleus” – and a caller that swallowed it would carry on with a count that is quietly wrong.Initialize self. See help(type(self)) for accurate signature.
- class spacr.ops_objects.PlateObject[source]¶
One nucleus, numbered for the whole well. The join key for sampling.
The field names are the
ops_objectscolumns the storage contract specifies, so the table is written from these without a translation layer.- Parameters:
object_id – the well-wide number. Unique across the whole well, which is the entire reason this type exists – per-window numbering gives two windows an object #1 apiece.
centroid_x – column of the centroid, in well-frame pixels.
centroid_y – row of the centroid, same frame.
area – pixel count of the mask that was kept, which is the UNCLIPPED observation wherever one exists.
bbox –
(top, left, bottom, right)in well-frame pixels, every one of them inclusive, asWindowObjectcarries them.window – the window whose observation was kept, as
(row, col). Recorded so a suspect object can be traced back to the pixels it was segmented from.n_observations – how many windows saw this nucleus. Greater than one means it sat in an overlap and the observations were sewn.
- class spacr.ops_objects.WindowObject[source]¶
One segmented object as ONE window saw it, in well-frame coordinates.
- Parameters:
window – the window it was segmented in, so a disagreement can be traced back to the pixels that produced it.
label – its label value within that window’s label image.
clipped – whether its mask touches a window edge that is not also a canvas edge – meaning this window did NOT see all of it. A clipped observation is never preferred and never emitted alone.
centroid_y – row of the object’s centroid, in WELL-frame pixels – not window-frame. Matching across a seam compares centroids from two different windows, so they have to be in a frame both agree on.
centroid_x – column of the same centroid, same frame.
area – the mask’s pixel count as this window saw it. Smaller than the truth whenever
clippedis set, which is what makes it usable as the tie-break between two observations of one nucleus.bbox –
(top, left, bottom, right)in well-frame pixels, every one of them INCLUSIVE –bottomandrightare the last row and column the mask occupies, so its height isbottom - top + 1. This docstring said exclusive until 2026-09-19 and the code has always been inclusive (_label_extentsreduces withnp.maximum), which cost anyone measuring an extent from it one pixel in each direction.
- spacr.ops_objects.number(groups: Sequence[Sequence[WindowObject]], *, strict: bool = True) Tuple[PlateObject, ...][source]¶
One id per object, assigned in the well frame, deterministically.
RASTER ORDER ON THE WELL-FRAME CENTROID – top to bottom, then left to right – rather than the order windows were segmented in. The id is a join key, so two runs over the same data have to produce the same numbers; ordering by anything the scheduler can vary would break that quietly and only in the results.
Ids start at 1. Zero is background in every label image this came from, and an object numbered 0 would be invisible to any downstream mask comparison.
- Parameters:
groups – from
sew().strict – refuse a group with no complete observation. Turned off, such a group is dropped rather than numbered – nothing with a truncated area is emitted – and how many were dropped is logged as a warning.
- Raises:
ObjectsError – when a group holds only clipped observations: either the object is larger than the window overlap, or it is a fragment that matched no window’s whole view of its nucleus. The message gives the object’s extent and the remedy for each case.
- spacr.ops_objects.objects_frame(objects: Sequence[PlateObject])[source]¶
The
ops_objectstable, as a DataFrame.Imported locally so this module stays usable – and testable – without pandas, which is the same reason
spacr.scorecardreaches for the standard library.- Parameters:
objects – the numbered objects, one row each, in the order given.
- spacr.ops_objects.objects_in_window(window: spacr.ops_compose.Window, labels: numpy.ndarray, *, canvas: Tuple[int, int] | None = None) Tuple[WindowObject, ...][source]¶
Turn one window’s label image into well-frame observations.
- Parameters:
window – where this label image sits in the well.
labels – integer label image, zero is background, as a segmenter returns it. Must match the window’s shape.
canvas – the well’s
(height, width). Used only to tell a window edge that is also the WELL edge – where an object genuinely ends – from an interior seam, where a touching object is clipped. Omitted, every edge is treated as interior, which is the safe direction: it can only make this function more cautious.
- Raises:
ObjectsError – when
labelsis not the window’s shape, because every coordinate below would be silently wrong.
- spacr.ops_objects.segment_windows(windows: Iterable[spacr.ops_compose.Window], segment: Callable[[spacr.ops_compose.Window], numpy.ndarray], *, canvas: Tuple[int, int] | None = None) Tuple[WindowObject, ...][source]¶
Run a segmenter over each window and collect the observations.
THE SEGMENTER IS INJECTED, exactly as
compose_windowtakes itsread_tile. This module never imports cellpose, never chooses a model and never decides a diameter – it is the geometry of doing that window by window, and it is testable on planted labels with no model present.- Parameters:
windows – from
spacr.ops_compose.windows_over().segment – called with one window, returns its label image. A caller composes the pixels (
compose_window) and segments them inside this callable, so the composite is never held whole.canvas – the well’s shape, passed through to
objects_in_window().
- Returns:
every observation from every window, unsewn and unnumbered.
- spacr.ops_objects.sew(observations: Sequence[WindowObject], *, tolerance: float = DEFAULT_CENTROID_TOLERANCE, area_ratio: float = DEFAULT_AREA_RATIO) Tuple[Tuple[WindowObject, ...], ...][source]¶
Group observations that are the same nucleus.
Every group is one physical object. A nucleus in a window overlap is grouped from two (or four, at a corner) observations; one in a window’s interior forms a group of one.
THE CLIPPED ONES ARE THE POINT. An observation whose mask ran into an interior seam did not see the whole nucleus, so its area and centroid are both wrong. It is kept only long enough to be matched to a complete observation of the same nucleus and is then dropped in favour of it. A clipped observation matching nothing complete means no window saw that nucleus whole, which
number()refuses.Two labels of one window are never one object, because a label image does not give one nucleus two labels. Two complete observations from different windows are one nucleus when their centroids agree within
toleranceon each axis and either their areas agree toarea_ratioor their centroids lie within the smaller one’s equivalent radius – the radius of a disc of its area. Pairs whose areas agree are joined first, then the rest closest first, and a join that would give a group two labels of one window is refused.A clipped observation is matched by containment, not by a fixed distance: the cut moves its centroid inward by up to the nucleus’s radius, so a fixed tolerance misses exactly the deep clips. It joins a complete observation from another window when its centroid lies inside that observation’s equivalent disc, or its bounding box lies inside that observation’s box give or take two pixels. It joins only the closest such observation, so one clip can never make two nuclei one object. Clipped observations that match nothing complete are grouped with each other where their boxes overlap.
- Parameters:
observations – every window’s view of every object in one well, already in well-frame coordinates. Order does not matter; the grouping is by geometry, not by arrival.
- Returns:
groups, each a tuple of observations, in no particular order –
number()imposes the order that matters.
- spacr.ops_objects.unseen_records(groups: Sequence[Sequence[WindowObject]]) Tuple[Dict[str, object], ...][source]¶
One record per group no window saw whole, for a run report.
WHY THIS IS NOT THE REFUSAL MESSAGE.
_unseen_report()names the LARGEST group and the remedy, which is what an operator reading one line needs. It is also all that survived well A1’s run: 54 groups were dropped and the report kept one box and a count, so the question the run raised – are these Cellpose fragments with no counterpart, or clips whose complete observation the join missed? – could not be answered afterwards without segmenting the well again. These records are the cheap half of that answer, written while the observations are still in memory.The other half is
spacr.ops_engine.run_ops()’s, which compares each box against the objects that WERE numbered: a refusal with a numbered object over it is a join that missed, and one with empty well frame around it is a fragment.- Parameters:
groups – from
sew()– every group, not only the refused ones; the ones with a complete observation are skipped here.- Returns:
one dict per refused group, in the order the groups came, each carrying the group’s box in well-frame pixels, its centre, how many observations it holds and from how many windows, the largest window side it spans (0 when it spans none), and the summed and largest clipped areas.