neu_eval.disagree

Where the two labelings disagree, ranked worst-first, with a place to look.

A score cannot say whether the reference or the segmentation was wrong; only looking at the voxels can. This module makes looking cheap: a short list of disagreements, ordered by what each costs, each with a coordinate to paste into a viewer.

Severity is the pair’s VOI contribution; rank="fraction" orders by how broken a body or segment is instead (see rows()).

Specks are filtered asymmetrically. Bodies here are genuinely fragmented into many tiny pieces, and a one-voxel stray segment is 100% of itself, so a filter on “a real fraction of either side” would still call every body split. Whether a body is split is judged on fractions of that body, and whether a segment is a merge on fractions of that segment.

Attributes

Functions

counts_by_kind(report_rows) → dict[str, int]

How many rows of each kind, for the summary line.

locate(report_rows, a, b, *[, labels, at, ignore_a, ...]) → list[dict]

Add a world-coordinate click target to each row.

rows(c, *[, min_frac, min_voxels, top, ...]) → list[dict]

One row per structurally significant pair, worst first.

sample_ids(report_rows, pieces, *[, ignore]) → list[dict]

Add shape["ids"] = {name: [ids at p0 and p1]} for every labelling in pieces.

thin_pairs(c, a, b, *[, radius, min_frac, min_voxels, ...]) → dict[str, float]

{pair_key: interior radius} for the significant pairs with NO real interior.

Module Contents

neu_eval.disagree.counts_by_kind(report_rows: Sequence[dict]) → dict[str, int]

How many rows of each kind, for the summary line.

neu_eval.disagree.locate(report_rows: Sequence[dict], a: Any, b: Any, *, labels: Sequence[str] = ('a', 'b'), at: str = 'seam', ignore_a: Iterable[int] = (0,), ignore_b: Iterable[int] = (), crop: bool = True, max_edt_voxels: int = MAX_EDT_VOXELS, shapes: bool = False, anchor_radius: int = 24, gap: int = 2) → list[dict]

Add a world-coordinate click target to each row.

a and b are the two Piece objects the table was built from.

at="seam" (default) points at the error, the surface where the labelings part:

  • split: the boundary of a ∩ b inside the body, the false cut;

  • merge: the boundary of a ∩ b inside the segment, the false join;

  • tangle: both, and the larger contact wins;

  • match: no seam; falls back to the overlap.

The point is the seam voxel deepest inside the body (split) or segment (merge), so it sits mid-cut. at="overlap" instead gives the deepest voxel of a ∩ b, the middle of what the two agree on. A pair with no seam (a disconnected fragment) falls back to the overlap, and point_at records which was used.

``ignore_a`` / ``ignore_b`` must match the table’s. The seam is contact with another labelled thing, so if membrane is 0 and ignored but counted here, every split point lands on an ordinary cell boundary.

Cost is a few passes per row over the two labels’ bounding box, so bound it with rows(..., top=N). Adds z_nm/y_nm/x_nm (via Piece.to_nm) and the voxel index. shapes=True also adds a drawable description of the error under row["shapes"] (see _shapes()), since a single seam voxel can sit on an incidental contact.

neu_eval.disagree.rows(c: neu_eval.overlap.Contingency, *, min_frac: float = 0.05, min_voxels: int = 1, top: int | None = None, include_matches: bool = False, rank: str = 'severity', max_per_group: int | None = None, exclude: Iterable[str] = ()) → list[dict]

One row per structurally significant pair, worst first.

A pair counts toward “this body is split” only if it holds at least min_frac of the body, and toward “this segment is a merge” only if it holds min_frac of the segment (see the module docstring). min_voxels is a floor on the pair’s size.

rank sets the order:

  • "severity" (default): the pair’s VOI contribution, what to fix first to move the number. It inherits VOI’s size weighting: a 600-voxel body cut in half scores 0.003, a 100,000-voxel body losing a 6% sliver 0.242.

  • "fraction": the larger of the two fractions, so a badly broken small body ranks high. Both values are in every row.

One error can be many rows: a segment merging twenty bodies gives twenty pairs. Each row has group (the segment for a merge or tangle, the body for a split), group_size and group_rank; max_per_group keeps each group’s worst n, after ranking. The rows are not duplicates, since a segment often merges bodies at several places.

include_matches keeps pairs where the two sides agree. exclude is a set of pair_key values treated as not there at all, neither reported nor counted as a partner, as thin_pairs() needs. Returns plain dicts; see neu_eval.tables.

neu_eval.disagree.sample_ids(report_rows: Sequence[dict], pieces: Mapping[str, Any], *, ignore: Mapping[str, Iterable[int]] | None = None) → list[dict]

Add shape["ids"] = {name: [ids at p0 and p1]} for every labelling in pieces.

So an annotation names the objects at its two ends in every layer: the reference bodies, the merging segment, and whatever another segmentation has there.

pieces must be on the grid locate was given (p0_vox / p1_vox index it) and should hold the ids a viewer shows, even if the table was built from connected-component ids. Ignored values are left out.

neu_eval.disagree.thin_pairs(c: neu_eval.overlap.Contingency, a: Any, b: Any, *, radius: float = 3.0, min_frac: float = 0.05, min_voxels: int = 1, ignore_a: Iterable[int] = (0,), ignore_b: Iterable[int] = ()) → dict[str, float]

{pair_key: interior radius} for the significant pairs with NO real interior.

Two labelings draw a cell boundary a voxel or two apart, so each body carries a thin strip of its neighbour’s segment, which can be a large enough fraction to read as a merge or split. Such a strip has no interior: its largest inscribed radius is a voxel or two. A pair is thin when that radius is at most radius voxels.

Only pairs that could become rows() are measured. Pass the keys to rows(..., exclude=...).

neu_eval.disagree.FRAGMENT_SHARE = 0.25
neu_eval.disagree.KINDS = ('match', 'split', 'merge', 'tangle')
neu_eval.disagree.MAX_EDT_VOXELS = 16777216