spacr.counting

Manual counting on a points layer — click to add, click to remove, tally.

Counting by eye usually records only a total: there is no record of where each object was counted, no way to recount, and no way to compare two scorers beyond the total.

This module records manual counts on a spacr.layers.PointsLayer, so each click is stored as data. Every marker is a world coordinate on a layer that can be hidden, recoloured, saved and reopened; the tally is derived from the markers rather than typed; and the export is one row per click, so two scorers can be compared point by point and a disputed count can be inspected.

One layer per class

A class is a whole PointsLayer, not a property column on a shared one. That is what buys per-class colour and per-class visibility from the existing model — “hide the uninfected markers and count again” is a checkbox in the layer list, not a feature — and it is why the class colours here are the layer’s own face_color. The undo history that crosses classes lives in the session, which is the one thing a per-layer view cannot own.

Coordinates are world coordinates

A marker placed at 8× zoom and a marker placed at 1× are the same point, and a count exported from a downsampled preview lines up with the full-resolution mask it was counted on. CountingSession.to_frame() writes the world coordinates and the unit they are in, because a column of numbers headed x has been read as pixels when it was µm.

Classes

CountClass

One counted class: a name, a marker colour and a keyboard shortcut.

CountingSession

Manual point-counting session on a LayerStack.

Module Contents

class spacr.counting.CountClass[source]

One counted class: a name, a marker colour and a keyboard shortcut.

Parameters:
  • name – what it is called, in the tally and in the export. Unique within a session — two classes with one name is a count nobody can interpret.

  • color – the marker colour, in any form spacr.layers.to_rgba() takes.

  • shortcut – the key that selects it, blank by default — CountingSession.add_class() is what fills in '1'–'9' by position. A counting session is a keyboard job.

__post_init__() → None[source]

Normalize the name, display colour, and keyboard shortcut.

Returns:

None after storing normalized immutable fields.

Raises:

LayerError – when the class name is blank or the colour is not accepted by spacr.layers.to_rgba().

class spacr.counting.CountingSession(stack: spacr.layers.LayerStack, *, classes: Iterable[Any] | None = None, spacing: spacr.layers.Spacing | None = None, size: float = 12.0, field: spacr.layers.FieldKey | None = None)[source]

Manual point-counting session on a LayerStack.

Parameters:
  • stack – the stack the markers are added to. Its layers supply the spacing, so a marker is placed in the same world as the image being counted.

  • classes – the things being counted. Names, (name, colour) pairs or CountClass instances; defaults to DEFAULT_CLASSES.

  • spacing – the spacing for the marker layers. Defaults to the first 2-D layer’s, which is what makes a count line up with the field.

  • size – marker diameter in world units, so a marker is the same physical size at every zoom.

  • field – the FieldKey being counted, if it is known. It is written into the export, which is what lets a count join the measurement tables instead of being a loose CSV.

Create marker layers and session state for a manual count.

Parameters:
  • stack – layer stack that owns the generated point layers.

  • classes – class specifications, or None for DEFAULT_CLASSES.

  • spacing – marker spacing, or None to inherit the first two-dimensional layer and the stack’s units.

  • size – positive finite marker diameter in world units.

  • field – optional field identity copied into exported rows.

Raises:

LayerError – when the stack, marker size, class name, or class shortcut cannot define an unambiguous counting session.

add(world: Mapping[str, float], name: str | None = None) → int[source]

Place a marker at a world point; returns its index in its layer.

Parameters:

world – world-axis coordinates at which to place the marker.

add_class(spec: Any, *, shortcut_index: int | None = None) → CountClass[source]

Add a class and its marker layer; returns the class.

Parameters:
  • spec – a CountClass, a (name, colour) pair, or a bare name (which is given the next default colour).

  • shortcut_index – zero-based position deciding the '1'–'9' key the class answers to; defaults to the number of classes already added. Ignored when spec already carries a shortcut, and no key is assigned past '9'.

Raises:

LayerError – on a duplicate name.

class_for_shortcut(key: str) → str | None[source]

The class a keystroke selects, or None.

Parameters:

key – keyboard shortcut to look up.

clear(name: str | None = None) → int[source]

Remove every marker of a class, or of every class; returns how many.

counts() → Dict[str, int][source]

{class: how many markers}, in class order.

describe() → str[source]

One line for a status bar, 'nothing counted yet' when empty.

Every class gets a percentage and the total is appended: infected 12 (40%) · uninfected 18 (60%) · 30 total.

detach() → None[source]

Take the marker layers out of the stack, leaving the tally readable.

The counts are still available afterwards: the session keeps its layers, it just stops showing them. What a screen calls when it closes but the number is still wanted.

find(world: Mapping[str, float]) → Tuple[str, int] | None[source]

The (class, index) of the marker under a world point, if any.

Parameters:

world – world-axis coordinates to search around.

Searched over every class, not just the active one, and the topmost class wins a tie. Clicking a marker means “that one”, whatever it was scored as — a counter who has to re-select the class before they can take a marker back will leave the wrong marker there.

fraction(name: str) → float[source]

A class’s share of the total, or 0.0 when nothing is counted.

Parameters:

name – counted class whose share is requested.

The number a manual count is usually for — “42% infected” — computed rather than divided by hand, and 0.0 rather than a ZeroDivisionError on an empty session because a fresh panel asks for it before the first click.

layer(name: str | None = None) → spacr.layers.PointsLayer[source]

The marker layer of a class (the active one by default).

load_frame(frame) → int[source]

Put a previously exported count back on the canvas; returns how many.

Parameters:

frame – marker table containing class and world-axis columns.

Classes the session does not have are added as it goes, so reopening somebody else’s count does not require declaring their classes first. Markers are placed by WORLD coordinate, which is what makes the reload land where the clicks were even on a differently-scaled view.

Raises:

LayerError – if the frame was counted in different units — the coordinates would be silently wrong by whatever the two units differ by.

remove_at(world: Mapping[str, float]) → Tuple[str, int] | None[source]

Remove the marker under a world point; return (class, index) or None.

Parameters:

world – world-axis coordinates whose marker should be removed.

summary()[source]

One row per class: the field key, class, count, fraction and total.

to_csv(path: str, *, summary: bool = False) → str[source]

Write the export to path; returns the absolute path.

Parameters:
  • path – destination CSV path. Missing parent directories are created before the frame is written without its index.

  • summary – write one row per class instead of one per marker.

to_frame()[source]

One row per marker: class, world coordinates, units, field key.

The world coordinates and the unit travel together on purpose: a column headed x has been read as pixels when it was µm, and the two counts differ by a factor nobody notices until the figure is drawn.

toggle(world: Mapping[str, float], name: str | None = None) → Tuple[str, str, int][source]

One click: remove the marker there, or place one if there is none.

Parameters:

world – world-axis coordinates to remove from or add at.

Returns:

(action, class, index) where action is 'added' or 'removed'.

undo() → Tuple[str, str] | None[source]

Reverse the last add or remove; returns (action, class).

A counting session is thousands of clicks and some of them are wrong. Undo covers removals too, so a marker deleted by accident comes back where it was rather than where the cursor now is.

property active: str[source]

The class a new marker gets.

property class_names: Tuple[str, ...][source]

Class names, in the order the classes were added.

property classes: Tuple[CountClass, ...][source]

Every class being counted, in the order they were added.

property total: int[source]

How many markers there are altogether.