morphometry
Voxel geometry for en-face and volumetric biomarker measurement.
One home for the scale maths, so the conversion between cell counts and physical units cannot drift between biomarkers. Every function here is pure and takes its physical scale explicitly; nothing reads a config or a cache.
The en-face plane is (b-scan, column): the axial axis collapses under
projection, so an area needs only slice_thickness and
pixel_spacing_column, while a volume needs all three spacings.
Module
Functions
en_face_area_mm2
def en_face_area_mm2(cell_count: int, scale: VoxelScale) ‑> float:Convert an en-face cell count to mm².
The product is evaluated left to right and must not be parenthesised.
Floating-point multiplication is not associative, and every other area site
in this codebase groups left to right; grouping the two spacings first
differs in the final bits and breaks the exact cross-check against the GA
path. test_area_is_not_parenthesised fails on the parenthesised form, so
this is enforced rather than merely requested.
Arguments
cell_count: Number of set cells on the(b-scan, column)plane.scale: The cube's voxel dimensions.
Returns The area in mm².
en_face_distance_grid
def en_face_distance_grid( num_bscans: int, num_cols: int, reference: tuple[float, float], scale: VoxelScale,) ‑> numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]:Euclidean distance in mm from a reference point, per en-face cell.
Built the way the CST step builds its circular sampling region: mm coordinate grids on both en-face axes, then Euclidean distance. Working in mm rather than index space matters because the two axes are sampled about twenty times apart — an index-space radius would be a skewed ellipse.
Arguments
num_bscans: Size of the B-scan axis.num_cols: Size of the column axis.reference:(bscan, column)reference point, in index space.scale: The cube's voxel dimensions.
Returns
A (num_bscans, num_cols) array of distances in mm.
has_en_face_scale
def has_en_face_scale(scale: VoxelScale) ‑> bool:Whether the two en-face axes are usable, so an area can be measured.
Arguments
scale: The cube's voxel dimensions.
Returns
True when slice_thickness_mm and pixel_spacing_column_mm are both
finite and positive. The axial spacing is not consulted.
has_voxel_scale
def has_voxel_scale(scale: VoxelScale) ‑> bool:Whether all three axes are usable, so a volume can be measured.
Arguments
scale: The cube's voxel dimensions.
Returns
True when every spacing is finite and positive.
label_en_face
def label_en_face( mask: NDArray[Any],) ‑> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.int32]], int]:Label 4-connected components on an en-face (b-scan, column) mask.
Matches the GA path's lesion identity, so a lesion count means the same thing in both.
Arguments
mask: A 2-D(num_bscans, num_cols)mask, non-zero where present.
Returns
(labelled, count) — an int array of component ids (0 is background)
and the number of components found.
label_volume
def label_volume( mask: NDArray[Any],) ‑> tuple[numpy.ndarray[typing.Any, numpy.dtype[numpy.int32]], int]:Label 6-connected components on a 3-D (b-scan, row, column) mask.
For biomarkers where axial separation means separate lesions — two fluid cysts stacked in the same column are two cysts. Drusen sit in a thin band above Bruch's membrane and do not have that failure mode, which is why the en-face labeller is the default.
Arguments
mask: A 3-D mask, non-zero where present.
Returns
(labelled, count).
volume_mm3
def volume_mm3(voxel_count: int, scale: VoxelScale) ‑> float:Convert a 3-D voxel count to mm³.
Left-to-right for the same reason as en_face_area_mm2.
Arguments
voxel_count: Number of set voxels.scale: The cube's voxel dimensions.
Returns The volume in mm³.
volume_nl
def volume_nl(voxel_count: int, scale: VoxelScale) ‑> float:Convert a 3-D voxel count to nanolitres.
Arguments
voxel_count: Number of set voxels.scale: The cube's voxel dimensions.
Returns The volume in nL, the unit fluid volumes are already reported in.
Classes
VoxelScale
class VoxelScale( slice_thickness_mm: float, pixel_spacing_row_mm: float, pixel_spacing_column_mm: float,):Physical voxel dimensions for one OCT cube, in millimetres.
Attributes
slice_thickness_mm: Gap between B-scans — the en-face vertical axis.pixel_spacing_row_mm: Axial spacing. Collapses under en-face projection, so it is needed for volume but not for area.pixel_spacing_column_mm: Lateral spacing — the en-face horizontal axis.
Variables
- static
pixel_spacing_column_mm : float
- static
pixel_spacing_row_mm : float
- static
slice_thickness_mm : float