lesions
Per-lesion extraction and diameter measurement.
Lesion identity is connected components on the en-face (b-scan, column)
footprint, which gives a count matching what a clinician sees on the
projection and volumes that reconcile exactly with the group total.
volume connectivity labels 6-connected components in the group's full 3-D
voxel mask instead, for biomarkers that genuinely stack axially — two fluid
cysts in the same column are two cysts. It needs that mask, which
parse_bscan_masks retains only for a group that asks for it, so volume
without one is a programming error rather than a silent fallback. Note the
consequence: two axially stacked lesions project onto overlapping en-face
cells, so per-lesion CELL counts can sum to more than the group footprint,
while per-lesion VOXEL counts still partition the group total exactly.
Two diameters are reported, because the sampling is anisotropic. A macular cube samples roughly 244 µm between B-scans and 11.5 µm between columns — a factor of twenty. A 125 µm lesion therefore spans about eleven columns and very often a single B-scan:
max_base_width_mmis the widest run of set columns on any one B-scan. It is sampled at the fine spacing, and it is the cross-sectional base width a reading centre measures on a B-scan.max_feret_mmis the widest en-face extent in any direction under the anisotropic scale. Correct for a large confluent lesion, unreliable below about three B-scans of extent.
Gate criteria on the base width; report the Feret diameter.
Module
Functions
extract_lesions
def extract_lesions( footprint: NDArray[np.uint8], depth: NDArray[np.int32] | None, *, connectivity: LesionConnectivity = en_face, volume_mask: NDArray[np.bool_] | None = None, max_lesions: int = 256, group_name: str = '',) ‑> tuple[tuple[RawLesion, ...], int]:Label a group's footprint into lesions and measure each in index space.
Arguments
footprint:(num_bscans, num_cols)combined en-face mask for the group.depth:(num_bscans, num_cols)voxel counts, already restricted to the footprint, so per-lesion volumes sum to the group total.Nonefor a group with no meaningful volume, whose lesions report avoxel_countof 0.connectivity:en_facelabels components on the projection.volumelabels 6-connected components in volume_mask, separating lesions that stack axially in one column.volume_mask: The group's(num_bscans, num_rows, num_cols)voxel mask, fromScanMasks.group_volume_masks. Required when connectivity isvolume.max_lesions: Cap on lesions measured. Beyond it the largest are kept.group_name: Group name, for the truncation warning and the volume-mask error message.
Returns
(lesions, truncated) — the measured lesions ordered largest first, and
how many were dropped by the cap.
Raises
ValueError: If connectivity isvolumeand volume_mask isNone. Falling back to en-face identity would answer a different question under the name of the one that was asked.ValueError: If connectivity is not aLesionConnectivityspelling.
max_base_width_mm
def max_base_width_mm(lesion: RawLesion, scale: VoxelScale) ‑> float:The lesion's widest cross-sectional base width, in mm.
The widest run of set columns on any single B-scan, converted with
pixel_spacing_column_mm. This is the well-sampled diameter and the one to
gate a size criterion on.
Arguments
lesion: The extracted lesion.scale: The cube's voxel dimensions.
Returns The width in mm.
max_feret_mm
def max_feret_mm(lesion: RawLesion, scale: VoxelScale) ‑> float:The lesion's widest en-face extent in any direction, in mm.
Measured from cell EDGE to cell edge along the column axis, and centre to centre along the B-scan axis. The asymmetry is the sampling: a column is about 11.5 µm, so half a cell is 5.75 µm and including it costs nothing, while a B-scan slab is about 244 µm, so extending the coarse axis by half a cell either side would add pure sampling artefact — a lesion one B-scan tall would report that thickness whatever its real width.
Taking the column edges is what makes max_feret_mm >= max_base_width_mm
hold, and it holds by construction rather than by luck. The base width is
the longest contiguous run of columns on one B-scan, and the candidate set
below always contains that B-scan's own two column edges, which span at
least that run. A centre-to-centre measure on both axes gives
(n - 1) * spacing against the base width's n * spacing, which inverts
the two for a lesion one B-scan tall and needed a special case to hide it.
There is no special case here: a single-B-scan lesion's Feret diameter comes
out exactly equal to its base width, which is the honest answer for a shape
the cube resolves in one direction only.
Still unreliable below about three B-scans of extent, where the coarse B-scan sampling dominates the diagonal. Report it, do not gate on it.
Arguments
lesion: The extracted lesion.scale: The cube's voxel dimensions.
Returns The Feret diameter in mm.
Classes
RawLesion
class RawLesion( lesion_id: int, cell_count: int, voxel_count: int, max_width_cols: int, max_width_bscan_index: int, bscan_extent: int, bscan_first: int, bscan_last: int, centroid_bscan: float, centroid_col: float, cells: NDArray[np.int64],):One connected lesion, in cell counts and index space.
Physical conversion is deliberately deferred to the measurement helpers, so the extraction is testable without a scale and one scale cannot be applied twice.
Frozen with an NDArray field: the generated __eq__ raises on the array
comparison and the generated __hash__ fails on the unhashable cells
member, so compare instances field-wise (np.array_equal for cells),
never with ==, and do not put them in a set or use them as dict keys.
Attributes
lesion_id: Component id within its group, 1-based.cell_count: Set cells on the en-face plane — the footprint.voxel_count: Set voxels, summed fromdepthinside the footprint.max_width_cols: Longest run of contiguous set columns on any single B-scan. Not the union of columns across B-scans.max_width_bscan_index: The B-scan carrying that longest run.bscan_extent: Number of B-scans the lesion appears on.bscan_first: Lowest B-scan index it appears on.bscan_last: Highest B-scan index it appears on.centroid_bscan: Mean B-scan index of its cells.centroid_col: Mean column index of its cells.cells:(2, N)array of(bscan, col)indices, retained so the fovea geometry helpers can work on the exact cell set without relabelling.
Variables
- static
bscan_extent : int
- static
bscan_first : int
- static
bscan_last : int
- static
cell_count : int
- static
cells : numpy.ndarray[typing.Any, numpy.dtype[numpy.int64]]
- static
centroid_bscan : float
- static
centroid_col : float
- static
lesion_id : int
- static
max_width_bscan_index : int
- static
max_width_cols : int
- static
voxel_count : int