Download src/dr_grader.py from bs01338/DR_Clinical_Report_Generator: direct link, hf CLI and curl.
- Browser
- Download file 5.67 kB
-
https://huggingface.co/spaces/bs01338/DR_Clinical_Report_Generator/resolve/main/src/dr_grader.py
- Command line
-
hf download hf://spaces/bs01338/DR_Clinical_Report_Generator/src/dr_grader.py
-
curl -L -o dr_grader.py https://huggingface.co/spaces/bs01338/DR_Clinical_Report_Generator/resolve/main/src/dr_grader.py
5.67 kB
| """DR grader: loads the trained EfficientNet-B4 + Swin (CORN) model and grades | |
| a fundus image into one of five diabetic-retinopathy severity classes. | |
| The model architecture and weights are reused from the original DR Grading | |
| project; the weights are downloaded from the public HuggingFace Space on first | |
| use. Inference uses the training-matched preprocessing (crop + CLAHE). | |
| """ | |
| from __future__ import annotations | |
| import torch | |
| import torch.nn as nn | |
| import timm | |
| from torchvision import transforms | |
| from huggingface_hub import hf_hub_download | |
| from src.config import ( | |
| DR_WEIGHTS_REPO, DR_WEIGHTS_REPO_TYPE, DR_WEIGHTS_FILE, | |
| EFFICIENTNET_NAME, SWIN_NAME, NUM_CLASSES, FUSION_HIDDEN_DIM, FUSION_DROPOUT, | |
| IMAGE_SIZE, NORM_MEAN, NORM_STD, CLASS_NAMES, | |
| ) | |
| from src.preprocessing import preprocess_fundus | |
| class ParallelEfficientNetSwinCORN(nn.Module): | |
| """EfficientNet-B4 + Swin Transformer parallel backbones with a CORN ordinal head. | |
| Must match the trained architecture exactly (layer names included) so the | |
| saved weights load with strict=True. | |
| """ | |
| def __init__(self, efficientnet_name, swin_name, num_classes, | |
| fusion_hidden_dim=1024, fusion_dropout=0.3, pretrained=False): | |
| super().__init__() | |
| self.efficientnet = timm.create_model(efficientnet_name, pretrained=pretrained, num_classes=0) | |
| self.swin = timm.create_model(swin_name, pretrained=pretrained, num_classes=0) | |
| eff_dim = getattr(self.efficientnet, "num_features", None) | |
| swin_dim = getattr(self.swin, "num_features", None) | |
| if eff_dim is None or swin_dim is None: | |
| raise ValueError("Could not determine backbone feature dimensions.") | |
| fusion_dim = eff_dim + swin_dim | |
| self.fusion = nn.Sequential( | |
| nn.LayerNorm(fusion_dim), | |
| nn.Linear(fusion_dim, fusion_hidden_dim), | |
| nn.GELU(), | |
| nn.Dropout(fusion_dropout), | |
| nn.Linear(fusion_hidden_dim, num_classes - 1), # CORN: K-1 outputs | |
| ) | |
| def _to_vector(feat): | |
| if feat.ndim == 2: | |
| return feat | |
| if feat.ndim == 3: | |
| return feat.mean(dim=1) | |
| if feat.ndim == 4: | |
| if feat.shape[1] > feat.shape[-1] and feat.shape[1] > feat.shape[-2]: | |
| return feat.mean(dim=(2, 3)) | |
| return feat.mean(dim=(1, 2)) | |
| raise ValueError(f"Unexpected feature shape: {feat.shape}") | |
| def forward(self, x): | |
| eff = self._to_vector(self.efficientnet(x)) | |
| swin = self._to_vector(self.swin(x)) | |
| return self.fusion(torch.cat([eff, swin], dim=1)) | |
| def _clean_state_dict(state_dict): | |
| """Strip any 'module.' prefixes left by DataParallel during training.""" | |
| return {(k[len("module."):] if k.startswith("module.") else k): v | |
| for k, v in state_dict.items()} | |
| _model = None | |
| _device = None | |
| _transform = transforms.Compose([ | |
| transforms.ToTensor(), | |
| transforms.Normalize(NORM_MEAN, NORM_STD), | |
| ]) | |
| def load_grader(): | |
| """Load the model + weights once (cached for repeated calls).""" | |
| global _model, _device | |
| if _model is not None: | |
| return _model, _device | |
| _device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print(f"Downloading DR weights from {DR_WEIGHTS_REPO} (first run only) ...") | |
| weights_path = hf_hub_download( | |
| repo_id=DR_WEIGHTS_REPO, filename=DR_WEIGHTS_FILE, repo_type=DR_WEIGHTS_REPO_TYPE, | |
| ) | |
| model = ParallelEfficientNetSwinCORN( | |
| efficientnet_name=EFFICIENTNET_NAME, swin_name=SWIN_NAME, | |
| num_classes=NUM_CLASSES, fusion_hidden_dim=FUSION_HIDDEN_DIM, | |
| fusion_dropout=FUSION_DROPOUT, pretrained=False, | |
| ) | |
| state_dict = _clean_state_dict(torch.load(weights_path, map_location=_device)) | |
| model.load_state_dict(state_dict, strict=True) | |
| model.to(_device).eval() | |
| _model = model | |
| return _model, _device | |
| def grade_fundus(pil_image): | |
| """Grade one fundus image. | |
| Returns the grade (0-4), its label, and the cumulative ordinal | |
| probabilities P(y > k). CORN decoding: the K-1 logits are conditional | |
| probabilities, so the cumulative P(y > k) is the cumulative product of | |
| their sigmoids, and the predicted grade is the count of cumulative | |
| probabilities above 0.5 (equivalent to coral_pytorch.corn_label_from_logits). | |
| """ | |
| model, device = load_grader() | |
| processed = preprocess_fundus(pil_image) # training-matched crop + CLAHE | |
| x = _transform(processed).unsqueeze(0).to(device) | |
| with torch.inference_mode(): | |
| logits = model(x) | |
| # CORN decoding: the K-1 outputs are CONDITIONAL probabilities | |
| # P(y>k | y>k-1), so the cumulative P(y>k) is the cumulative PRODUCT of | |
| # the sigmoids. The grade is the number of cumulative probs above 0.5. | |
| # This matches coral_pytorch.corn_label_from_logits exactly. | |
| probs = torch.cumprod(torch.sigmoid(logits), dim=1) # P(grade > 0..3) | |
| grade = int((probs > 0.5).sum(dim=1).item()) | |
| return { | |
| "grade": grade, | |
| "label": CLASS_NAMES[grade], | |
| "ordinal_probabilities": [round(p, 4) for p in probs.squeeze(0).cpu().tolist()], | |
| "processed_image": processed, | |
| } | |
| if __name__ == "__main__": | |
| # Usage: python -m src.dr_grader <path-to-fundus-image> | |
| import sys | |
| from PIL import Image | |
| if len(sys.argv) < 2: | |
| print("Usage: python -m src.dr_grader <path-to-fundus-image>") | |
| raise SystemExit(1) | |
| result = grade_fundus(Image.open(sys.argv[1])) | |
| print(f"\nGrade {result['grade']} - {result['label']}") | |
| print(f"Ordinal probabilities P(>0..>3): {result['ordinal_probabilities']}") | |