Downloads Β· 30 days
0
fundusnap/fundusnap-v1-severitycls-rn34-22m
fundusnap-v1-severitycls-rn34-22m is a image classification model from fundusnap. Use it when you need a label for an image. It is set up for fastai. The card lists the license as cc-by-nc-4.0.
<div align="center" <table border="1" <tr <td align="center" style="padding: 20px;" <h3π’ Domain & Email Migration Notice</h3 <pFrom <bMay 30th, 2026</b, Fundusnap will transition to new domains as <codefundusnap.com<β¦
Downloads Β· 30 days
0
Access
Public
Updated Aug 15, 2026
Repo size
477 MB
Likes
1
Public
Click a slice to open those files.
.pth262 MB Β· 60%
From the Hugging Face model README
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.
This model is released for research and engineering use, and at most as a triage assist inside a workflow that a qualified clinician supervises.
Anyone deploying it in a screening context is responsible for their own validation and for keeping a human grader in the loop.
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.
| 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. |
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.
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))
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 |
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.
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
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.
@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}
}