The Real Problem: Clinical Capture Variability
When training vision models on curated open-source datasets (such as EyePACS, Messidor, or APTOS), images have already undergone manual review. In real-world clinics and remote screening camps, however, images are captured by varied personnel using different fundus cameras, pupil dilation levels, and patient movement conditions.
Common real-world artifacts include:
- Motion Blur: Involuntary patient eye saccades during shutter release.
- Severe Corneal Flare / Overexposure: Light bounce from improperly aligned optical lenses.
- Insufficient Field of View: Partial capture where the macula or optic disc is clipped outside the camera circle.
The Fast-Reject Pipeline Architecture
Running an expensive neural network to assess quality on every image is slow. Instead, we architect a two-tier gate:
Raw Upload Image
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Tier 1: Sub-Millisecond Mathematical Heuristics (OpenCV) │
│ • Laplacian Variance (Blur Detection) │
│ • Luminance Histogram Skewness (Over/Underexposure) │
│ • Circular Mask Aspect Ratio (Retinal Field Completeness) │
└──────────────────────────────┬──────────────────────────────┘
│ Passes Tier 1
▼
┌─────────────────────────────────────────────────────────────┐
│ Tier 2: Lightweight MobileNet Quality Classifier │
│ • Gradable vs. Ungradable binary triage │
└──────────────────────────────┬──────────────────────────────┘
│ Passes Tier 2
▼
[ Proceed to Primary Diagnostic Model ]
OpenCV Quality Verification Code
import cv2
import numpy as np
def evaluate_image_quality(image_path: str) -> dict:
'''Evaluates blur, brightness, and contrast using vectorized OpenCV operations.'''
img = cv2.imread(image_path)
if img is None:
return {"gradable": False, "reason": "File corruption or unreadable format"}
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 1. Blur detection via Laplacian Variance
laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
is_blurred = laplacian_var < 85.0 # Calibrated empirical threshold
# 2. Illumination and Exposure Audit
mean_brightness = np.mean(gray)
is_underexposed = mean_brightness < 25.0
is_overexposed = mean_brightness > 220.0
# 3. Contrast check via standard deviation
contrast_std = np.std(gray)
is_low_contrast = contrast_std < 28.0
is_gradable = not (is_blurred or is_underexposed or is_overexposed or is_low_contrast)
reasons = []
if is_blurred: reasons.append(f"Severe motion blur (Laplacian: {laplacian_var:.1f})")
if is_underexposed: reasons.append(f"Severe underexposure (Mean: {mean_brightness:.1f})")
if is_overexposed: reasons.append(f"Overexposure / optical flare (Mean: {mean_brightness:.1f})")
if is_low_contrast: reasons.append(f"Insufficient contrast (StdDev: {contrast_std:.1f})")
return {
"gradable": is_gradable,
"metrics": {
"laplacian_variance": round(laplacian_var, 2),
"mean_brightness": round(mean_brightness, 2),
"contrast_std": round(contrast_std, 2)
},
"reasons": reasons
}