spacr.counting

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

The oldest measurement in the building. Somebody opens a field, counts the infected cells by eye, writes a number on a sticky note, and the number is the result: no record of where the clicks were, no way to recount, no way for a second scorer to agree or disagree with anything but the total.

This module is that job done on top of spacr.layers.PointsLayer, so the clicks are 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 looked at rather than argued about.

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 thing being counted: a name, a colour and a shortcut.

CountingSession

Counting by hand over a LayerStack.

Module Contents

class spacr.counting.CountClass[source]

One thing being counted: a name, a colour and a 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]

Counting by hand over 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 — world, 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]

Take away the marker under a world point; returns what went.

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]

Just the names, in order.

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.