#!/usr/bin/env python3 """ DrRetina — Gradio UI (Light Theme, Clean Medical Design) """ import gradio as gr from backend import ( predict, generate_report, qwen_qa, template_qa, validate_image, check_image_quality, generate_referral_letter_from_agent, batch_process_zip, GRADES, EMOJI, COLORS, URGENCY, ) import datetime import os import pandas as pd # ───────────────────────────────────────────────────────────────── # CSS — Clean Light Medical Theme # ───────────────────────────────────────────────────────────────── CSS = """ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap'); *, *::before, *::after { box-sizing: border-box; } body, .gradio-container, .gradio-container * { font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif !important; } body { background: linear-gradient(135deg, #f0f4f8 0%, #e2e8f0 100%) !important; background-attachment: fixed !important; color: #1a202c !important; } .gradio-container { background: transparent !important; max-width: 1250px !important; margin: 0 auto !important; padding: 0 1.5rem 3rem !important; } footer, .built-with { display: none !important; } /* ── Tab Navigation ── */ .tab-nav { background: rgba(255, 255, 255, 0.6) !important; backdrop-filter: blur(12px) !important; border: 1px solid rgba(255, 255, 255, 0.8) !important; border-radius: 16px !important; padding: 6px !important; gap: 6px !important; margin-bottom: 2rem !important; box-shadow: 0 4px 16px rgba(0,0,0,0.04) !important; } .tab-nav button { background: transparent !important; color: #718096 !important; border: none !important; border-radius: 12px !important; font-weight: 600 !important; font-size: 0.92rem !important; padding: 0.6rem 1.6rem !important; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important; } .tab-nav button:hover { background: rgba(255, 255, 255, 0.8) !important; color: #2d3748 !important; transform: translateY(-1px) !important; } .tab-nav button.selected { background: linear-gradient(135deg, #2b6cb0 0%, #3182ce 100%) !important; color: #ffffff !important; box-shadow: 0 4px 12px rgba(43,108,176,0.35) !important; transform: translateY(-1px) !important; } /* ── Cards & Panels (Glassmorphism) ── */ .gr-panel, .gradio-group, .gr-box, fieldset { background: rgba(255, 255, 255, 0.7) !important; backdrop-filter: blur(16px) !important; border: 1px solid rgba(255, 255, 255, 0.9) !important; border-radius: 20px !important; box-shadow: 0 8px 32px rgba(31, 38, 135, 0.05) !important; transition: transform 0.2s ease !important; } /* ── Labels ── */ label span, .gr-form label { color: #2d3748 !important; font-weight: 700 !important; font-size: 0.85rem !important; letter-spacing: 0.02em !important;} /* ── Inputs ── */ textarea, input[type="text"], input[type="file"] { background: rgba(255, 255, 255, 0.8) !important; border: 1.5px solid #e2e8f0 !important; color: #1a202c !important; border-radius: 12px !important; font-size: 0.95rem !important; transition: all 0.2s ease !important; } textarea:focus, input[type="text"]:focus { border-color: #3182ce !important; box-shadow: 0 0 0 3px rgba(49,130,206,0.15) !important; background: #ffffff !important; outline: none !important; } /* ── Primary Button ── */ .gr-button-primary, button.primary { background: linear-gradient(135deg, #2b6cb0 0%, #2c5282 100%) !important; border: none !important; color: #fff !important; font-weight: 800 !important; font-size: 1rem !important; border-radius: 14px !important; padding: 0.9rem 2rem !important; box-shadow: 0 4px 14px rgba(43,108,176,0.35), inset 0 1px 0 rgba(255,255,255,0.2) !important; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important; cursor: pointer !important; width: 100% !important; letter-spacing: 0.02em !important; } .gr-button-primary:hover, button.primary:hover { transform: translateY(-2px) !important; box-shadow: 0 8px 24px rgba(43,108,176,0.45), inset 0 1px 0 rgba(255,255,255,0.2) !important; background: linear-gradient(135deg, #3182ce 0%, #2a4365 100%) !important; } .gr-button-secondary { background: linear-gradient(135deg, #edf2f7 0%, #e2e8f0 100%) !important; color: #2d3748 !important; border: 1px solid #cbd5e0 !important; font-weight: 700 !important; border-radius: 14px !important; transition: all 0.2s ease !important; } .gr-button-secondary:hover { transform: translateY(-1px) !important; box-shadow: 0 4px 12px rgba(0,0,0,0.05) !important; background: #ffffff !important; } /* ── Image ── */ .gr-image-preview, .image-container { background: rgba(255, 255, 255, 0.5) !important; border: 2px dashed #cbd5e0 !important; border-radius: 16px !important; transition: all 0.2s ease !important; } .gr-image-preview:hover, .image-container:hover { border-color: #3182ce !important; background: rgba(255, 255, 255, 0.8) !important; } /* ── Markdown ── */ .prose, .gr-markdown, .md { color: #2d3748 !important; line-height: 1.8 !important; font-size: 0.96rem !important; } .prose h1, .prose h2, .prose h3, .gr-markdown h1, .gr-markdown h2, .gr-markdown h3 { color: #1a202c !important; font-weight: 800 !important; margin-top: 1.5rem !important; letter-spacing: -0.02em !important; } .prose strong, .gr-markdown strong { color: #1a202c !important; font-weight: 700 !important;} .prose blockquote, .gr-markdown blockquote { border-left: 4px solid #3182ce !important; background: linear-gradient(90deg, rgba(235,248,255,0.8) 0%, rgba(235,248,255,0.2) 100%) !important; padding: 1rem 1.2rem !important; border-radius: 0 12px 12px 0 !important; color: #2c5282 !important; font-size: 0.92rem !important; font-style: italic !important; } /* ── Chatbot ── */ .chatbot-container { max-width: 900px !important; margin: 0 auto !important; } .chatbot { background: rgba(255, 255, 255, 0.4) !important; backdrop-filter: blur(10px) !important; border: 1px solid rgba(255,255,255,0.8) !important; border-radius: 24px !important; padding: 1rem !important; } /* Removed custom bubble CSS for Gradio 5 native support */ .chat-input-container { background: #ffffff !important; border: 1.5px solid #e2e8f0 !important; border-radius: 18px !important; padding: 6px !important; margin-top: 1rem !important; box-shadow: 0 10px 30px rgba(0,0,0,0.05) !important; transition: all 0.3s ease !important; } .chat-input-container:focus-within { border-color: #3182ce !important; box-shadow: 0 10px 30px rgba(49,130,206,0.1) !important; } /* ── Scrollbar ── */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #cbd5e0; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #3182ce; } """ # ───────────────────────────────────────────────────────────────── # HTML COMPONENTS # ───────────────────────────────────────────────────────────────── HEADER_HTML = """
Clinical Intelligence · Diagnostic Support System
👁️

