| """Minimal inference example for DoB24/fundus-9model-benchmark. |
| |
| Loads one of the 9 trained checkpoints, applies the same preprocessing used |
| during evaluation, and predicts a class for a single fundus image. |
| |
| Usage: |
| python inference_example.py path/to/image.jpg --model densenet121 |
| """ |
| from __future__ import annotations |
| import argparse, json |
| from pathlib import Path |
| import torch |
| import torch.nn.functional as F |
| from torchvision import transforms |
| from PIL import Image |
| import timm |
| from huggingface_hub import hf_hub_download |
|
|
| REPO = "DoB24/fundus-9model-benchmark" |
| CLASSES = [ |
| "Central Serous Chorioretinopathy", |
| "Diabetic Retinopathy", "Disc Edema", "Glaucoma", "Healthy", |
| "Macular Scar", "Myopia", "Pterygium", |
| "Retinal Detachment", "Retinitis Pigmentosa", |
| ] |
| |
| TIMM_ID = { |
| "vgg19": "vgg19", "resnet50": "resnet50", "resnet101": "resnet101", |
| "densenet121": "densenet121", "inception_v3": "inception_v3", |
| "swin_b": "swin_base_patch4_window7_224", |
| } |
|
|
| def load_model(name: str, num_classes: int = 10): |
| weights = hf_hub_download(REPO, f"weights/{name}_best.pth") |
| if name in TIMM_ID: |
| model = timm.create_model(TIMM_ID[name], pretrained=False, num_classes=num_classes) |
| if name == "inception_v3": |
| model = timm.create_model("inception_v3", pretrained=False, |
| num_classes=num_classes, aux_logits=False) |
| elif name == "clip_openai": |
| import open_clip |
| model, _, _ = open_clip.create_model_and_transforms("ViT-B-16", pretrained=None) |
| model.visual.proj = None |
| model = torch.nn.Sequential(model.visual, torch.nn.Linear(768, num_classes)) |
| elif name == "dinov2_l": |
| model = torch.hub.load("facebookresearch/dinov2", "dinov2_vitl14") |
| model = torch.nn.Sequential(model, torch.nn.Linear(1024, num_classes)) |
| elif name == "retfound": |
| model = timm.create_model("vit_large_patch16_224", pretrained=False, |
| num_classes=num_classes) |
| else: |
| raise ValueError(f"Unknown model: {name}") |
| sd = torch.load(weights, map_location="cpu", weights_only=False) |
| if isinstance(sd, dict) and "state_dict" in sd: |
| sd = sd["state_dict"] |
| model.load_state_dict(sd, strict=False) |
| model.eval() |
| return model |
|
|
| def preprocess(img_path: str) -> torch.Tensor: |
| tf = transforms.Compose([ |
| transforms.Resize((224, 224)), |
| transforms.ToTensor(), |
| transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), |
| ]) |
| return tf(Image.open(img_path).convert("RGB")).unsqueeze(0) |
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("image") |
| ap.add_argument("--model", default="densenet121", |
| choices=list(TIMM_ID) + ["clip_openai", "dinov2_l", "retfound"]) |
| args = ap.parse_args() |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| model = load_model(args.model).to(device) |
| x = preprocess(args.image).to(device) |
| with torch.no_grad(): |
| probs = F.softmax(model(x), dim=1)[0].cpu().numpy() |
| order = probs.argsort()[::-1] |
| print(f"\nTop-3 predictions for {args.image} using {args.model}:") |
| for i in order[:3]: |
| print(f" {CLASSES[i]:40s} {probs[i]*100:5.2f} %") |
|
|
| if __name__ == "__main__": |
| main() |
|
|