Skip to main content

segmentation_masks

One scan's segmentation cube, reduced to what the measurements need.

parse_mask_json rasterises one B-scan's polygon instances into a (num_classes, num_rows, num_cols) array. Holding that for a whole cube across every label is not viable — at 128 B-scans by 800 by 1024 by 15 labels it is about 1.5 GB. So each B-scan is reduced as it is parsed. Peak memory stays at one B-scan's per-class raster, about 12 MB at those dimensions.

Two reductions come out, and they answer different questions:

  • presenceany over rows, per LABEL: 1 where the class appears anywhere in that column. Combined per group by combine_label_mask into the group's en-face footprint, which drives area, lesion identity, width and geometry.
  • group_depth — per GROUP: the count of voxels the group actually occupies in that column. Drives volume and mean axial thickness.

group_depth is computed per group inside the parse, from the raster while it is still in hand, rather than by reducing a per-label depth afterwards. That is not an optimisation. A per-label voxel COUNT cannot be combined into a group count: reducing counts with maximum under-reports a union whose subtypes sit at different axial positions in one column, and reducing with minimum reports a count for an intersection whose labels never coincide at all. The axial positions needed to do it correctly exist only during the walk.

The two reductions are combined at different stages, deliberately:

  • The footprint collapses axially and THEN combines labels, matching ga_metrics.parse_bscan_predictions (np.any(mask, axis=1) precedes its include/exclude combine). An intersection footprint therefore means "every included label appears SOMEWHERE in this column", and area agrees with the GA path bit for bit.
  • The volume combines labels axially, so it counts voxels the group genuinely occupies.

For a union group those agree on what the group is, and the volume is exact. For an INTERSECTION group they are different questions — co-occurring columns versus co-located voxels — and a column can be in the footprint while no voxel in it belongs to all the included labels. Reporting both would put a positive area beside a near-zero volume in one row, so an intersection group's volume is not reported at all; see parse_bscan_masks.

B-scans with no model output are retained as all-zero rows rather than dropped, so axis 1 is indexed by true B-scan number. A consumer wanting the GA path's compacted view removes those rows itself; the reverse is impossible.

That holds only once trailing PADDING is gone, which is why it is removed here rather than left to a group's na_bscans. The inference cache stores one key per column of a run-wide DataFrame, and that DataFrame was padded to its longest row (pd.DataFrame on ragged lists, then pd.concat unioning columns), so a scan with fewer frames than the widest in its run arrives with trailing nulls that is_na_prediction cannot tell from a frame the model skipped. Those indices are not true B-scan numbers — they are frames that never existed. Left in, they set the length of axis 1, and the image centre distance_from_image_centre is measured about is derived from that length, so they displace it by half the padding width. parse_bscan_masks sizes the axis by declared_num_bscans (the scan's own frame count) or, failing that, by the last B-scan carrying output.

Module

Functions

combine_label_mask

def combine_label_mask(    masks: ScanMasks, spec: MaskGroupSpec,)> numpy.ndarray[typing.Any, numpy.dtype[numpy.uint8]]:

Combine a group's labels into one en-face mask.

Arguments

  • masks: The parsed scan.
  • spec: The group definition.

Returns A (num_bscans, num_cols) uint8 mask. An empty include list yields an all-zero mask — including nothing, never everything, matching the GA parser.

group_reports_volume

def group_reports_volume(spec: MaskGroupSpec)> bool:

Whether a volume is a meaningful quantity for this group.

A union group's footprint and its voxel set describe the same tissue, so its volume is the exact count of voxels under the footprint.

A multi-label INTERSECTION group's do not. Its footprint means "every included label appears somewhere in this column" while its voxel set means "every included label occupies this voxel"; the two can disagree completely, and a column can be in the footprint with no voxel belonging to the whole group. There is no volume to report that does not contradict the area beside it.

A SINGLE-label group is exempt whichever mode it declares, because with one label all and any are the same reduction on both axes — there is no "co-occurring" versus "co-located" distinction to fall foul of. That matters in practice rather than in theory: combine defaults to intersection, so without this a one-label group would silently lose its volume by declaring nothing at all.

Arguments

  • spec: The group definition.

Returns True when the group's footprint and voxel set describe the same tissue.

parse_bscan_masks

def parse_bscan_masks(    bscan_prediction_strs: tuple[Any, ...],    label_indices: Mapping[str, int],    groups: Mapping[str, MaskGroupSpec],    *,    collect_depth: bool = True,    declared_num_bscans: int | None = None,)> ScanMasks:

Parse per-B-scan predictions into per-label presence and per-group depth.

Arguments

  • bscan_prediction_strs: One prediction cell per B-scan, in B-scan order. Typed Any rather than str because these arrive as an object-dtype DataFrame column whose cells are either a prediction string or one of several null spellings — None for a padding B-scan column, pd.NA, a float NaN, or an empty string. Narrowing this to str would be a lie about the input is_na_prediction exists to handle.
  • label_indices: Label name to mask-plane index. Must be contiguous 0..len-1 sized to the mapping — parse_mask_json's requirement. Plane order in the result follows the mapping's values, not its insertion order.
  • groups: The groups to accumulate depth for. Depth has to be combined here, while each B-scan's raster still carries axial positions; a per-label voxel count cannot be combined into a group count afterwards. A group whose combine is "intersection" gets None (see group_reports_volume), and a group declaring lesion_connectivity="volume" additionally has its full 3-D voxel mask retained.
  • collect_depth: Whether to accumulate depth at all. Pass False when only area and en-face geometry are needed; every group's entry is then None.
  • declared_num_bscans: The scan's own frame count, when the caller knows it (a DICOM Number of Frames). Used to size the B-scan axis, so trailing padding is not measured — see the module docstring. None falls back to the last B-scan carrying model output, which is correct whenever the padding is what made the tail NA. A count LOWER than the last B-scan carrying output does not win: real segmentation is never discarded on the strength of a metadata field, so the larger of the two is used and the disagreement is logged.

Returns The scan's reductions.

Classes

ScanMasks

class ScanMasks(    labels: tuple[str, ...],    presence: NDArray[np.uint8],    group_depth: dict[str, NDArray[np.int32] | None],    group_volume_masks: dict[str, NDArray[np.bool_]],    num_bscans: int,    num_rows: int,    na_bscan_indices: tuple[int, ...],    ragged_bscans: tuple[tuple[int, int], ...] = (),    padding_bscan_count: int = 0,):

The reductions of one scan's segmentation cube.

eq=False because the generated __eq__ would compare NDArray members and raise on the ambiguous truth value. Nothing compares two of these, and a method that raises at call time is worse than one that does not exist.

Attributes

  • labels: Label names, in the axis-0 order of presence. Ordered by the parse mapping's values, not its insertion order.
  • presence: (num_labels, num_bscans, num_cols), 1 where the label appears anywhere in that column of that B-scan.
  • group_depth: Group name to (num_bscans, num_cols) voxel counts, already restricted to that group's en-face footprint so per-lesion volumes partition the group total. None for a group whose volume is not a meaningful quantity (an intersection group) or when parsed with collect_depth=False.
  • group_volume_masks: Group name to the group's full (num_bscans, num_rows, num_cols) boolean voxel mask. Present ONLY for a group declaring lesion_connectivity="volume", which needs 3-D connectivity; about 105 MB per group at 128 by 800 by 1024, so it is never retained speculatively.
  • num_bscans: Number of B-scans measured, including those with no model output. Defines the size of axis 1. This is the SCAN's own frame count, not the number of prediction cells supplied: trailing padding is removed (see padding_bscan_count).
  • num_rows: Rasterised B-scan height, needed to sanity-check depth against the axial extent.
  • na_bscan_indices: Indices of B-scans that had no model output, within the measured axis. Their rows are all-zero and indistinguishable from a genuinely empty frame without this, which is the distinction the GA path draws. Trailing padding is NOT listed here — it is not a frame that carried no output, it is not a frame at all.
  • ragged_bscans: (bscan_index, width) for every parsed B-scan rasterised narrower than num_cols. Those columns are zero-padded, which is indistinguishable from measured-and-absent, so the fact is recorded rather than left to a log line.
  • padding_bscan_count: Trailing prediction cells dropped as padding — supplied cells minus num_bscans. Non-zero whenever this scan has fewer frames than the widest scan in its inference run, which is routine. Recorded so a consumer can tell a truncated axis from a scan that genuinely has that many frames.

Variables

  • static labels : tuple[str, ...]
  • static na_bscan_indices : tuple[int, ...]
  • static num_bscans : int
  • static num_rows : int
  • static padding_bscan_count : int
  • static ragged_bscans : tuple[tuple[int, int], ...]
  • label_indices : dict[str, int] - Label name to its axis-0 index.