metrics
The output dataclasses of the per-scan calculation steps.
One dataclass per algorithm (GA, fluid volume, CST, GCC), each able to flatten
itself into a DataFrame record (to_record) and to advertise the columns a
criterion may match on (field_specs). The untyped per-label dict fields are
dropped by to_record and re-projected as flat per-label columns on request.
Classes
CSTMetrics
class CSTMetrics( *, cst_mean_um: float | None = None, cst_median_um: float | None = None, cst_std_um: float | None = None, cst_n_samples: int | None = None, cst_radius_mm: float | None = None, fovea_coordinates: tuple[float, float, float] | None = None, ilm_layer_present: bool = False, rpe_layer_present: bool = False, inner_layer_used: str | None = None, outer_layer_used: str | None = None, measurement_type: str | None = None, ez_intact_max_span_um: float | None = None, ez_attenuated_max_span_um: float | None = None, ez_measured_width_um: float | None = None, ez_reference_bscan_index: int | None = None, ez_reference_type: str = 'unavailable', ez_layers_measured: bool = False,):Output of the CST/CRT calculation algorithm.
The ez_* fields measure ellipsoid-zone integrity on the single B-scan
the fovea model reports as central, and are populated only by
cst_calculation v2 with an attenuation threshold configured. They live
here rather than in their own metrics type because the measurement needs
exactly the inputs CST already takes — the layer predictions and the fovea
centre — and reuses the boundary grid CST already builds.
Every field defaults to its empty state, so a row cached by v1 and
reconstructed as CSTMetrics(**value) stays valid.
Attributes
ez_intact_max_span_um: Widest contiguous run of A-scans whose EZ-to-RPE separation exceeds the configured threshold. This is the gate.ez_attenuated_max_span_um: Widest contiguous run measurable but not exceeding it — including where EZ is absent while RPE is present, which is total attenuation rather than no attenuation.ez_measured_width_um: Width of the reference B-scan that could be measured at all. Zero makes both spans absence of evidence.ez_reference_bscan_index: Which B-scan was measured.ez_reference_type: Where that B-scan's identity came from.ez_layers_measured: Whether the EZ layer pair was available, so an empty span distinguishes "measured, none found" from "not measured".
Variables
- static
cst_mean_um : float | None
- static
cst_median_um : float | None
- static
cst_n_samples : int | None
- static
cst_radius_mm : float | None
- static
cst_std_um : float | None
- static
ez_attenuated_max_span_um : float | None
- static
ez_intact_max_span_um : float | None
- static
ez_layers_measured : bool
- static
ez_measured_width_um : float | None
- static
ez_reference_bscan_index : int | None
- static
ez_reference_type : str
- static
fovea_coordinates : tuple[float, float, float] | None
- static
ilm_layer_present : bool
- static
inner_layer_used : str | None
- static
measurement_type : str | None
- static
outer_layer_used : str | None
- static
rpe_layer_present : bool
Static methods
expected_cols
def expected_cols() ‑> list[str]:Returns the expected columns for a dataframe.
field_specs
def field_specs() ‑> list[FieldSpec]:SCAN-grain FieldSpecs for CST scalar columns.
Methods
to_record
def to_record(self) ‑> dict[str, typing.Any]:Convert to a record format compatible with pd.DataFrame.from_records().
FluidVolumeMetrics
class FluidVolumeMetrics( *, total_fluid_volume: float, smallest_lesion_volume: float, largest_lesion_volume: float, num_bscans_with_fluid: int, num_fluid_lesions: int, distance_from_image_centre: float, max_cnv_probability: float, max_fluid_volume_bscan_index: int | None, segmentation_volumes: dict[str, float],):Output of the Fluid Volume calculation algorithm.
Variables
- static
distance_from_image_centre : float
- static
largest_lesion_volume : float
- static
max_cnv_probability : float
- static
max_fluid_volume_bscan_index : int | None
- static
num_bscans_with_fluid : int
- static
num_fluid_lesions : int
- static
segmentation_volumes : dict[str, float]
- static
smallest_lesion_volume : float
- static
total_fluid_volume : float
Static methods
expected_cols
def expected_cols() ‑> list[str]:Returns the expected columns for a dataframe.
field_specs
def field_specs() ‑> list[FieldSpec]:SCAN-grain FieldSpecs for fluid-volume scalar columns.
Methods
to_record
def to_record(self) ‑> dict[str, typing.Any]:Convert to a record format compatible with pd.DataFrame.from_records().
FoveaReference
class FoveaReference(*, centre: tuple[int, int, int], reference_type: str):A detected fovea landmark and how it was obtained.
The "no landmark" state is LesionMetrics.fovea_reference being None,
not an instance with a missing centre. A centre therefore only ever
exists alongside a reference_type, so the inconsistent "centre present
but provenance unavailable" combination cannot be built: reference_type
is never the "unavailable" sentinel, which is synthesized at emission
time for the None case.
GAMetrics
class GAMetrics( *, total_ga_area: float, smallest_lesion_size: float, largest_lesion_size: float, num_bscans_with_ga: int, num_ga_lesions: int, distance_from_image_centre: float, max_cnv_probability: float, max_ga_bscan_index: int | None, segmentation_areas: dict[str, float], max_pathology_probabilities: dict[str, float], n_scan_run_lengths: dict[str, int] = {}, raw_pathology_probabilities: dict[str, np.ndarray[typing.Any, np.dtype[np.float64]]] | None = None,):Output of the GA calculation algorithm.
Attributes
total_ga_area: Total area of GA in the image in mm^2.smallest_lesion_size: Size of the smallest lesion in the image in mm^2.largest_lesion_size: Size of the largest lesion in the image in mm^2.num_bscans_with_ga: Number of B-scans with GA in the image.num_ga_lesions: Number of GA lesions in the image.distance_from_image_centre: Distance from the image centre to the nearest lesion in mm.max_cnv_probability: Maximum probability of CNV across all B-scans.max_ga_bscan_index: Index of the B-scan with the largest GA lesion.segmentation_areas: Area of each segmentation class in mm^2.n_scan_run_lengths: Longest consecutive-B-scan run (at each biomarker's per-scan threshold) per N-scan biomarker label. Defaults to an empty dict so a cache blob written before this field existed still constructs (rather than raising on a missing keyword argument);to_recordflattens an empty dict the same as any other absent label, so this carries no behavioural cost.
Subclasses
Variables
- static
distance_from_image_centre : float
- static
largest_lesion_size : float
- static
max_cnv_probability : float
- static
max_ga_bscan_index : int | None
- static
max_pathology_probabilities : dict[str, float]
- static
n_scan_run_lengths : dict[str, int]
- static
num_bscans_with_ga : int
- static
num_ga_lesions : int
- static
raw_pathology_probabilities : dict[str, numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]] | None
- static
segmentation_areas : dict[str, float]
- static
smallest_lesion_size : float
- static
total_ga_area : float
Static methods
expected_cols
def expected_cols() ‑> list[str]:Returns the expected columns for a dataframe.
field_specs
def field_specs() ‑> list[FieldSpec]:The matchable SCAN-grain FieldSpecs this metrics type contributes.
Methods
to_record
def to_record( self, additional_pathology_prob_cols: Collection[str] | None = None, additional_area_cols: Collection[str] | None = None, additional_run_length_cols: Collection[str] | None = None,) ‑> dict[str, typing.Any]:Convert to a record format compatible with pd.DataFrame.from_records().
The scalar fields are emitted as-is; the untyped dict fields
(max_pathology_probabilities, raw_pathology_probabilities,
n_scan_run_lengths) are dropped and, when requested, re-projected
into flat per-label columns.
Arguments
additional_pathology_prob_cols: Pathology labels to flatten out ofmax_pathology_probabilitiesintomax_<label>_probabilitycolumns. A label absent from the map becomesnp.nan(→unknowndownstream), never a fabricated0.0.Noneemits no probability columns.additional_area_cols: Segmentation labels to flatten out ofsegmentation_areasinto<label>_areacolumns. A PRESENT key (incl.0.0) is a measured area; an ABSENT key becomesnp.nan(→unknown), so a criterion on a label outside the model's measured set does not silently pass a threshold.Noneemits no area columns.additional_run_length_cols: N-scan biomarker labels to flatten out ofn_scan_run_lengthsinton_scan_run_<label>columns. A label absent from the map becomesnp.nan(→unknowndownstream), never a fabricated0.Noneemits no run-length columns.
Returns
A {column: value} record: the scalar fields plus any requested
flattened probability/area/run-length columns.
GAMetricsWithFovea
class GAMetricsWithFovea( *, total_ga_area: float, smallest_lesion_size: float, largest_lesion_size: float, num_bscans_with_ga: int, num_ga_lesions: int, distance_from_image_centre: float, max_cnv_probability: float, max_ga_bscan_index: int | None, segmentation_areas: dict[str, float], max_pathology_probabilities: dict[str, float], n_scan_run_lengths: dict[str, int] = {}, raw_pathology_probabilities: dict[str, np.ndarray[typing.Any, np.dtype[np.float64]]] | None = None, distance_from_fovea_centre: float | None, fovea_centre: tuple[int, int, int] | None, fovea_landmarks: list[tuple[int, int, int]] | None, est_fovea_distance: float, distance_metric_type: str, subfoveal_indicator: str | None,):Output of the GA calculation algorithm with fovea detection.
Variables
- static
distance_from_fovea_centre : float | None
- static
distance_metric_type : str
- static
est_fovea_distance : float
- static
fovea_centre : tuple[int, int, int] | None
- static
fovea_landmarks : list[tuple[int, int, int]] | None
- static
subfoveal_indicator : str | None
Static methods
expected_cols
def expected_cols() ‑> list[str]:Inherited from:
Returns the expected columns for a dataframe.
field_specs
def field_specs() ‑> list[FieldSpec]:SCAN-grain FieldSpecs for GA + fovea scalar columns.
Methods
to_record
def to_record( self, additional_pathology_prob_cols: Collection[str] | None = None, additional_area_cols: Collection[str] | None = None, additional_run_length_cols: Collection[str] | None = None,) ‑> dict[str, typing.Any]:Inherited from:
Convert to a record format compatible with pd.DataFrame.from_records().
The scalar fields are emitted as-is; the untyped dict fields
(max_pathology_probabilities, raw_pathology_probabilities,
n_scan_run_lengths) are dropped and, when requested, re-projected
into flat per-label columns.
Arguments
additional_pathology_prob_cols: Pathology labels to flatten out ofmax_pathology_probabilitiesintomax_<label>_probabilitycolumns. A label absent from the map becomesnp.nan(→unknowndownstream), never a fabricated0.0.Noneemits no probability columns.additional_area_cols: Segmentation labels to flatten out ofsegmentation_areasinto<label>_areacolumns. A PRESENT key (incl.0.0) is a measured area; an ABSENT key becomesnp.nan(→unknown), so a criterion on a label outside the model's measured set does not silently pass a threshold.Noneemits no area columns.additional_run_length_cols: N-scan biomarker labels to flatten out ofn_scan_run_lengthsinton_scan_run_<label>columns. A label absent from the map becomesnp.nan(→unknowndownstream), never a fabricated0.Noneemits no run-length columns.
Returns
A {column: value} record: the scalar fields plus any requested
flattened probability/area/run-length columns.
GCCMetrics
class GCCMetrics( *, gcc_mean_superior: float | None = None, gcc_mean_inferior: float | None = None, gcc_asymmetry_index: float | None = None, gcc_radius_mm: float | None = None, min_thickness: float | None = None, glaucoma_stage: str | None = None, macula_coordinates: tuple[float, float, float] | None = None, rnfl_layer_present: bool = False, ipl_layer_present: bool = False, inner_layer_used: str | None = None, outer_layer_used: str | None = None, measurement_type: str | None = None,):Output of the GCC calculation algorithm.
Variables
- static
gcc_asymmetry_index : float | None
- static
gcc_mean_inferior : float | None
- static
gcc_mean_superior : float | None
- static
gcc_radius_mm : float | None
- static
glaucoma_stage : str | None
- static
inner_layer_used : str | None
- static
ipl_layer_present : bool
- static
macula_coordinates : tuple[float, float, float] | None
- static
measurement_type : str | None
- static
min_thickness : float | None
- static
outer_layer_used : str | None
- static
rnfl_layer_present : bool
Static methods
expected_cols
def expected_cols() ‑> list[str]:Returns the expected columns for a dataframe.
field_specs
def field_specs() ‑> list[FieldSpec]:SCAN-grain FieldSpecs for GCC scalar columns.
Methods
to_record
def to_record(self) ‑> dict[str, typing.Any]:Convert to a record format compatible with pd.DataFrame.from_records().
LesionGroupMetrics
class LesionGroupMetrics( *, total_area: float, num_lesions: int, largest_lesion_area: float, smallest_lesion_area: float, num_bscans_present: int, distance_from_image_centre: float, max_area_bscan_index: int | None, total_volume_nl: float | None = None, mean_thickness_um: float | None = None, largest_lesion_max_base_width_um: float | None = None, nearest_distance_from_fovea: float | None = None, aggregate_angular_span: float | None = None, lesions: tuple[LesionRecord, ...] = (), truncated_lesions: int = 0, roi: LesionGroupMetrics | None = None, lesions_measured: bool = False,):En-face area and lesion geometry for one named mask group.
The GAMetrics scalar shape, measured for a config-defined group of
segmentation labels rather than the one hardcoded GA mask.
Attributes
total_area: Projected area of the group mask, in mm².num_lesions: Connected components on the en-face plane.largest_lesion_area: Largest single component in mm²;nanwhen the group is absent from the scan on the compute path,Noneon an instance rebuilt from a cache blob (thejson_safewrite coercesnantoNone).smallest_lesion_area: Smallest single component in mm²;nanwhen the group is absent from the scan on the compute path,Noneon an instance rebuilt from a cache blob (thejson_safewrite coercesnantoNone).num_bscans_present: B-scans on which the group appears at all.distance_from_image_centre: Distance in mm from the image centre to the nearest component;nanwhen the group is absent from the scan on the compute path,Noneon an instance rebuilt from a cache blob (thejson_safewrite coercesnantoNone).max_area_bscan_index: B-scan carrying the most of the group, orNonewhen the group is absent.total_volume_nl: Sum of every lesion's volume in nL, orNonewhen axial spacing was absent.mean_thickness_um:total_volume_nl / total_areain µm, orNonewhen volume isNone.largest_lesion_max_base_width_um: The widest single-B-scan base width across every lesion in the group, in µm, orNonewhen the group has no lesions.nearest_distance_from_fovea: mm from the fovea landmark to the closest lesion cell in the group, orNonewhen no landmark was available. Never a fallback to image centre.aggregate_angular_span: Degrees of the fovea's circumference occupied by at least one lesion in the group, orNonewhen no landmark was available. Carries the same near-fovea over-read asLesionRecord.angular_span.lesions: Every measured lesion in the group, ordered largest cell count first. Empty when the group has no lesions.truncated_lesions: Count of lesions beyond the configured cap (LesionCalculationConfig.max_lesions_per_group) that were dropped fromlesions;0when nothing was truncated.roi: The same aggregates restricted to the configured region of interest, orNonewhen no ROI was configured. Its ownroiis alwaysNone.lesions_measured: Whether per-lesion measurement ran for this group (MaskGroupSpec.measure_lesions). Without it, every fovea-derived field beingNoneis ambiguous: the group might not have been measured, or it might have been measured and found no landmark, or measured and found no lesions. A criterion on<group>_aggregate_angular_spanevaluatesunknownfor the whole cohort in the first two cases, so which one it was has to be recoverable from the report.
Variables
- static
aggregate_angular_span : float | None
- static
distance_from_image_centre : float
- static
largest_lesion_area : float
- static
largest_lesion_max_base_width_um : float | None
- static
lesions : tuple[LesionRecord, ...]
- static
lesions_measured : bool
- static
max_area_bscan_index : int | None
- static
mean_thickness_um : float | None
- static
nearest_distance_from_fovea : float | None
- static
num_bscans_present : int
- static
num_lesions : int
- static
roi : LesionGroupMetrics | None
- static
smallest_lesion_area : float
- static
total_area : float
- static
total_volume_nl : float | None
- static
truncated_lesions : int
LesionMetrics
class LesionMetrics( *, groups: dict[str, LesionGroupMetrics], roi_radius_mm: float | None = None, fovea_reference: FoveaReference | None = None,):Output of the lesion_calculation step, one per scan.
Attributes
groups: Group name to its measurements. Group names come from the step's config, so nothing static enumerates them — hence the emptyexpected_cols/field_specsbelow.roi_radius_mm: Radius in mm of the configured region of interest around the fovea landmark, orNonewhen no ROI was configured.fovea_reference: Where the fovea landmark came from, orNonewhen no landmark was available. The landmark voxel and its provenance live together in aFoveaReferenceso a centre cannot exist without a reference type.
Variables
- static
fovea_reference : FoveaReference | None
- static
groups : dict[str, LesionGroupMetrics]
- static
roi_radius_mm : float | None
Static methods
expected_cols
def expected_cols() ‑> list[str]:No statically-known columns: group names come from the config.
convert_metrics_to_df fabricates an empty column for every name this
reports that the record lacks, and logs a missing-column warning while
doing so. Since the group set is per-run, listing anything here would
warn on every run.
Returns An empty list.
field_specs
def field_specs() ‑> list[FieldSpec]:No static catalog specs, for the same reason as expected_cols.
Area-group columns are SCAN-grain, which is exactly what a
column_criteria with no explicit grain defaults to, so an absent
catalog entry costs nothing but the unit annotation. criteria_matching
still warns for a group column that neither the catalog nor the frame
carries, which is the signal that matters (the step is unwired).
Returns An empty list.
Methods
to_record
def to_record(self) ‑> dict[str, typing.Any]:Flatten every group's scalar metrics into <group>_<metric> columns.
Per-lesion lists are deliberately omitted: a DataFrame cell holding a
list of records is not something a ColumnFilter can compare. They stay
in the cache blob, where a future MethodFilter could reach them, but
NO such filter exists today — every shipped criterion is answered by a
group scalar below, and largest_lesion_max_base_width_um in particular
exists so "at least one lesion wider than T" needs no per-lesion access
at all. Treat the persisted records as diagnostic detail, not as a
matchable surface. The groups container itself is not surfaced as a
column either, mirroring the GA converter dropping
segmentation_areas.
The nested ROI record is re-projected into its own scalar columns,
mirroring how GA re-projects n_scan_run_lengths into
n_scan_run_<label>: <group>_roi_<metric> answers a region-of-
interest question as a plain column comparison, without the ROI
threshold entering the step's config or the cache key. Absent ROI
yields None for every roi_<metric> column, never 0.0 — a
fabricated zero would read as "measured, and nothing qualifies".
reference_type is emitted per group rather than once per scan, even
though the fovea landmark is a scan-level fact. A criterion targets a
<group>_<metric> column, so when it evaluates unknown the reader
needs the provenance beside that group's own columns; an unqualified
reference_type also collides with any other step that reports one, and
is dropped by the report's group-prefix column selection. A group that
did not measure lesions reports NOT_MEASURED rather than the scan's
landmark state, because for that group the landmark is not why the
fovea-derived columns are empty.
Returns
A {column: value} record: one entry per group per scalar metric,
one roi_<metric> entry per group per scalar metric, plus one
<group>_reference_type per group.
LesionRecord
class LesionRecord( *, lesion_id: int, area: float, volume_nl: float | None, mean_thickness_um: float | None, max_base_width_um: float, max_base_width_bscan_index: int, max_feret_um: float, bscan_extent: int, centroid_bscan: float, centroid_col: float, distance_from_fovea: float | None, angular_span: float | None, within_roi: bool | None,):One measured lesion of one group.
Attributes
lesion_id: Component id within its group, 1-based.area: En-face area in mm².volume_nl: Volume in nL, orNonewhen the axial spacing was absent.mean_thickness_um:volume / areain µm — the physical sanity value.Nonewhen volume isNone.max_base_width_um: Widest run of set columns on any single B-scan, in µm. The well-sampled diameter, and the one a size criterion should gate on.max_base_width_bscan_index: The B-scan carrying that widest run.max_feret_um: Widest en-face extent in any direction, in µm. Reported for completeness; unreliable below about three B-scans of extent because the B-scan axis is sampled about twenty times coarser.bscan_extent: Number of B-scans the lesion appears on.centroid_bscan: Mean B-scan index of its cells.centroid_col: Mean column index of its cells.distance_from_fovea: mm from the fovea landmark to the lesion's nearest cell, orNonewhen no landmark was available. Never a fallback to image centre.angular_span: Degrees subtended about the fovea landmark, orNonewhen no landmark was available. Over-reads near the fovea landmark itself, where a single cell can subtend on the order of 180 degrees; not a precise measurement at short range.within_roi: Whether any part of the lesion lies inside the configured region of interest.Nonewhen no ROI was configured or no landmark was available.
Variables
- static
angular_span : float | None
- static
area : float
- static
bscan_extent : int
- static
centroid_bscan : float
- static
centroid_col : float
- static
distance_from_fovea : float | None
- static
lesion_id : int
- static
max_base_width_bscan_index : int
- static
max_base_width_um : float
- static
max_feret_um : float
- static
mean_thickness_um : float | None
- static
volume_nl : float | None
- static
within_roi : bool | None
ParsedBScanPredictions
class ParsedBScanPredictions( column_masks: NDArray[_Floating], class_probabilities: dict[str, NDArray[_Floating]], class_probabilities_by_bscan: dict[str, NDArray[_Floating]], class_areas: dict[str, list[float]], num_bscans: int, na_bscan_indices: tuple[int, ...] = (),):Container for the various outputs of parsing bscan predictions.
Attributes
column_masks: Per-B-scan columnar GA masks.class_probabilities: Probabilities appended once per reported detection — a mask instance, or aclassesentry. Its length is therefore a detection count, not a B-scan count, and its indices carry no B-scan alignment. Suitable only for order-independent reductions such asmax.class_probabilities_by_bscan: Probabilities indexed by B-scan, one entry per input B-scan,0.0where the class was not reported on that B-scan. Required for any positional reduction, such as the longest consecutive run. Validated againstnum_bscanson construction.class_areas: Per-class areas, per B-scan, for logging.num_bscans: The number of input B-scans, including those with no model output. Defines the index space ofclass_probabilities_by_bscan.na_bscan_indices: Indices, in the input B-scan index space, of the frames that carried no model output.column_masksskips these, so its first axis is COMPACTED: rowjofcolumn_masksis input B-scanj + count(i in na_bscan_indices where i <= j). Anything comparing an input-space B-scan coordinate againstcolumn_masksmust first shift it by the count of dropped frames below it — seecompact_bscan_index.
Raises
ValueError: If anyclass_probabilities_by_bscanarray does not hold exactlynum_bscansentries.
Variables
- static
class_areas : dict[str, list[float]]
- static
class_probabilities : dict[str, numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]
- static
class_probabilities_by_bscan : dict[str, numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]]
- static
column_masks : numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]]
- static
na_bscan_indices : tuple[int, ...]
- static
num_bscans : int