DrRetina

Clinical AI-Powered Diabetic Retinopathy Detection Agent

🔥 Finetuned on AMD MI300X 🧠 ViT-MAE & Qwen3-8B ✅ Kappa 0.9097
""" EMPTY_STATE_HTML = """
🔍

Upload a retinal fundus image and click
Analyse Image to get your DR grade & clinical report.

""" LOADING_HTML = """

Analysing image & generating AI report...

""" LOADING_REPORT_HTML = "*⏳ AI report generating... please wait.*" GRADE_BG = {0: "#f0fff4", 1: "#fffff0", 2: "#fff7ed", 3: "#fff5f5", 4: "#fff5f5"} GRADE_BORDER = {0: "#9ae6b4", 1: "#f6e05e", 2: "#fbd38d", 3: "#fc8181", 4: "#fc8181"} GRADE_TEXT = {0: "#22543d", 1: "#744210", 2: "#7b341e", 3: "#742a2a", 4: "#63171b"} def make_grade_badge(grade, probs): color = COLORS[grade] bg = GRADE_BG[grade] border = GRADE_BORDER[grade] text = GRADE_TEXT[grade] name = GRADES[grade][0] conf = probs[grade] * 100 # F5: Confidence Tier if conf >= 88: conf_tier = "HIGH CONFIDENCE" conf_color = "#22c55e" # Green elif conf >= 72: conf_tier = "BORDERLINE" conf_color = "#eab308" # Yellow else: conf_tier = "LOW CONFIDENCE" conf_color = "#ef4444" # Red prob_pills = "".join( f"""
{EMOJI[i]}{GRADES[i][0]}: {p*100:.1f}%
""" for i, p in enumerate(probs) ) return f"""
DR Grade Detected
{EMOJI[grade]} Grade {grade} — {name}
{URGENCY[grade]}
Confidence
{conf:.1f}%
{conf_tier}
All Class Probabilities
{prob_pills}
""" # ───────────────────────────────────────────────────────────────── # STEP 1: Fast inference (grade + heatmap, no LLM) # ───────────────────────────────────────────────────────────────── def fast_analyse(pil_img): """Returns grade + images immediately. No LLM call.""" if pil_img is None: return None, None, EMPTY_STATE_HTML, LOADING_REPORT_HTML, None, None # F5: Image Quality Pre-check q_ok, q_msg = check_image_quality(pil_img) # FR-01: Validate image ok, msg = validate_image(pil_img) if not ok: err = f"""
⚠️ Invalid Image

