#!/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
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""""""
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""""""
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("""
""")
# Grade result
grade_html = gr.HTML(EMPTY_STATE_HTML)
# Divider
gr.HTML("""
""")
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:
Grade explanation
Next steps
Prevention
""")
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