functions
CST/CRT-specific thickness calculation functions.
The shared retinal-layer primitives (parse_center_coordinates,
build_thickness_map, layer selection, etc.) live in
bitfount.steps.data_utils.thickness_metrics and are used by both the CST
and GCC steps. The shared per-file orchestration loop lives in
bitfount.steps.data_utils.thickness_calculation.
This module provides the CST/CRT-specific region statistics
(compute_average_thickness_around_center) and the high-level per-file
compute_cst_metrics_for_scan — ported from _CSTWorkerSide in
federated/algorithms/ophthalmology/cst_calculation_algorithm.py
(_compute_average_thickness_around_center lines 180-308 and
_calculate_metric lines 88-178).
It also hosts the EZ-integrity measurement (compute_ez_integrity), which
cst_calculation v2 adds. This module is shared by both versions, so the EZ
block is gated on an attenuation threshold being configured; v1 never passes
one and its path through compute_cst_metrics_for_scan is unchanged, down to
the log lines.
Module
Functions
compute_average_thickness_around_center
def compute_average_thickness_around_center( thickness_um: NDArray[Any], center_coords: tuple[float, float, float], slice_thickness_mm: float, pixel_spacing_x_mm: float, radius_mm: float = 0.5,) ‑> dict[str, float | None]:Compute CST and/or CRT based on radius setting.
- If radius_mm == 0: This measures the thickness of single center point. (In CST calculations, this measurement is specifically referred to as CRT)
- If radius_mm > 0: We measure the average thickness, using a circular region within the provided radius. If using thickness of ILM to RPE, this is referred to as CST.
Arguments
thickness_um: Thickness map in micrometers, shape (num_slices, width).center_coords: Center coordinates (slice, x, y). Only slice and x are used.slice_thickness_mm: Slice thickness in mm.pixel_spacing_x_mm: Pixel spacing in x direction in mm.radius_mm: radius of circular region in mm. If 0, uses single center point.
Returns Dictionary with mean_um, median_um, std_um, n_samples, and radius_mm.
compute_cst_metrics_for_scan
def compute_cst_metrics_for_scan( layer_pred: pd.Series, file_row: pd.Series, *, inner_layer: RetinalLayer, outer_layer: RetinalLayer, strict_measurement: bool, landmark_idx: int, radius_mm: float, center_landmark_type: str = 'fovea', metric_name: str = 'CST/CRT', ez_inner_layer: RetinalLayer | None = None, ez_outer_layer: RetinalLayer | None = None, ez_attenuation_threshold_um: float | None = None,) ‑> CSTMetrics:Calculate CST or CRT for a single file.
Ports _CSTWorkerSide._calculate_metric. Per-instance configuration
that the legacy worker read from self is passed as explicit parameters.
Arguments
layer_pred: Layer segmentation predictions for one file.file_row: Row containing DICOM metadata and fovea predictions.inner_layer: Desired inner layer for the thickness measurement.outer_layer: Desired outer layer for the thickness measurement.strict_measurement: If True, only calculate if both desired inner and outer layers are available (no fallback).landmark_idx: Index of the landmark to use (0=start, 1=end, 2=middle).radius_mm: Radius of the circular region in mm (0 for CRT single point).center_landmark_type: Nature of the center landmark ("fovea"/"macula").metric_name: Name of the metric being calculated (log text only).ez_inner_layer: Inner layer of the EZ-integrity pair.ez_outer_layer: Outer layer of the EZ-integrity pair.ez_attenuation_threshold_um: Separation above which the EZ counts as intact.Noneswitches the EZ measurement off entirely, which is whatcst_calculationv1 does — its path here is then unchanged.
Returns CSTMetrics object with calculated values.
compute_ez_integrity
def compute_ez_integrity( boundaries: Mapping[RetinalLayer, NDArray[Any]], *, ez_layer: RetinalLayer, rpe_layer: RetinalLayer, reference_bscan_index: int, pixel_spacing_row_mm: float, pixel_spacing_column_mm: float, threshold_um: float,) ‑> EZIntegrity | None:Measure EZ integrity along one B-scan.
Every A-scan on the reference B-scan is classified into exactly one of three states, and the two spans are the longest contiguous run of the first two:
- intact: both layers measured here and separated by more than
threshold_um. - attenuated: RPE measured here, but the separation does not exceed
the threshold — including where EZ is absent entirely. That inclusion
is the point.
attenuated = ~intactwould count unmeasurable columns as attenuated, andseparation <= thresholdalone would report zero attenuation exactly where attenuation is total, because a missing EZ makes the comparison false rather than true. - unmeasurable: no RPE here, so nothing can be said. Columns padded onto a narrow B-scan land here too, which is why they do not inflate the attenuated span.
Arguments
boundaries: Boundary grids frombuild_layer_boundaries, which must include both layers.ez_layer: The inner layer of the pair.rpe_layer: The outer layer of the pair.reference_bscan_index: Row of the grid to measure.pixel_spacing_row_mm: Axial spacing, for the separation.pixel_spacing_column_mm: Lateral spacing, for the spans.threshold_um: Separation above which the EZ counts as intact.
Returns
The measurement, or None if the reference B-scan is outside the grid.
Classes
EZIntegrity
class EZIntegrity( intact_max_span_um: float, attenuated_max_span_um: float, measured_width_um: float, layers_measured: bool,):The EZ-integrity measurement on one reference B-scan.
Attributes
intact_max_span_um: Widest contiguous run of A-scans whose EZ-to-RPE separation exceeds the threshold. The criterion gates on this.attenuated_max_span_um: Widest contiguous run that was measurable but did not exceed it.measured_width_um: How much of the B-scan was measurable at all. Zero makes both spans absence of evidence rather than a finding.layers_measured: Whether both layers appear anywhere in the volume.
Variables
- static
attenuated_max_span_um : float
- static
intact_max_span_um : float
- static
layers_measured : bool
- static
measured_width_um : float