{msg}

""" return None, None, err, "", None, None try: grade, probs, pil224, cam_pil = predict(pil_img) max_prob = float(probs[grade]) badge = make_grade_badge(grade, probs) return pil224, cam_pil, badge, LOADING_REPORT_HTML, grade, probs.tolist() except Exception as e: import traceback; traceback.print_exc() err = f"""

❌ Error: {e}

""" return None, None, err, "", None, None # ───────────────────────────────────────────────────────────────── # STEP 2: Slow LLM report # ───────────────────────────────────────────────────────────────── def get_report(grade, probs_list, language): """Called after fast_analyse via .then() — generates LLM report.""" import gradio as gr if grade is None or probs_list is None: return "", "", gr.DownloadButton(visible=False) import numpy as np import tempfile import os import markdown from weasyprint import HTML probs = np.array(probs_list) report = generate_report(grade, probs, language) html_body = markdown.markdown(report, extensions=['tables']) is_rtl = language in ["Urdu", "Arabic"] dir_attr = 'dir="rtl"' if is_rtl else 'dir="ltr"' text_align = 'right' if is_rtl else 'left' html_content = f"""

DrRetina Clinical Report

{html_body} """ tmp_path = os.path.join(tempfile.gettempdir(), "DrRetina_Clinical_Report.pdf") try: HTML(string=html_content).write_pdf(tmp_path) except Exception as e: # Fallback to TXT if pdfkit fails locally without wkhtmltopdf tmp_path = os.path.join(tempfile.gettempdir(), "DrRetina_Clinical_Report.txt") with open(tmp_path, "w", encoding="utf-8") as f: f.write("DrRetina Clinical Report\n==========================\n\n" + report) return gr.Markdown(value=report, rtl=is_rtl), report, gr.DownloadButton(value=tmp_path, visible=True) def create_referral(grade, probs_list): if grade is None or probs_list is None: return "⚠️ Analyze an image first." conf = probs_list[grade] * 100 letter = generate_referral_letter_from_agent(grade, conf) return letter # ───────────────────────────────────────────────────────────────── # CHAT FUNCTION # ───────────────────────────────────────────────────────────────── def user_input(message, history, g_state): if history is None: history = [] if not message.strip(): return "", history, history if g_state is None: history.append({"role": "user", "content": message}) history.append({ "role": "assistant", "content": "⚠️ Please upload and analyse a retinal image first, then I can answer your questions." }) return "", history, history history.append({"role": "user", "content": message}) return "", history, history def bot_response(history, g_state, r_state, probs_state): if g_state is None or not history or history[-1]["role"] == "assistant": return history, history message = history[-1]["content"] # Calculate real confidence from the probs array real_conf = (probs_state[g_state] * 100) if probs_state is not None else 90.0 ans = qwen_qa(message, g_state, r_state, history=history[:-1], confidence=real_conf) or template_qa(message, g_state) history.append({"role": "assistant", "content": ans}) return history, history # ───────────────────────────────────────────────────────────────── # BUILD UI # ───────────────────────────────────────────────────────────────── def build_ui(): with gr.Blocks( css=CSS, title="DrRetina — AI Diabetic Retinopathy Detection", theme=gr.themes.Base( primary_hue="blue", neutral_hue="slate", font=gr.themes.GoogleFont("Plus Jakarta Sans"), ), ) as demo: g_state = gr.State(None) r_state = gr.State("") probs_state = gr.State(None) gr.HTML(HEADER_HTML) with gr.Tabs(): # ━━━ Tab 1: Diagnosis ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ with gr.TabItem("🔬 Diagnosis"): with gr.Row(equal_height=False): # Left: Upload + controls with gr.Column(scale=1, min_width=320): gr.HTML("""
Analysis Setup
""") with gr.Row(): lang_in = gr.Dropdown(["English", "Urdu", "Hindi", "Arabic", "Spanish", "French"], label="Report Language", value="English", scale=1) img_in = gr.Image( type="pil", label="Retinal Fundus Image", height=280, show_label=True, show_download_button=False, sources=["upload", "clipboard"], ) btn = gr.Button("🔍 Analyse Image", variant="primary", size="lg") gr.HTML("""
📋 How to use Upload a clear retinal fundus photo (JPG/PNG). The AI detects the DR grade, highlights affected areas with GradCAM, and generates a personalised clinical report.
""") # Right: Outputs with gr.Column(scale=1, min_width=300): gr.HTML("""
Analysis Output
""") with gr.Row(): img_pre = gr.Image(label="Enhanced View", height=145, show_download_button=False) img_cam = gr.Image(label="Clinical Attention Map", height=145, show_download_button=False) # GradCAM color legend gr.HTML("""
GradCAM Legend:
Low → High Attention
""") # Grade result grade_html = gr.HTML(EMPTY_STATE_HTML) # Divider gr.HTML("""
📋 AI Clinical Report
""") report_md = gr.Markdown( value="*Analyse an image to generate a personalised AI clinical report.*", ) download_btn = gr.DownloadButton("📥 Download Clinical Report", visible=False) btn.click( fn=fast_analyse, inputs=[img_in], outputs=[img_pre, img_cam, grade_html, report_md, g_state, probs_state], api_name=False, ).then( fn=get_report, inputs=[g_state, probs_state, lang_in], outputs=[report_md, r_state, download_btn], api_name=False, ) # ━━━ Tab 2: Clinical Q&A ━━━━━━━━━━━━━━━━━━━━━━━━━━ with gr.TabItem("💬 Clinical Q&A"): with gr.Column(elem_classes="chatbot-container"): gr.HTML("""

