← All projects

★ Featured · Fall 2025 – Oct 2026 · Georgia Institute of Technology

FundusVision — Retinal Disease Screening

Team lead · CS 7641 Machine Learning

0.82ROC AUC · screening0.82Dice · vessels3,358Patients, split by patient

Roughly 40 million people are blind and half or more of those cases were preventable with early detection; diabetic retinopathy, glaucoma and macular degeneration all leave visible traces on the retina long before symptoms. A fundus camera is cheap and fast, but the specialist who reads the photograph is neither. FundusVision started as a CS 7641 team project asking how far standard deep learning gets on two parts of that reading task: deciding whether an eye needs a specialist at all, and tracing the retinal vessels whose shape carries much of the diagnostic signal.

The system is two PyTorch models behind one command-line package. A ResNet34 pretrained on ImageNet is fine-tuned on 6,392 fundus photographs from the ODIR-5K dataset to output a single Normal-vs-Abnormal screening score, with the decision threshold chosen on validation data for balanced sensitivity and specificity. A 31-million-parameter U-Net trained from scratch labels every pixel of a photograph as vessel or background. Both train in minutes on an Apple-silicon GPU and are exported to ONNX so the demo on this page runs entirely in your browser.

The most important finding was not a model but an evaluation bug. ODIR-5K lists both eyes of a patient on one row, and the team's first pipeline looped over both, duplicating every image roughly twice and then splitting the rows at random, so about half of the "test" images had also been trained on. That version reported 84% accuracy and 0.91 AUC. When I rebuilt the pipeline in October 2026 I deduplicated to one row per physical eye, labelled each eye from its own diagnostic keywords, and split by patient with a grouped stratified fold so no patient straddles train and test. The honest ROC AUC is 0.82, and the repository now asserts the absence of leakage before any training run.

Rebuilding the segmentation model exposed a second lesson in the opposite direction. Two textbook additions, random flips and cosine learning-rate decay, dropped test Dice from 0.82 to 0.65; a five-run ablation isolated the flips as the cause. With only 480 training images, a from-scratch U-Net converges far faster when it can rely on the fixed geometry of a fundus photograph, so the shipped recipe is the plain one and both options stay available as flags. The final model scores Dice 0.82 and IoU 0.69 on 200 held-out images, reproducing the course result from code anyone can run.

The engineering around the models is what makes the numbers trustworthy: a hexagonal `fundusvision` package with data, model, training and evaluation layers; a synthetic-data test suite that exercises the full train-evaluate-explain path on CPU in under ten seconds and runs in GitHub Actions; model cards that record every run's config, history and metrics; Grad-CAM for the classifier; and int8 ONNX exports that lose less than 0.005 AUC or Dice while shrinking the weights to 61 MB. The 1.6 GB of images that had been committed to git were purged from history, so a clone now takes a second.

What I would do next, in order of expected payoff: train the U-Net on 512-pixel patches, since at 224 pixels the capillaries are one to three pixels wide and dominate the error; move the classifier to a per-disease multi-label head at higher input resolution; and validate both models on a second dataset such as Messidor or EyePACS, because ODIR-5K comes from a handful of hospitals and one camera family.

Architecture

SCREENING · ODIR-5K · 6,392 EYESPer-eye labelsFROM KEYWORDSPatient-grouped splitNO LEAKAGE, ASSERTEDResNet34FINE-TUNED, 224 PXThreshold on valYOUDEN'S JVESSELS · 800 IMAGE / MASK PAIRSImages + masks2048 PX → 224 PXSeeded 80 / 20 split480 TRAIN · 120 VALU-NetFROM SCRATCH, 31M PARAMSBest val DiceCHECKPOINTHeld-out test split, scored onceROC AUC 0.82 · DICE 0.82Every number on this page comes from the test split;thresholds and checkpoints are chosen on validation only.

Results

Four held-out fundus images with ground-truth vessel masks, U-Net predictions and an error overlay
U-Net on held-out images — white true positive, red missed vessel, cyan false positive
ROC and precision-recall curves of the screening classifier on the test split
Screening classifier on the test split — ROC AUC 0.817, PR AUC 0.875
Grad-CAM heat-maps over eight test eyes, four abnormal and four normal
Grad-CAM over layer4 for correctly classified test eyes
Twelve test eyes the classifier got wrong with the highest confidence
Most confident test-set errors — several 'normal' labels show visible lesions, suggesting label noise

Live demo

Loading demo…

Key Contributions

  • Found and fixed train/test leakage in the team's original pipeline (duplicated images, row-level split); replaced it with per-eye labels and a patient-grouped, class-stratified split guarded by leakage assertions
  • Rebuilt the project as an installable PyTorch package with a CLI for download, training, evaluation and Grad-CAM; ran lint, synthetic-data smoke tests and the full test suite in GitHub Actions
  • Re-trained both models with honest numbers: ResNet34 screener at ROC AUC 0.82 / PR AUC 0.88, 71% sensitivity at 79% specificity on 913 held-out eyes from 480 patients; U-Net at Dice 0.82 / IoU 0.69 on 200 held-out images
  • Ran a five-configuration ablation showing flip augmentation costs 0.13 Dice on the small vessel set while cosine annealing is neutral, and documented it in the model card
  • Chose the screening operating point by Youden's J on validation rather than max-F1, which on 55%-positive data drifts to a threshold with 48% specificity
  • Exported int8 ONNX models (22 MB + 39 MB) with verified parity and built the in-browser demo below with ONNX Runtime Web; images never leave the device
  • Led the five-person course team: scoping, task allocation, the proposal and final report
MLResearchPyTorchComputer VisionONNX