Skip to main content

fovea_geometry

Fovea-relative geometry: distance, region of interest, angular span.

Every quantity here is measured in millimetre space about a reference point, never in index space. The two en-face axes are sampled about twenty times apart, so an index-space radius would be an ellipse and an index-space angle would be skewed by the same factor.

There is no image-centre fallback. When the fovea landmark is unavailable the caller reports None for every field derived here: a radius or an angular span measured about the image centre answers a different question from the one the protocol asks, and would pass or fail a threshold on that different question with no signal that it had.

Angular span is measured on the lesion's RAW FOOTPRINT, not on its convex hull. A C-shaped atrophy therefore reports the arc it actually occupies, not the near-full circle its hull would close. Raw is the stricter reading, so the error direction is "fails to exclude" rather than "wrongly excludes" — the safer side for a screening proxy whose verdicts a reading centre re-adjudicates. If a reading centre's "surrounds the fovea" turns out to mean the closure, that is a deliberate change of definition, not a bug fix.

Module

Functions

aggregate_angular_span_deg

def aggregate_angular_span_deg(occupancies: Iterable[NDArray[np.bool_]])> float:

Total angle subtended by a group's lesions about the reference point.

The per-lesion occupancies are OR-ed, not summed, so two lesions covering the same arc contribute it once. That is what "an aggregate of more than 270 degrees" asks for.

Arguments

  • occupancies: Per-lesion occupancy arrays from angular_occupancy.

Returns The occupied angle in degrees, 0.0 for no lesions.

angular_occupancy

def angular_occupancy(    lesion: RawLesion,    reference: tuple[float, float],    scale: VoxelScale,    *,    min_radius_mm: float = 0.0,)> numpy.ndarray[typing.Any, numpy.dtype[numpy.bool_]]:

Which angular bins about the reference point the lesion occupies.

Each cell marks every bin its own footprint subtends, not just the bin of its centre. A cell asserts tissue across a slice_thickness_mm by pixel_spacing_column_mm slab, not at a point, so binning only the centre would leave real gaps between adjacent cells wherever the coarse B-scan spacing makes the centre-to-centre angular step wider than one bin -- close to the reference point, a lesion that genuinely encircles it would then read as having holes it does not have.

The trade-off is a bounded over-read at the ends of a genuine gap or sector: each cell's corners span roughly degrees(slice_thickness_mm / r) beyond its own true angular position at distance r, so a real edge is over-stated by about that much, worst near the reference point. This is the deliberate direction of error. The alternative, binning only cell centres, silently under-reports an encircling lesion instead -- and the tissue between B-scans is not imaged at all, so an uncertainty of this order is a property of the sampling, not of this function; no binning scheme resolves it exactly.

Taken to its limit, this term is large right next to the reference point: on a standard 0.244 mm / 0.0115 mm cube, a single cell one column away reports about 176 degrees, so two cells straddling the reference point aggregate past a 270-degree threshold from two segmented pixels.

That is not a defect in the binning, and it cannot be fixed in the plane. At radius r the B-scan sampling gives an angular resolution of about degrees(slice_thickness_mm / r) per sector end — 14 degrees at 1 mm, 4.7 at 3 mm, meaningless at 0.01 mm — and the 244 µm between B-scans is unimaged tissue. Working from the model's polygons instead of the rasterised cells does NOT help: one column out, the B-scan half-slab is 0.122 mm against the column half-cell's 0.0057 mm, 21 times larger, so exact column edges move that 176 degrees only to about 170.

Hence min_radius_mm: below it there is no angular resolution to report. It is a parameter rather than a constant here because the value changes who is recruited, which is a clinical decision and not a geometry one.

Arguments

  • lesion: The extracted lesion.
  • reference: (bscan, column) reference point, in index space.
  • scale: The cube's voxel dimensions.
  • min_radius_mm: Cells whose centre lies within this distance of the reference contribute no angle. 0.0 skips only the containing cell, which is the previous behaviour. Centre distance rather than nearest footprint corner, matching min_distance_from_reference_mm, so the two agree about how far away a cell is.

Returns A boolean array of 360 / ANGULAR_BIN_DEG bins, True where the lesion's footprint reaches. The cell containing the reference point, and every cell inside min_radius_mm, are skipped.

min_distance_from_reference_mm

def min_distance_from_reference_mm(    lesion: RawLesion, reference: tuple[float, float], scale: VoxelScale,)> float:

Distance in mm from the reference point to the lesion's nearest cell.

Nearest cell rather than centroid: "within the central region" asks whether any part of the lesion is inside it, so a large lesion reaching in must not be excluded by a centroid that sits outside.

Arguments

  • lesion: The extracted lesion.
  • reference: (bscan, column) reference point, in index space.
  • scale: The cube's voxel dimensions.

Returns The minimum distance in mm.

roi_mask

def roi_mask(    shape: tuple[int, int],    reference: tuple[float, float],    radius_mm: float,    scale: VoxelScale,)> numpy.ndarray[typing.Any, numpy.dtype[numpy.bool_]]:

A circular region of interest about the reference point.

Arguments

  • shape: (num_bscans, num_cols) of the en-face plane.
  • reference: (bscan, column) reference point, in index space.
  • radius_mm: Radius in mm, not diameter. A protocol naming a diameter (for example, i-SIGHT2's 3000 µm ROI) must pass half of it, 1.5 mm.
  • scale: The cube's voxel dimensions.

Returns A boolean mask, True inside the radius.