adarshcod30's picture
Deploy DR screening Gradio demo (app.py fetches weights from the model repo)
b379113 verified
Raw History Blame Contribute Delete
3.46 kB
"""CNN embedding extraction from the canonical grading checkpoint, for
Phase 5's fusion head.
Uses `baseline_effb0_512_fold0` (QWK 0.8930), not the 1024px checkpoint --
Phase 3's own conclusion was "the baseline stands" (docs/07_PHASE3_RESULTS.md),
and Result 14 retracted the 1024px checkpoint's causal story (its edge over
the baseline is now suspected to be a backend artifact, not resolution).
Building the fusion head on a result the project already walked back would
undercut that finding.
Reuses the checkpoint-loading convention scripts/evaluate.py and
scripts/calibrate.py already established (strip the Lightning `model.`
prefix, load into a bare `FundusGrader`) rather than inventing a new one.
**Images come from the Ben-Graham-preprocessed cache
(`data/processed/aptos_512/`), not the raw APTOS files** -- unlike
`drdetect.fusion.features`, which deliberately uses raw images because the
Phase 4 lesion models were trained on IDRiD's un-enhanced originals. The
grading model was trained on the preprocessed cache, so its embedding is
only meaningful computed on the same distribution it actually learned from.
Two different "the right image for this model" answers for two different
models feeding the same fusion vector -- not an inconsistency.
"""
from __future__ import annotations
from pathlib import Path
import torch
from drdetect.data.dataset import build_transforms
from drdetect.grading.model import FundusGrader, build_model
__all__ = ["load_grading_backbone", "extract_cnn_embedding", "EMBEDDING_DIM"]
EMBEDDING_DIM = 1280 # efficientnet_b0's pre-classifier feature width
def load_grading_backbone(
checkpoint: str, *, backbone: str = "efficientnet_b0", device: torch.device
) -> FundusGrader:
model = build_model(backbone, num_outputs=5, pretrained=False, freeze_bn=True)
ckpt = torch.load(checkpoint, map_location="cpu", weights_only=False)
state = ckpt.get("state_dict", ckpt)
state = {k.removeprefix("model."): v for k, v in state.items() if k.startswith("model.")}
model.load_state_dict(state)
return model.to(device).eval()
def extract_cnn_embedding(
image_path: str | Path, model: FundusGrader, device: torch.device, *, size: int = 512
) -> torch.Tensor:
"""The 1280-d pre-classifier pooled feature (`forward_head(...,
pre_logits=True)`), not the 5-way logits `model(x)` would give."""
embedding, _logits = extract_cnn_embedding_and_logits(image_path, model, device, size=size)
return embedding
def extract_cnn_embedding_and_logits(
image_path: str | Path, model: FundusGrader, device: torch.device, *, size: int = 512
) -> tuple[torch.Tensor, torch.Tensor]:
"""Both the fusion head's input (the embedding) and the baseline
grading-only prediction (the 5-way logits) from one forward pass --
used when comparing fusion against grading-alone on the same images, so
the baseline's own prediction doesn't need a second pass over the model."""
import cv2
transform = build_transforms(size, train=False)
image = cv2.cvtColor(cv2.imread(str(image_path), cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB)
tensor = transform(image=image)["image"].unsqueeze(0).to(device)
with torch.no_grad():
features = model.backbone.forward_features(tensor)
embedding = model.backbone.forward_head(features, pre_logits=True)
logits = model.backbone.forward_head(features)
return embedding.squeeze(0).cpu(), logits.squeeze(0).cpu()