adarshcod30's picture
Deploy DR screening Gradio demo (app.py fetches weights from the model repo)
b379113 verified
Raw History Blame Contribute Delete
2.95 kB
"""Quadrant mapping from the OD-fovea axis.
Needed for ICDR's 4-2-1 rule (severe NPDR: haemorrhages in 4 quadrants, or
venous beading in 2, or IRMA in 1), which counts lesions per retinal
quadrant. This module defines the four quadrants geometrically, as the
roadmap specifies: one dividing line through the optic disc along the
OD-fovea axis, one perpendicular to it through the same point.
**Labelling deliberately avoids true anatomical nasal/temporal names.**
Whether the "away from fovea" side of the OD is nasal or temporal depends on
which eye (OD/OS) the image is of, and neither IDRiD nor this project's
other datasets carry reliable per-image eye-laterality labels to resolve
that. The labels used here -- "foveal-side" / "disc-side" for the axis-
parallel direction, "superior" / "inferior" for the perpendicular one --
describe the geometry precisely without asserting an anatomical mapping
this project cannot verify. A deployment with real laterality metadata
could remap these to true nasal/temporal in one place (`QUADRANT_LABELS`)
without touching the geometry.
"""
from __future__ import annotations
import numpy as np
__all__ = ["quadrant_axes", "assign_quadrant", "QUADRANT_LABELS"]
QUADRANT_LABELS = {
(1, 1): "superior-foveal-side",
(1, -1): "inferior-foveal-side",
(-1, 1): "superior-disc-side",
(-1, -1): "inferior-disc-side",
}
def quadrant_axes(
od_xy: tuple[float, float], fovea_xy: tuple[float, float]
) -> tuple[np.ndarray, np.ndarray]:
"""Unit vectors (along_axis, perpendicular_axis) through the OD, where
along_axis points from OD toward the fovea. Raises if OD and fovea
coincide (undefined axis) -- that should never happen for real data and
signals a genuine upstream bug (e.g. a broken localisation checkpoint)
if it does.
"""
od = np.asarray(od_xy, dtype=float)
fovea = np.asarray(fovea_xy, dtype=float)
axis = fovea - od
norm = np.linalg.norm(axis)
if norm < 1e-6:
raise ValueError(f"OD and fovea coincide ({od_xy} == {fovea_xy}) -- cannot define an axis.")
along = axis / norm
# Perpendicular, image coordinates (y grows downward): rotate -90 degrees
# so positive `perp` points toward smaller y (up the image, superior).
perp = np.array([along[1], -along[0]])
return along, perp
def assign_quadrant(
point_xy: tuple[float, float], od_xy: tuple[float, float], fovea_xy: tuple[float, float]
) -> str:
"""Which of the 4 quadrants `point_xy` falls in, relative to the OD as
origin and the OD-fovea line as one dividing axis. See module docstring
for why the labels describe geometry, not asserted anatomy."""
along, perp = quadrant_axes(od_xy, fovea_xy)
od = np.asarray(od_xy, dtype=float)
v = np.asarray(point_xy, dtype=float) - od
along_sign = 1 if np.dot(v, along) >= 0 else -1
perp_sign = 1 if np.dot(v, perp) >= 0 else -1
return QUADRANT_LABELS[(along_sign, perp_sign)]