Instructions to use fundusnap/fundusnap-v1-severitycls-rn34-22m with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- fastai
How to use fundusnap/fundusnap-v1-severitycls-rn34-22m with fastai:
from huggingface_hub import from_pretrained_fastai learn = from_pretrained_fastai("fundusnap/fundusnap-v1-severitycls-rn34-22m") - Notebooks
- Google Colab
- Kaggle
π’ Domain & Email Migration NoticeFrom May 30th, 2026, Fundusnap will transition to new domains as π Website: fundusnap.faizath.com (formerly fundusnap.com) |
AI-assisted diabetic retinopathy screening β from a phone camera to a graded fundus image.
π€ Hugging Face β’ π GitHub
fundusnap-v1-severitycls-rn34-22m
Diabetic retinopathy severity classifier for colour fundus (retinal) photographs. Given a single
fundus image it predicts one of the five standard ordinal DR grades (0 = No DR through
4 = Proliferative DR) and returns a probability for each. The model is a ResNet34 backbone
(ImageNet-pretrained) with a fastai classification head β roughly 22M parameters, hence the name:
severitycls (severity classification) + rn34 (ResNet34) + 22m (parameter count).
It is shipped both as a fastai/PyTorch checkpoint for further training and as an ONNX graph with a dynamic batch axis for deployment.
β οΈ Intended use β not for clinical use
This model is released for research and engineering use, and at most as a triage assist inside a workflow that a qualified clinician supervises.
- It is not a medical device and has no regulatory clearance (FDA, CE/MDR, or otherwise).
- It must never be the sole basis for a diagnosis, referral, or treatment decision.
- It has not been validated prospectively, on any specific camera or population, or against a reference grading standard beyond the public dataset described below.
- Its weakest area is exactly the clinically hardest one: separating early grades 0/1/2 (see Limitations).
Anyone deploying it in a screening context is responsible for their own validation and for keeping a human grader in the loop.
Label scheme
Outputs are the five-level International Clinical DR severity scale, as coded in the training
labels (level column):
| Index | Grade | Clinical meaning |
|---|---|---|
| 0 | No DR | No visible retinopathy |
| 1 | Mild | Microaneurysms only |
| 2 | Moderate | More than microaneurysms, less than severe NPDR |
| 3 | Severe | Severe non-proliferative DR |
| 4 | Proliferative | Proliferative DR (neovascularisation) |
The grades are ordinal, but the model is trained as a plain 5-way classifier β it does not exploit that ordering, and it is not calibrated for ordinal-regression style thresholding.
Files
| Path | What it is |
|---|---|
models/model.onnx |
ONNX graph (opset 14). Weights are stored externally. |
models/model.onnx.data |
External weight blob for model.onnx. Must sit next to it β loading the graph alone fails. |
models/retinopathy201519-rn34-1.pth |
fastai learn.save() checkpoint (a dict with model and opt keys) β use this to fine-tune. |
FastAI_Training.ipynb |
Training run: data prep, augmentation, fine-tuning, evaluation. |
Model_Converter.ipynb |
Checkpoint β ONNX export, and the ONNX β TensorFlow/TFLite path. |
schema_generated.py |
Generated TFLite FlatBuffers schema used by the onnx2tf toolchain. Build-time helper only β not part of the model. |
Usage
ONNX Runtime (recommended for inference)
pip install onnxruntime pillow numpy
import numpy as np
import onnxruntime as ort
from PIL import Image
LABELS = ["No DR", "Mild", "Moderate", "Severe", "Proliferative"]
# ImageNet statistics β fastai's vision_learner normalises with these for pretrained models.
MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
def preprocess(path, size=224):
"""Mirror fastai's Resize(224) default (ResizeMethod.Crop): resize the shorter
side to 224, then take the centre crop."""
img = Image.open(path).convert("RGB")
w, h = img.size
scale = size / min(w, h)
img = img.resize((round(w * scale), round(h * scale)), Image.BILINEAR)
w, h = img.size
left, top = (w - size) // 2, (h - size) // 2
img = img.crop((left, top, left + size, top + size))
x = np.asarray(img, dtype=np.float32) / 255.0 # HWC, [0, 1]
x = (x - MEAN) / STD
return x.transpose(2, 0, 1)[None] # NCHW, shape (1, 3, 224, 224)
# model.onnx.data must be in the same directory as model.onnx.
session = ort.InferenceSession("models/model.onnx", providers=["CPUExecutionProvider"])
logits = session.run(["output"], {"input": preprocess("fundus.jpg")})[0] # (1, 5)
# The graph emits raw logits β apply softmax yourself.
e = np.exp(logits - logits.max(axis=1, keepdims=True))
probs = (e / e.sum(axis=1, keepdims=True))[0]
grade = int(probs.argmax())
print(f"grade {grade} ({LABELS[grade]}) p={probs[grade]:.4f}")
print({LABELS[i]: round(float(p), 4) for i, p in enumerate(probs)})
The input axis 0 is dynamic, so you can batch: pass (N, 3, 224, 224) and get (N, 5) back.
fastai / PyTorch checkpoint
The checkpoint stores a state dict only, so rebuild the identical learner before loading it:
import torch
import torch.nn.functional as F
from fastai.vision.all import *
import pandas as pd
# A throwaway DataLoaders just to give the learner the right shape (5 classes).
dummy = pd.DataFrame({"image": ["sample"] * 5, "level": [0, 1, 2, 3, 4]})
dls = ImageDataLoaders.from_df(
dummy, path=".", fn_col="image", label_col="level", suff=".jpg",
valid_pct=0.0, bs=1, item_tfms=[], batch_tfms=[], shuffle=False,
)
learn = vision_learner(dls, resnet34, path=".", loss_func=FocalLoss(),
metrics=[accuracy], n_out=5)
ckpt = torch.load("models/retinopathy201519-rn34-1.pth",
weights_only=False, map_location="cpu")
learn.model.load_state_dict(ckpt["model"])
learn.dls.vocab = [0, 1, 2, 3, 4]
_, pred_idx, probs = learn.predict("fundus.jpg")
probs = F.softmax(probs, dim=0) # learn.predict returns logits for this head
print(int(pred_idx), probs)
On some fastcore versions the fastai import chain raises
AttributeError: 'L' object has no attribute 'starmap'. Patch it before building the learner:import itertools from fastcore.foundation import L if not hasattr(L, "starmap"): L.starmap = lambda self, f: L(itertools.starmap(f, self))
Training
Data β resized-2015-2019-diabetic-retinopathy-detection,
the resized_traintest15_train19 image set with traintestLabels15_trainLabels19.csv labels. This
combines the EyePACS 2015 and APTOS 2019 DR detection releases.
Class balancing β the raw label distribution is heavily skewed toward grade 0, so each grade was
resampled to 10,000 rows with replacement (df.groupby('level').sample(10000, replace=True)),
giving a 50,000-image balanced training frame. 10% was held out for validation.
Augmentation β albumentations wrapped in a custom
AlbTransform(Transform):
ShiftScaleRotate(rotate_limit=20, border_mode=0)HorizontalFlipRandomBrightnessContrastHueSaturationValue(hue_shift_limit=5, sat_shift_limit=5, val_shift_limit=5)
Setup
| Backbone | resnet34, ImageNet-pretrained |
| Head | fastai default (AdaptiveConcatPool2d β BN/dropout β linear), n_out=5 |
| Loss | FocalLoss() |
| Input | Resize(224) (centre crop), ImageNet normalisation |
| Batch size | 32 |
| Schedule | learn.fine_tune(4) β 1 frozen epoch, then 4 unfrozen |
| LR | from learn.lr_find(), valley suggestion |
| Seed | 3865 |
Run log (unfrozen phase, β6:45 per epoch):
| Epoch | train_loss | valid_loss | accuracy |
|---|---|---|---|
| 0 | 0.5260 | 0.4779 | 0.5772 |
| 1 | 0.3620 | 0.3204 | 0.6894 |
| 2 | 0.2090 | 0.2327 | 0.7812 |
| 3 | 0.1030 | 0.2229 | 0.8154 |
Evaluation
Measured on the held-out 10% validation split (n = 5,000) of the balanced training frame.
| Grade | Precision | Recall | F1 | Support |
|---|---|---|---|---|
| 0 β No DR | 0.66 | 0.65 | 0.65 | 1,013 |
| 1 β Mild | 0.72 | 0.71 | 0.72 | 1,032 |
| 2 β Moderate | 0.75 | 0.76 | 0.76 | 954 |
| 3 β Severe | 0.97 | 0.98 | 0.97 | 1,003 |
| 4 β Proliferative | 0.97 | 0.98 | 0.98 | 998 |
| Aggregate | Precision | Recall | F1 |
|---|---|---|---|
| Accuracy | 0.82 | ||
| Macro avg | 0.81 | 0.82 | 0.82 |
| Weighted avg | 0.81 | 0.82 | 0.81 |
Macro-F1 to four decimals: 0.8153. FastAI_Training.ipynb also plots the confusion matrix,
which shows the errors concentrated in the 0β1β2 block.
These figures are self-reported β computed by the author in FastAI_Training.ipynb, not
verified by Hugging Face β and are mirrored in the model-index metadata at the top of this card,
which is why the Hub labels them as such.
Limitations and bias
- Early grades are the weak point. Grades 3 and 4 are separated almost perfectly (F1 0.97β0.98), but 0/1/2 sit at 0.65β0.76. That is the reverse of what a screening deployment wants most, since distinguishing "no DR" from "mild/moderate" is what drives the referral decision.
- Validation is optimistic. The validation split was drawn after oversampling with replacement, so duplicated images can appear on both sides of the split, and the class balance (β1,000 per grade) does not resemble real screening prevalence, where grade 0 dominates. Expect meaningfully lower real-world performance than the headline 0.82 β treat these numbers as a relative sanity check, not a deployment estimate.
- Resolution. 224Γ224 with a centre crop discards the fine detail that microaneurysms and small haemorrhages live in, and can crop away peripheral lesions entirely.
- Domain shift. Training images come from the public 2015/2019 releases, dominated by specific camera models, capture protocols, and patient populations. Different hardware, field of view, illumination, or demographics will degrade accuracy, and the model has not been audited for performance differences across subgroups.
- Image quality. There is no reject/ungradable option β a blurred, over-exposed, or non-fundus image still yields a confident-looking 5-way distribution.
- Uncalibrated. Softmax outputs are not calibrated probabilities; do not read them as risk.
Reproducing and converting
FastAI_Training.ipynb reproduces the training run end to end. Model_Converter.ipynb reloads the
checkpoint and exports it.
The one non-obvious step in the export: PyTorch's ONNX exporter cannot handle
AdaptiveMaxPool2d, which fastai's AdaptiveConcatPool2d head layer contains. It is swapped for an
ONNX-friendly equivalent before torch.onnx.export:
class ONNXFriendlyConcatPool(nn.Module):
def forward(self, x):
max_pool = x.amax(dim=[-1, -2], keepdim=True)
avg_pool = x.mean(dim=[-1, -2], keepdim=True)
return torch.cat([max_pool, avg_pool], dim=1)
learn.model[1][0] = ONNXFriendlyConcatPool()
torch.onnx.export(
learn.model, torch.randn(1, 3, 224, 224), "model.onnx",
export_params=True, opset_version=14, do_constant_folding=True,
input_names=["input"], output_names=["output"],
dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}},
)
For a TensorFlow / TFLite build (for on-device inference), continue with:
onnx2tf --input_onnx_file_path model.onnx --output_folder_path model_tf
License
Released under CC BY-NC 4.0 β attribution required, non-commercial use only. The underlying training data carries its own Kaggle / EyePACS / APTOS terms; check those before redistributing anything derived from it.
Citation
@software{fundusnap_severitycls_rn34_22m,
title = {fundusnap-v1-severitycls-rn34-22m: ResNet34 diabetic retinopathy severity classifier},
author = {Fundusnap},
url = {https://huggingface.co/fundusnap/fundusnap-v1-severitycls-rn34-22m},
license = {CC-BY-NC-4.0}
}
Model tree for fundusnap/fundusnap-v1-severitycls-rn34-22m
Base model
timm/resnet34.tv_in1kEvaluation results
- Accuracy on Resized 2015 & 2019 Diabetic Retinopathy Detection (EyePACS + APTOS, Kaggle)validation set self-reported0.815
- F1 (macro) on Resized 2015 & 2019 Diabetic Retinopathy Detection (EyePACS + APTOS, Kaggle)validation set self-reported0.815
- Precision (macro) on Resized 2015 & 2019 Diabetic Retinopathy Detection (EyePACS + APTOS, Kaggle)validation set self-reported0.810
- Recall (macro) on Resized 2015 & 2019 Diabetic Retinopathy Detection (EyePACS + APTOS, Kaggle)validation set self-reported0.820