Skip to main content

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_mm is 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_mm is 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. None for a group with no meaningful volume, whose lesions report a voxel_count of 0.
  • connectivity: en_face labels components on the projection. volume labels 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, from ScanMasks.group_volume_masks. Required when connectivity is volume.
  • 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 is volume and volume_mask is None. 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 a LesionConnectivity spelling.

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 from depth inside 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 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