Download app.py from Gokul-G1/Project2: direct link, hf CLI and curl.
- Browser
- Download file 21.9 kB
-
https://huggingface.co/spaces/Gokul-G1/Project2/resolve/main/app.py
- Command line
-
hf download hf://spaces/Gokul-G1/Project2/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Gokul-G1/Project2/resolve/main/app.py
21.9 kB
| import os | |
| import time | |
| import pickle | |
| import warnings | |
| from typing import Dict, Optional, Tuple | |
| from huggingface_hub import hf_hub_download | |
| import cv2 | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from PIL import Image as PILImage | |
| import timm | |
| import albumentations as A | |
| from albumentations.pytorch import ToTensorV2 | |
| from pytorch_grad_cam import GradCAMPlusPlus | |
| from pytorch_grad_cam.utils.image import show_cam_on_image | |
| import gradio as gr | |
| warnings.filterwarnings("ignore") | |
| # ============================================================================= | |
| # Constants & Config | |
| # ============================================================================= | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| IMG_SIZE = 512 | |
| NUM_CLASSES = 5 | |
| BACKBONE = "tf_efficientnetv2_m" | |
| FV_BACKBONE = "convnext_tiny.fb_in22k" | |
| GRADE_MAP = {0: "No DR", 1: "Mild DR", 2: "Moderate DR", 3: "Severe DR", 4: "Proliferative DR"} | |
| GRADE_COLORS = ["#2ecc71", "#f1c40f", "#e67e22", "#e74c3c", "#8e44ad"] | |
| SEVERITY_ICONS = ["π’", "π‘", "π ", "π΄", "π£"] | |
| CLINICAL_ACTION = [ | |
| "No DR detected. Routine annual screening recommended.", | |
| "Mild NPDR. Optimise glycaemic and blood-pressure control. Follow up in 12 months.", | |
| "Moderate NPDR. Ophthalmology referral within 3β6 months.", | |
| "Severe NPDR. Urgent ophthalmology referral. Consider anti-VEGF or laser assessment.", | |
| "Proliferative DR. URGENT referral β high blindness risk. Same-week appointment required.", | |
| ] | |
| IMAGENET_MEAN = [0.485, 0.456, 0.406] | |
| IMAGENET_STD = [0.229, 0.224, 0.225] | |
| STRONG_HEUR = {"red_dominance": 0.55, "disc_coverage": 0.25, "edge_density": 0.008} | |
| WEAK_HEUR = {"red_dominance": 0.30, "disc_coverage": 0.12, "edge_density": 0.006} | |
| # ============================================================================= | |
| # Model Definitions | |
| # ============================================================================= | |
| class GeM(nn.Module): | |
| def __init__(self, p: float = 3.0, eps: float = 1e-6): | |
| super().__init__() | |
| self.p = nn.Parameter(torch.tensor(p)) | |
| self.eps = eps | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return F.adaptive_avg_pool2d(x.clamp(min=self.eps).pow(self.p), 1).pow(1.0 / self.p) | |
| class CoralHead(nn.Module): | |
| def __init__(self, in_features: int, num_classes: int): | |
| super().__init__() | |
| self.linear = nn.Linear(in_features, 1, bias=False) | |
| self.bias = nn.Parameter(torch.zeros(num_classes - 1)) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.linear(x) + self.bias | |
| class DRHead(nn.Module): | |
| def __init__(self, in_features: int, num_classes: int = 5, dropout: float = 0.3): | |
| super().__init__() | |
| self.net = nn.Sequential( | |
| nn.Linear(in_features, 512), nn.BatchNorm1d(512), nn.SiLU(inplace=True), nn.Dropout(dropout), | |
| nn.Linear(512, 256), nn.BatchNorm1d(256), nn.SiLU(inplace=True), nn.Dropout(dropout / 2), | |
| nn.Linear(256, num_classes), | |
| ) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.net(x) | |
| class DRModel(nn.Module): | |
| def __init__(self, backbone: str = BACKBONE, num_classes: int = 5, pretrained: bool = False): | |
| super().__init__() | |
| self.backbone = timm.create_model(backbone, pretrained=pretrained, num_classes=0, global_pool="") | |
| in_feat = self.backbone.num_features | |
| self.pool = GeM(p=3.0) | |
| self.head_cls = DRHead(in_feat, num_classes) | |
| self.head_coral = CoralHead(in_feat, num_classes) | |
| def forward(self, x: torch.Tensor) -> Dict[str, torch.Tensor]: | |
| feat = self.pool(self.backbone(x)).flatten(1) | |
| return {"logits": self.head_cls(feat), "coral": self.head_coral(feat)} | |
| class FundusValidator(nn.Module): | |
| def __init__(self, backbone_name: str = FV_BACKBONE): | |
| super().__init__() | |
| self.backbone = timm.create_model(backbone_name, pretrained=False, num_classes=0, global_pool="avg") | |
| for p in self.backbone.parameters(): | |
| p.requires_grad = False | |
| with torch.no_grad(): | |
| feat_dim = self.backbone(torch.zeros(1, 3, 224, 224)).shape[1] | |
| self.head = nn.Sequential(nn.Linear(feat_dim, 256), nn.GELU(), nn.Dropout(0.3), nn.Linear(256, 2)) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| with torch.no_grad(): | |
| feats = self.backbone(x) | |
| return self.head(feats) | |
| # ============================================================================= | |
| # Preprocessing | |
| # ============================================================================= | |
| _clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) | |
| def apply_clahe_lab(rgb: np.ndarray) -> np.ndarray: | |
| lab = cv2.cvtColor(rgb, cv2.COLOR_RGB2LAB) | |
| lab[:, :, 0] = _clahe.apply(lab[:, :, 0]) | |
| return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB) | |
| def retinal_mask(rgb: np.ndarray) -> np.ndarray: | |
| gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY) | |
| _, m = cv2.threshold(gray, 15, 255, cv2.THRESH_BINARY) | |
| k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) | |
| return cv2.morphologyEx(cv2.morphologyEx(m, cv2.MORPH_CLOSE, k), cv2.MORPH_OPEN, k) | |
| def crop_retinal_disc(rgb: np.ndarray, pad: int = 10) -> np.ndarray: | |
| contours, _ = cv2.findContours(retinal_mask(rgb), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| if not contours: | |
| return rgb | |
| x, y, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea)) | |
| x, y = max(0, x - pad), max(0, y - pad) | |
| x2, y2 = min(rgb.shape[1], x + w + 2 * pad), min(rgb.shape[0], y + h + 2 * pad) | |
| return rgb[y:y2, x:x2] | |
| def fundus_heuristics(rgb: np.ndarray) -> Dict[str, float]: | |
| r, g, b = rgb[..., 0].astype(np.float32), rgb[..., 1].astype(np.float32), rgb[..., 2].astype(np.float32) | |
| bright = (r > 15) | (g > 15) | (b > 15) | |
| n_bright = int(bright.sum()) | |
| red_dom = float(((r > g) & (r > b))[bright].mean()) if n_bright > 100 else float(((r > g) & (r > b)).mean()) | |
| disc_cov = float(retinal_mask(rgb).mean() / 255.0) | |
| gx = cv2.Sobel(cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY), cv2.CV_32F, 1, 0, ksize=3) | |
| gy = cv2.Sobel(cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY), cv2.CV_32F, 0, 1, ksize=3) | |
| mag = np.sqrt(gx**2 + gy**2) | |
| edge_den = float(mag[bright].mean() / 255.0) if n_bright > 100 else float(mag.mean() / 255.0) | |
| return {"red_dominance": red_dom, "disc_coverage": disc_cov, "edge_density": edge_den} | |
| # ============================================================================= | |
| # Model Loading β downloads weights from HF repo at runtime | |
| # ============================================================================= | |
| SPACE_REPO = "Gokul-G1/Project2-Models" | |
| def _get_weight(filename: str) -> str: | |
| """Return local path if available, otherwise download from HF Space repo.""" | |
| if os.path.exists(filename): | |
| return filename | |
| print(f" Downloading {filename} from {SPACE_REPO}β¦") | |
| return hf_hub_download( | |
| repo_id=SPACE_REPO, | |
| filename=filename, | |
| repo_type="model", | |
| ) | |
| def load_dr_model(): | |
| m = DRModel(BACKBONE, NUM_CLASSES, pretrained=False).to(DEVICE) | |
| path = _get_weight("best_model.pt") | |
| ckpt = torch.load(path, map_location=DEVICE, weights_only=False) | |
| state = ckpt.get("model_state", ckpt) | |
| m.load_state_dict({k: v for k, v in state.items() if k in m.state_dict()}, strict=False) | |
| return m.eval() | |
| def load_fv_model(): | |
| m = FundusValidator(FV_BACKBONE).to(DEVICE) | |
| path = _get_weight("fundus_validator.pt") | |
| ckpt = torch.load(path, map_location=DEVICE, weights_only=False) | |
| head_state = ckpt.get("head_state_dict", ckpt) | |
| m.head.load_state_dict(head_state, strict=False) | |
| return m.eval() | |
| def load_calib(): | |
| path = _get_weight("calibration.pkl") if os.path.exists("calibration.pkl") or True else None | |
| try: | |
| path = _get_weight("calibration.pkl") | |
| with open(path, "rb") as f: | |
| return pickle.load(f) | |
| except Exception: | |
| return None | |
| print("Loading modelsβ¦") | |
| model = load_dr_model() | |
| fv_model = load_fv_model() | |
| calib = load_calib() | |
| print(f" DR model loaded | FV model loaded | calib={'yes' if calib else 'no'} | device={DEVICE}") | |
| # Transforms | |
| inf_tfm = A.Compose([A.Resize(IMG_SIZE, IMG_SIZE), A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD), ToTensorV2()]) | |
| fv_tfm = A.Compose([A.Resize(224, 224), A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD), ToTensorV2()]) | |
| # Grad-CAM setup | |
| class _SingleLogit(nn.Module): | |
| def __init__(self, inner): super().__init__(); self.inner = inner | |
| def forward(self, x): return self.inner(x)["logits"] | |
| _cam_model = _SingleLogit(model).to(DEVICE).eval() | |
| try: | |
| _target_layers = [model.backbone.blocks[-1]] | |
| cam_obj = GradCAMPlusPlus(model=_cam_model, target_layers=_target_layers) | |
| GRADCAM_OK = True | |
| except Exception as e: | |
| print(f"Grad-CAM init failed: {e}") | |
| GRADCAM_OK = False | |
| # ============================================================================= | |
| # Fundus Gating | |
| # ============================================================================= | |
| def gate_fundus(pil_img: PILImage.Image) -> Tuple[bool, float, Dict, str]: | |
| rgb_raw = np.array(pil_img.convert("RGB")) | |
| try: | |
| rgb = apply_clahe_lab(crop_retinal_disc(rgb_raw)) | |
| except Exception: | |
| rgb = rgb_raw | |
| h = fundus_heuristics(rgb) | |
| x = fv_tfm(image=rgb)["image"].unsqueeze(0).to(DEVICE) | |
| with torch.no_grad(): | |
| p = float(F.softmax(fv_model(x), dim=1).cpu().numpy()[0][1]) | |
| strong_ok = all(h[k] >= v for k, v in STRONG_HEUR.items()) | |
| weak_ok = all(h[k] >= v for k, v in WEAK_HEUR.items()) | |
| if strong_ok: return True, p, h, "Accepted (strong visual signatures)." | |
| if p >= 0.75: return True, p, h, f"Accepted (validator: {p*100:.1f}%)." | |
| if p >= 0.50 and weak_ok: return True, p, h, "Accepted (validator + heuristics)." | |
| if h["disc_coverage"] >= 0.25 and p >= 0.35: return True, p, h, "Accepted (clear retinal disc)." | |
| if p >= 0.90: return True, p, h, "Accepted (high validator confidence)." | |
| return False, p, h, "Not a fundus image. Please upload a colour retinal fundus photograph." | |
| # ============================================================================= | |
| # Grad-CAM Rendering & Lesion ROIs | |
| # ============================================================================= | |
| def render_cam(vis: np.ndarray, tensor: torch.Tensor) -> Optional[np.ndarray]: | |
| if not GRADCAM_OK: | |
| return None | |
| try: | |
| gc = cam_obj(input_tensor=tensor, targets=None)[0, :] | |
| gc = cv2.resize(gc, (vis.shape[1], vis.shape[0])) | |
| overlay = show_cam_on_image(vis.astype(np.float32) / 255.0, gc, use_rgb=True) | |
| return (overlay * 255).astype(np.uint8) | |
| except Exception: | |
| return None | |
| def render_rois(vis: np.ndarray, tensor: torch.Tensor) -> Optional[np.ndarray]: | |
| if not GRADCAM_OK: | |
| return None | |
| try: | |
| gc = cam_obj(input_tensor=tensor, targets=None)[0, :] | |
| gc = cv2.resize(gc, (vis.shape[1], vis.shape[0])) | |
| _, thresh = cv2.threshold((gc * 255).astype(np.uint8), 127, 255, cv2.THRESH_BINARY) | |
| contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| roi_img = vis.copy() | |
| cv2.drawContours(roi_img, contours, -1, (255, 0, 0), 2) # Highlight in red | |
| return roi_img | |
| except Exception: | |
| return None | |
| # ============================================================================= | |
| # Inference | |
| # ============================================================================= | |
| def predict(uploaded): | |
| t0 = time.time() | |
| if uploaded is None: | |
| return None, None, None, "<p style='color:var(--body-text-color);padding:20px'>Upload a fundus image to begin.</p>" | |
| pil_img = PILImage.fromarray(uploaded.astype(np.uint8)) | |
| accepted, p_fundus, heur, reason = gate_fundus(pil_img) | |
| if not accepted: | |
| rd = heur.get("red_dominance", 0) | |
| dc = heur.get("disc_coverage", 0) | |
| ed = heur.get("edge_density", 0) | |
| html = f""" | |
| <div class="error-box"> | |
| <h3 style="margin:0 0 8px">β οΈ Image Rejected</h3> | |
| <p style="margin:0">{reason}</p> | |
| <div style="margin-top:12px;font-size:12px;opacity:0.8"> | |
| <strong>Validator score:</strong> {p_fundus*100:.1f}% (need β₯75%)<br> | |
| <strong>Red dominance:</strong> {rd:.3f} (need β₯{WEAK_HEUR['red_dominance']:.2f}) | |
| {'β' if rd >= WEAK_HEUR['red_dominance'] else 'β'}<br> | |
| <strong>Disc coverage:</strong> {dc:.3f} (need β₯{WEAK_HEUR['disc_coverage']:.2f}) | |
| {'β' if dc >= WEAK_HEUR['disc_coverage'] else 'β'}<br> | |
| <strong>Edge density:</strong> {ed:.4f} (need β₯{WEAK_HEUR['edge_density']:.3f}) | |
| {'β' if ed >= WEAK_HEUR['edge_density'] else 'β'} | |
| </div> | |
| </div>""" | |
| return None, None, None, html | |
| # Pre-process - Enforce CLAHE | |
| rgb = np.array(pil_img.convert("RGB")) | |
| try: | |
| rgb = crop_retinal_disc(rgb) | |
| rgb = apply_clahe_lab(rgb) | |
| except Exception: | |
| pass | |
| vis = cv2.resize(rgb, (320, 320)) | |
| tensor = inf_tfm(image=cv2.resize(rgb, (IMG_SIZE, IMG_SIZE)))["image"].unsqueeze(0).to(DEVICE) | |
| # 4-view TTA | |
| with torch.no_grad(): | |
| views = [tensor, | |
| torch.flip(tensor, dims=[-1]), | |
| torch.flip(tensor, dims=[-2]), | |
| torch.rot90(tensor, 2, dims=(-2, -1))] | |
| logits_all, coral_all = [], [] | |
| for v in views: | |
| out = model(v) | |
| logits_all.append(out["logits"].cpu().numpy()) | |
| coral_all.append(torch.sigmoid(out["coral"]).cpu().numpy()) | |
| logits_mean = np.mean(logits_all, axis=0) # (1, 5) | |
| coral_mean = np.mean(coral_all, axis=0) # (1, 4) | |
| # Probabilities | |
| if calib is not None: | |
| try: | |
| probs = calib.apply(logits_mean)[0] | |
| except Exception: | |
| ex = np.exp(logits_mean - logits_mean.max()) | |
| probs = (ex / ex.sum())[0] | |
| else: | |
| ex = np.exp(logits_mean - logits_mean.max()) | |
| probs = (ex / ex.sum())[0] | |
| # Fusion: CORAL ordinal + softmax | |
| argmax_pred = int(probs.argmax()) | |
| coral_pred = int((coral_mean[0] > 0.5).sum()) | |
| coral_conf = float(coral_mean[0, coral_pred - 1]) if coral_pred > 0 else 1.0 | |
| pred = coral_pred if coral_conf > float(probs[argmax_pred]) else argmax_pred | |
| pred = max(0, min(4, pred)) | |
| conf = float(probs[pred]) | |
| # Grad-CAM and ROIs | |
| cam_img = render_cam(vis, tensor) | |
| roi_img = render_rois(vis, tensor) | |
| proc_time = time.time() - t0 | |
| # Build probability bars | |
| bar_html = "" | |
| for i, (lbl, col) in enumerate(zip(GRADE_MAP.values(), GRADE_COLORS)): | |
| w = float(probs[i]) * 100 | |
| weight = "700" if i == pred else "400" | |
| bar_html += f""" | |
| <div style="margin-bottom:6px"> | |
| <div style="display:flex;justify-content:space-between;font-size:13px;font-weight:{weight}"> | |
| <span>{SEVERITY_ICONS[i]} {lbl}</span><span>{w:.1f}%</span> | |
| </div> | |
| <div style="height:6px;background:var(--border-color-primary);border-radius:3px;overflow:hidden"> | |
| <div style="width:{w}%;background:{col};height:100%"></div> | |
| </div> | |
| </div>""" | |
| html = f""" | |
| <div class="report-card"> | |
| <!-- Grade header --> | |
| <div class="report-header"> | |
| <span style="font-size:52px">{SEVERITY_ICONS[pred]}</span> | |
| <div> | |
| <div style="font-size:22px;font-weight:700;color:{GRADE_COLORS[pred]}">{GRADE_MAP[pred]}</div> | |
| <div style="font-size:13px;opacity:0.7">Grade {pred} / 4 Β· ICDR Scale</div> | |
| </div> | |
| </div> | |
| <!-- Confidence bars --> | |
| <div class="report-bars"> | |
| <div style="font-size:13px;font-weight:600;margin-bottom:8px">Grade Probabilities</div> | |
| {bar_html} | |
| </div> | |
| <!-- Clinical action --> | |
| <div class="action-box" style="border-left-color: {GRADE_COLORS[pred]}"> | |
| <div style="font-size:12px;font-weight:600;margin-bottom:4px;opacity:0.8">CLINICAL RECOMMENDATION</div> | |
| <div style="font-size:14px">{CLINICAL_ACTION[pred]}</div> | |
| </div> | |
| <!-- Metrics footer --> | |
| <div style="display:flex;justify-content:space-between;font-size:11px;opacity:0.6; | |
| border-top:1px solid var(--border-color-primary);padding-top:10px"> | |
| <span>β± Processing: <strong>{proc_time:.2f}s</strong></span> | |
| <span>π Validator: <strong>{p_fundus*100:.1f}%</strong></span> | |
| <span>π― Confidence: <strong>{conf*100:.1f}%</strong></span> | |
| </div> | |
| </div>""" | |
| return vis, cam_img, roi_img, html | |
| def safe_predict(uploaded): | |
| """Wrapper to catch network/processing errors and prevent crashes.""" | |
| try: | |
| return predict(uploaded) | |
| except Exception as e: | |
| err_msg = f"<div class='error-box'><h3>β οΈ Processing Error</h3><p>{str(e)}</p><p>Please try uploading the image again.</p></div>" | |
| return None, None, None, err_msg | |
| # ============================================================================= | |
| # Gradio UI β Google Premium Material Design 3 (Dark & Light) | |
| # ============================================================================= | |
| premium_theme = gr.themes.Default( | |
| primary_hue="indigo", | |
| secondary_hue="blue", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], | |
| ).set( | |
| body_background_fill="var(--background-fill-primary)", | |
| body_background_fill_dark="var(--background-fill-primary)", | |
| block_background_fill="var(--block-background-fill)", | |
| block_border_width="1px", | |
| block_shadow="0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", | |
| button_primary_background_fill="*primary_600", | |
| button_primary_background_fill_hover="*primary_700", | |
| button_primary_text_color="white", | |
| ) | |
| css = """ | |
| .report-card { background: var(--block-background-fill); padding: 20px; border-radius: 14px; box-shadow: 0 4px 16px rgba(0,0,0,.08); font-family: 'Inter', sans-serif; line-height: 1.5; color: var(--body-text-color); } | |
| .report-header { display: flex; align-items: center; gap: 14px; margin-bottom: 16px; } | |
| .report-bars { background: var(--background-fill-secondary); border-radius: 8px; padding: 12px; margin-bottom: 14px; } | |
| .action-box { padding: 12px; background: var(--background-fill-secondary); border-left: 4px solid var(--primary-500); border-radius: 6px; margin-bottom: 14px; } | |
| .hero-banner { text-align: center; padding: 28px 20px 22px; background: linear-gradient(135deg, #1A73E8 0%, #0D47A1 100%); color: #fff; border-radius: 16px; margin-bottom: 20px; } | |
| .error-box { padding: 20px; border-radius: 10px; background: rgba(220, 38, 38, 0.1); color: #ef4444; border: 1px solid rgba(220, 38, 38, 0.2); font-family: 'Inter', sans-serif; } | |
| """ | |
| with gr.Blocks(theme=premium_theme, css=css, title="DR Grading AI β Clinical Decision Support") as demo: | |
| gr.HTML(""" | |
| <div class="hero-banner"> | |
| <div style="font-size:2.4em;font-weight:800;letter-spacing:-0.5px"> | |
| π Diabetic Retinopathy Grading | |
| </div> | |
| <div style="margin-top:8px;opacity:.88;font-size:1.05em"> | |
| EfficientNetV2-M Β· CLAHE Β· CORAL Ordinal Β· Grad-CAM++ Β· Lesion ROIs | |
| </div> | |
| <div style="margin-top:6px;opacity:.7;font-size:.85em"> | |
| For research & educational use only β not a medical device | |
| </div> | |
| </div> | |
| """) | |
| with gr.Row(equal_height=True): | |
| # ββ LEFT PANEL ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(scale=1, min_width=340): | |
| inp = gr.Image( | |
| label="Upload Retinal Fundus Photograph", | |
| type="numpy", | |
| height=380, | |
| sources=["upload", "clipboard"], | |
| ) | |
| with gr.Row(): | |
| btn_clear = gr.Button("π Clear", variant="secondary", size="sm") | |
| btn_analyze = gr.Button("π¬ Analyze", variant="primary", size="lg") | |
| gr.Markdown(""" | |
| **How to use** | |
| 1. Upload a colour fundus photograph (JPEG / PNG) | |
| 2. Click **Analyze** β the AI validates it first | |
| 3. Review the grade, heatmap, and recommendation | |
| > *Grades 0β4 follow the International Clinical DR (ICDR) severity scale.* | |
| """) | |
| # ββ RIGHT PANEL βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(scale=1, min_width=340): | |
| with gr.Row(): | |
| out_vis = gr.Image(label="Enhanced Fundus (CLAHE)", interactive=False, height=200) | |
| out_cam = gr.Image(label="Pathology Heatmap (Grad-CAM++)", interactive=False, height=200) | |
| out_roi = gr.Image(label="Detected Lesion ROIs", interactive=False, height=200) | |
| out_report = gr.HTML() | |
| # ββ Events ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| btn_analyze.click( | |
| fn=safe_predict, | |
| inputs=inp, | |
| outputs=[out_vis, out_cam, out_roi, out_report], | |
| ) | |
| btn_clear.click( | |
| fn=lambda: (None, None, None, None, ""), | |
| inputs=[], | |
| outputs=[inp, out_vis, out_cam, out_roi, out_report], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(default_concurrency_limit=5) | |
| demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True) | |