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:
presence—anyover rows, per LABEL: 1 where the class appears anywhere in that column. Combined per group bycombine_label_maskinto 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. TypedAnyrather thanstrbecause these arrive as an object-dtype DataFrame column whose cells are either a prediction string or one of several null spellings —Nonefor a padding B-scan column,pd.NA, a float NaN, or an empty string. Narrowing this tostrwould be a lie about the inputis_na_predictionexists to handle.label_indices: Label name to mask-plane index. Must be contiguous0..len-1sized 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 whosecombineis"intersection"getsNone(seegroup_reports_volume), and a group declaringlesion_connectivity="volume"additionally has its full 3-D voxel mask retained.collect_depth: Whether to accumulate depth at all. PassFalsewhen only area and en-face geometry are needed; every group's entry is thenNone.declared_num_bscans: The scan's own frame count, when the caller knows it (a DICOMNumber of Frames). Used to size the B-scan axis, so trailing padding is not measured — see the module docstring.Nonefalls 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 ofpresence. 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.Nonefor a group whose volume is not a meaningful quantity (an intersection group) or when parsed withcollect_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 declaringlesion_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 (seepadding_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 thannum_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 minusnum_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
group_depth : dict[str, numpy.ndarray[typing.Any, numpy.dtype[numpy.int32]] | None]
- static
group_volume_masks : dict[str, numpy.ndarray[typing.Any, numpy.dtype[numpy.bool_]]]
- static
labels : tuple[str, ...]
- static
na_bscan_indices : tuple[int, ...]
- static
num_bscans : int
- static
num_rows : int
- static
padding_bscan_count : int
- static
presence : numpy.ndarray[typing.Any, numpy.dtype[numpy.uint8]]
- static
ragged_bscans : tuple[tuple[int, int], ...]
label_indices : dict[str, int]- Label name to its axis-0 index.