DrRetina AI Assistant

Ask questions about your screening results, treatment guidelines, or general eye health. Powered by Qwen3-8B.

""") chatbot = gr.Chatbot( height=350, type="messages", show_label=False, elem_classes="chatbot", placeholder="
👋 Hello! I'm your clinical assistant.
Upload an image in the analysis tab to start a detailed discussion.
", ) with gr.Row(elem_classes="chat-input-container"): msg = gr.Textbox( placeholder="Type your question here...", scale=9, show_label=False, container=False, ) send = gr.Button("↑", variant="primary", scale=1, min_width=50) gr.HTML("""
Try asking:
""") chat_hist = gr.State(None) send.click( user_input, inputs=[msg, chat_hist, g_state], outputs=[msg, chatbot, chat_hist], api_name=False, ).then( bot_response, inputs=[chat_hist, g_state, r_state, probs_state], outputs=[chatbot, chat_hist], api_name=False, ) msg.submit( user_input, inputs=[msg, chat_hist, g_state], outputs=[msg, chatbot, chat_hist], api_name=False, ).then( bot_response, inputs=[chat_hist, g_state, r_state, probs_state], outputs=[chatbot, chat_hist], api_name=False, ) # ━━━ Tab 3: Batch Processing (F3) ━━━━━━━━━━━━━━━━━ with gr.TabItem("ℹ️ How it Works"): gr.Markdown(""" ## 👁️ DrRetina: Advanced Clinical AI DrRetina is a next-generation diagnostic assistant for Diabetic Retinopathy (DR) screening. It combines high-performance vision transformers with generative medical intelligence to provide clinicians with clear, actionable insights. --- ### 🛡️ Built-in Quality Assurance Our system ensures clinical reliability through a sophisticated automated pipeline: - **Smart Image Validation**: Automatically detects poor lighting, blur, or incorrect focus before analysis begins. - **Retinal Enhancement**: Applies clinical-grade contrast enhancement to make subtle microaneurysms and haemorrhages more visible. - **Explainable Diagnostics**: Generates a **Clinical Attention Map** that highlights the specific pathological areas identified by the AI. --- ### 🧠 Modern AI Architecture DrRetina is built on cutting-edge infrastructure optimized for medical precision: - **Vision Core**: A Vision Transformer (ViT-MAE) specialized in ophthalmic features. - **Agentic Layer**: Powered by **Qwen3-8B**, providing structured clinical reporting and natural language Q&A. - **High-Performance Hardware**: Fine-tuned on **AMD Instinct™ MI300X** for superior medical precision. --- ### 📋 Seamless Workflow | Phase | Description | |-------|-----------| | **1. Analysis** | Instant grading from No DR to Proliferative DR with confidence metrics. | | **2. Visualization** | Detailed attention mapping showing areas of clinical concern. | | **3. Reporting** | Automated, multi-lingual clinical reports generated in seconds. | --- ### 📈 Grading Scale & Clinical Action | Grade | Name | Recommended Action | |-------|------|--------------------| | 🟢 0 | No DR | Routine annual review. | | 🟡 1 | Mild DR | 6-month follow-up and metabolic control. | | 🟠 2 | Moderate DR | Specialist referral within 3 months. | | 🔴 3 | Severe DR | Urgent referral within 1 month. | | 🆘 4 | Proliferative | Emergency referral; immediate risk of vision loss. | --- > ⚠️ **Clinical Note**: DrRetina is an AI screening tool designed to support, not replace, professional ophthalmic evaluation. Always consult a qualified medical professional for definitive diagnosis. ### ⚙️ Technology Stack | Component | Technology | Framework / Source | |-----------|------------|---------------------| | **Vision Model** | ViT-MAE Encoder + Classification Head | PyTorch (ROCm), HuggingFace | | **Explainability** | GradCAM Engine | pytorch-grad-cam | | **Agent** | Report Generator + Q&A | Qwen3-8B (Featherless AI) | | **Interface** | Gradio Web UI | Gradio 5.x | | **Training Compute** | AMD Instinct MI300X | ROCm 6.x | | **Deployment** | HF Spaces | AMD Hackathon Org | --- ### 🧠 Model Architecture ``` facebook/vit-mae-base └─ 12 Transformer Encoder Blocks (768-dim, 12 heads) └─ 224×224 input → 196 patches (16×16 each) └─ mask_ratio = 0.0 (all patches used for inference) Classification Head: Linear(768, 256) → BatchNorm1d → ReLU → Dropout(0.3) → Linear(256, 5) ``` **Training:** Fine-tuned on APTOS 2019 (3,662 images) on AMD Instinct MI300X | Hyperparameter | Value | |---------------|-------| | Optimizer | AdamW + Weight Decay | | Backbone LR | 2e-5 | | Head LR | 1e-3 | | Scheduler | Cosine Decay + 5-epoch warmup | | Batch Size | 128 | | Epochs | 30 | | Loss | Focal Loss (γ=2) + Label Smoothing | --- ### 📊 Performance Results | Metric | Target | **Achieved** | |--------|--------|-------------| | Cohen’s Kappa | > 0.85 | **0.9097** ✅ | | Accuracy | > 80% | **85.01%** ✅ | | Inference Latency | < 5s | **~2–3s** ✅ | """) gr.Markdown(""" --- ### 📈 DR Grading Scale (ICDR Classification) | Grade | Name | Expected Lesions | Action | |-------|------|-----------------|--------| | 🟢 0 | No DR | None | 12-month routine review | | 🟡 1 | Mild DR | Microaneurysms only | 6-month follow-up | | 🟠 2 | Moderate DR | Microaneurysms, exudates, oedema | Referral within 3 months | | 🔴 3 | Severe DR | >20 haemorrhages/quadrant, IRMA | Urgent referral 1 month | | 🆘 4 | Proliferative DR | Neovascularisation, vitreous haemorrhage | Emergency referral | --- """) return demo