EyeScreen / app.py
wwwwweee's picture
Update app.py
4aa2eee verified
Raw History Blame Contribute Delete
3.95 kB
"""
Eye Screen backend — deploy on Hugging Face Spaces (Gradio SDK).
Pipeline: image quality gate -> pretrained DR classifier -> Grad-CAM -> calibrated confidence.
This is the "real model" half of the app. Google AI Studio frontend calls this via its API.
"""
import spaces
import cv2
import numpy as np
import torch
import torch.nn.functional as F
import timm
import gradio as gr
from PIL import Image
from pytorch_grad_cam import GradCAM
from pytorch_grad_cam.utils.image import show_cam_on_image
LABELS = ["No DR", "Mild", "Moderate", "Severe", "Proliferative DR"]
TEMPERATURE = 1.8 # softmax temperature for calibrated confidence; tune on a val split if time allows
# --- Model ---
# Pretrained on APTOS/EyePACS/IDRID/DDR fundus data (timm hub format).
# If this exact repo id doesn't load cleanly, swap for another fundus-pretrained
# timm/HF checkpoint (e.g. search "diabetic retinopathy" on huggingface.co/models)
# and keep everything else (quality gate, Grad-CAM, calibration) unchanged.
MODEL_ID = "hf_hub:ClementP/FundusDRGrading-efficientnet_b0"
model = timm.create_model(MODEL_ID, pretrained=True)
model.eval()
# last conv block for Grad-CAM (EfficientNet: conv_head is the final conv layer)
target_layers = [model.conv_head] if hasattr(model, "conv_head") else [list(model.children())[-3]]
cam = GradCAM(model=model, target_layers=target_layers)
IMG_SIZE = 224
def check_quality(img_bgr):
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
blur_score = cv2.Laplacian(gray, cv2.CV_64F).var()
brightness = gray.mean()
issues = []
if blur_score < 100:
issues.append("Image appears blurry/out of focus")
if brightness < 40:
issues.append("Image is underexposed (too dark)")
if brightness > 210:
issues.append("Image is overexposed (too bright)")
return issues, {"blur_score": round(float(blur_score), 1), "brightness": round(float(brightness), 1)}
def preprocess(img_pil):
img = img_pil.resize((IMG_SIZE, IMG_SIZE))
arr = np.array(img).astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
arr = (arr - mean) / std
tensor = torch.tensor(arr.transpose(2, 0, 1), dtype=torch.float32).unsqueeze(0)
return tensor, arr
@spaces.GPU(duration=30)
def predict(image):
if image is None:
return "No image uploaded", "", None, {}
img_pil = Image.fromarray(image).convert("RGB")
img_bgr = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
issues, metrics = check_quality(img_bgr)
if issues:
msg = "RETAKE REQUESTED: " + "; ".join(issues)
return msg, "N/A — quality check failed before classification", None, metrics
tensor, norm_img = preprocess(img_pil)
with torch.no_grad():
logits = model(tensor)
calibrated_probs = F.softmax(logits / TEMPERATURE, dim=1)[0]
pred_idx = int(torch.argmax(calibrated_probs))
confidence = float(calibrated_probs[pred_idx]) * 100
severity = LABELS[pred_idx]
# Grad-CAM heatmap
grayscale_cam = cam(input_tensor=tensor, targets=None)[0]
display_img = np.array(img_pil.resize((IMG_SIZE, IMG_SIZE))).astype(np.float32) / 255.0
overlay = show_cam_on_image(display_img, grayscale_cam, use_rgb=True)
result_text = f"{severity} ({confidence:.1f}% confidence)"
disclaimer = "Screening aid only — not a diagnosis. Consult an ophthalmologist for confirmation."
return result_text, disclaimer, overlay, metrics
demo = gr.Interface(
fn=predict,
inputs=gr.Image(label="Fundus photo"),
outputs=[
gr.Textbox(label="Result"),
gr.Textbox(label="Note"),
gr.Image(label="Grad-CAM heatmap"),
gr.JSON(label="Quality metrics (debug)"),
],
title="Eye Screen — DR Grading API",
description="Upload a retinal fundus image. Rejects low-quality photos before grading.",
)
if __name__ == "__main__":
demo.launch()