neu_eval.overlap

The contingency table between two labelings, and the one pass over the voxels.

The only module that reads an array, so the only place a GPU or block boundary shows. A Contingency keeps its pairs sorted by (a_id, b_id) with no zero counts and no duplicates, which is what makes c1 + c2 == c3 an exact test of additivity.

Ignored labels are asymmetric by default, because 0 means different things: on side a, a partly annotated reference, 0 is “not annotated” and leaves the denominator; on side b, a dense segmentation, 0 inside an annotated region is an error. But many pipelines label membrane 0. Scored with the default ignore_b=(), that membrane becomes one huge segment touching every body, a false merge that inflated VOI 2-7x on real vendor segmentations. Nothing in the array tells the two conventions apart, so pass ignore_b=(0,) when 0 is membrane, and read frac_scored beside every score.

contingency(a, b).T is not contingency(b, a): the second applies the default ignore sets to the swapped sides.

Attributes

Classes

Contingency

Voxel counts for every (a_id, b_id) pair that co-occurs.

Functions

boundary_band(labels[, width, ignore])

Boolean mask of voxels within width voxels of a boundary in labels.

contingency(a, b, *[, ignore_a, ignore_b, mask, labels]) → Contingency

Count the voxels shared by every pair of labels in a and b.

pool(tables) → Contingency

Combine tables from regions whose label spaces are independent.

Module Contents

class neu_eval.overlap.Contingency

Voxel counts for every (a_id, b_id) pair that co-occurs.

a_ids, b_ids and counts are parallel arrays sorted by (a_id, b_id), with only nonzero pairs, always in CPU memory. labels names the two sides; it is part of equality and addition, so tables with swapped sides cannot be summed.

a_totals() → tuple[numpy.ndarray, numpy.ndarray]

(ids, voxels) for side a, ids ascending. The row marginal.

b_totals() → tuple[numpy.ndarray, numpy.ndarray]

(ids, voxels) for side b, ids ascending. The column marginal.

keep_pairs(keep: numpy.ndarray) → Contingency

A table holding only the pairs where keep is True.

Dropped voxels move into n_ignored, so n_scored + n_ignored stays the size of the region and frac_scored stays honest.

relabel_sides(labels: Sequence[str]) → Contingency

The same counts, with the two sides named differently.

restrict(*, a_ids: Iterable | None = None, b_ids: Iterable | None = None) → Contingency

A table restricted to the given labels on either side, or both.

For example, the labels neu_eval.assign.completeness() keeps.

to_frame()

A DataFrame of the pairs, columns named after the two sides. Needs pandas (neu-eval[report]).

property T: Contingency

The same table with the sides exchanged (not the same as re-running contingency() with the arguments swapped; see the module docstring).

a_ids: numpy.ndarray
b_ids: numpy.ndarray
counts: numpy.ndarray
ignore_a: frozenset
ignore_b: frozenset
labels: tuple[str, str] = ('a', 'b')
property n_a: int

Distinct labels on side a that survived the ignore sets.

property n_b: int
n_ignored: int = 0
property n_pairs: int

How many (a_id, b_id) pairs co-occur.

property n_scored: int

Voxels this table counts. Excludes everything ignore_* or a mask dropped.

neu_eval.overlap.boundary_band(labels, width: int = 2, *, ignore: Iterable[int] = (0,))

Boolean mask of voxels within width voxels of a boundary in labels.

For a boundary-tolerant score, pass mask=~boundary_band(gt) to contingency(), so small differences in where a boundary is drawn are not counted. Report it beside the ordinary score, not instead of it: a wide band also removes thin processes entirely. A boundary is any 6-neighbour pair of different labels, ignored values included.

neu_eval.overlap.contingency(a: Any, b: Any, *, ignore_a: Iterable[int] = (0,), ignore_b: Iterable[int] = (), mask: Any = None, labels: Sequence[str] = ('a', 'b')) → Contingency

Count the voxels shared by every pair of labels in a and b.

The arrays must have the same shape and already be registered, e.g. read with read_piece(seg, level=..., crop=gt_piece). ignore_a / ignore_b drop those label values and mask restricts to where it is True; dropped voxels are counted in n_ignored. See the module docstring for the asymmetric defaults, and especially for segmentations that label membrane 0.

Runs where the arrays live, CPU or GPU (both must be on the same one); the table comes back in CPU memory.

neu_eval.overlap.pool(tables: Sequence[Contingency]) → Contingency

Combine tables from regions whose label spaces are independent.

Use + for regions of one labeling, where a shared id is one body. Use this for separately annotated crops, where each numbers its bodies from 1 and a shared id means nothing: adding those tables would fuse unrelated cells. Each table’s ids are renumbered and offset past the previous table’s, as neu-vol relabel does to a volume.

Pooled ids do not name bodies, so the result is for scalar metrics only.

neu_eval.overlap.DENSE_CELL_LIMIT = 4194304