Spaces:
Sleeping
Sleeping
File size: 13,286 Bytes
2e552e9 e7e6abe 2e552e9 be96fd0 2e552e9 e7e6abe 68bb893 e7e6abe 2e552e9 e7e6abe 2e552e9 e7e6abe 2e552e9 e7e6abe 2e552e9 e7e6abe 478712e bdd5f1a 2e552e9 e7e6abe 2e552e9 e7e6abe 50d6286 e7e6abe 50d6286 e7e6abe 50d6286 2e552e9 e7e6abe 2e552e9 e7e6abe 2e552e9 0481484 e7e6abe 0481484 e7e6abe 478712e e7e6abe 2e552e9 bdd5f1a 2e552e9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | import io
import os
import requests
import gradio as gr
from PIL import Image
PORT = os.getenv("PORT", "7860")
BACKEND_URL = os.getenv("BACKEND_PREDICT_URL", f"http://127.0.0.1:{PORT}/predict")
def call_backend(image: Image.Image, api_url: str) -> dict:
buf = io.BytesIO()
image.convert("RGB").save(buf, format="PNG")
buf.seek(0)
resp = requests.post(
api_url,
files={"file": ("xray.png", buf.getvalue(), "image/png")},
timeout=60,
)
resp.raise_for_status()
return resp.json()
def render_top_metrics(payload: dict) -> tuple[str, str]:
model_arm = payload.get("selected_model", "Unknown")
latency_ms = max(payload.get("latency_ms", 0), 0)
latency_html = f'<div class="metric-value">{latency_ms} ms</div>'
if latency_ms < 0:
latency_html = f'''
<div class="metric-value" style="color: #ef4444;">{latency_ms} ms</div>
<div style="background: #452424; border-left: 4px solid #ef4444; padding: 8px 12px; border-radius: 4px; margin-top: 8px;">
<div style="color:#fca5a5;font-weight:600;font-size:0.85rem;">Bug: negative latency</div>
<div style="color:#fecaca;font-size:0.8rem;">Timing value is invalid β likely a clock sync issue.<br>Fix the timer before showing this to patients.</div>
</div>
'''
arm_html = f'''
<div class="custom-card">
<div class="metric-title">MODEL (A/B ARM)</div>
<div class="metric-value">{model_arm}</div>
</div>
'''
lat_html = f'''
<div class="custom-card">
<div class="metric-title">INFERENCE LATENCY</div>
{latency_html}
</div>
'''
return arm_html, lat_html
def render_quality(payload: dict) -> str:
drift = payload.get("drift", {})
alert = drift.get("drift_alert", "NORMAL")
if alert == "DRIFT_DETECTED":
return '''
<div class="custom-card" style="border-left: 4px solid #ef4444;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div class="metric-title">INPUT IMAGE QUALITY CHECK</div>
<div style="background:#452424; color:#fca5a5; padding:4px 10px; border-radius:12px; font-size:0.75rem;">β Out of Distribution</div>
</div>
<div style="font-size:1.1rem; font-weight:500; margin-top:4px;">Distribution differs from training baseline</div>
<div style="background:#452424; padding:12px; border-radius:6px; margin-top:12px;">
<div style="color:#fbbf24; font-weight:600; font-size:0.9rem; margin-bottom:4px;">What this means for the patient</div>
<div style="color:#d1d5db; font-size:0.85rem;">The model has never seen images like this. Predictions may be unreliable. Proceed with extreme caution.</div>
</div>
</div>
'''
return '''
<div class="custom-card">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div class="metric-title">INPUT IMAGE QUALITY CHECK</div>
<div style="background:#064e3b; color:#34d399; padding:4px 10px; border-radius:12px; font-size:0.75rem;">β Acceptable</div>
</div>
<div style="font-size:1.1rem; font-weight:500; margin-top:4px; margin-bottom:12px;">Distribution matches training baseline</div>
<div style="background:#453015; padding:12px; border-radius:6px; margin-top:8px; border-left: 4px solid #f59e0b;">
<div style="color:#fbbf24; font-weight:600; font-size:0.9rem; margin-bottom:4px;">What this means for the patient</div>
<div style="color:#d1d5db; font-size:0.85rem; line-height:1.4;">This <i>does not</i> mean your lungs are normal. It only means the uploaded X-ray image is technically readable by the model β the contrast, resolution, and framing look similar to images it was trained on.</div>
</div>
</div>
'''
_TIER_COLORS = {
"Pneumonia": "#ef4444", "Pneumothorax": "#ef4444", "Mass": "#ef4444", "Nodule": "#ef4444",
"Effusion": "#f59e0b", "Cardiomegaly": "#f59e0b", "Consolidation": "#f59e0b",
}
_TIER_DESC = {
"Pneumonia": "Infection inflaming air sacs, potentially filling with fluid.",
"Pneumothorax": "Collapsed lung β air leaked into the space between lung and chest wall.",
"Mass": "Larger opacity (>3cm). Not actionable at this score.",
"Nodule": "A small round opacity β may be benign or require follow-up.",
"Effusion": "Fluid build-up in the pleural space.",
"Cardiomegaly": "Enlarged heart β could indicate underlying heart conditions.",
"Consolidation": "Region of normally compressible lung tissue filled with liquid.",
"Infiltration": "Fluid or tissue density where there should be air β could suggest infection, inflammation, or early pneumonia.",
}
def render_top3(payload: dict) -> str:
top3 = payload.get("top_predictions", [])
inconclusive = payload.get("inconclusive_scan", False)
html = '<div class="custom-card">'
html += '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">'
html += '<div class="metric-title">TOP-3 PREDICTED PATHOLOGIES</div>'
html += '<div style="font-size:0.8rem; color:#9ca3af;"><span style="color:#ef4444;">β</span> Critical <span style="color:#f59e0b;">β</span> Significant <span style="color:#3b82f6;">β</span> Standard</div>'
html += '</div>'
for p in top3:
lbl = p["label"]
conf = p["confidence"]
col = _TIER_COLORS.get(lbl, "#3b82f6")
desc = _TIER_DESC.get(lbl, "A generic chest anomaly flagged by the model.")
conf_text = f"Confidence is very low ({conf}%)." if conf < 10 else f"Confidence: {conf}%."
flagged = '<span style="background:#1e3a8a; color:#93c5fd; padding:4px 10px; border-radius:12px; font-size:0.75rem; margin-left:8px;">Flagged finding</span>' if p["detected"] else ''
html += f'''
<div style="background:#333333; padding:16px; border-radius:8px; margin-bottom:12px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<div style="font-size:1.1rem; font-weight:600;"><span style="color:{col}; font-size:1.2rem;">β</span> {lbl} {flagged}</div>
<div style="font-weight:600; font-size:1.2rem;">{conf}%</div>
</div>
<div style="width:100%; background:#4b5563; height:8px; border-radius:4px; overflow:hidden; margin-bottom:12px;">
<div style="width:{conf}%; background:{col}; height:100%;"></div>
</div>
<div style="font-size:0.85rem; color:#d1d5db;">{desc} <strong>{conf_text}</strong></div>
</div>
'''
if inconclusive:
html += '''
<div style="background:#452424; border:1px solid #7f1d1d; padding:16px; border-radius:8px; margin-top:16px;">
<div style="color:#fca5a5; font-weight:600; margin-bottom:4px;">All confidence scores are below 10% β interpret with extreme caution</div>
<div style="color:#fecaca; font-size:0.9rem; line-height:1.4;">Scores this low indicate the model is uncertain. A well-calibrated model should ideally show >50% before a finding is clinically noteworthy. These results should <i>not</i> drive any clinical decision without radiologist review.</div>
</div>
'''
html += '</div>'
return html
def render_14_classes(payload: dict) -> str:
all_preds = payload.get("all_predictions", [])
html = '<div class="custom-card">'
html += '<div class="metric-title" style="margin-bottom:12px;">ALL 14 SCREENED CONDITIONS</div>'
html += '<div style="display:flex; flex-wrap:wrap; gap:8px;">'
for p in all_preds:
lbl = p["label"]
conf = p["confidence"]
det = p["detected"]
if det:
bg, color = "#78350f", "#fcd34d" # Yellow highlight
else:
bg, color = "#374151", "#9ca3af" # Gray
text = f"{lbl} {conf}%" if det else lbl
html += f'<div style="background:{bg}; color:{color}; padding:6px 14px; border-radius:16px; font-size:0.85rem; font-weight:500; border:1px solid #4b5563;">{text}</div>'
html += '</div>'
html += '<div style="font-size:0.8rem; color:#6b7280; margin-top:16px;">Grey chips = below detection threshold. Not shown does not mean absent.</div>'
html += '</div>'
return html
def render_calibration_info() -> str:
"""Provides historical benchmark context as requested in the redesign proposal."""
return '''
<div class="custom-card">
<div class="metric-title">MODEL CALIBRATION & BENCHMARKING</div>
<div style="font-size:0.88rem; color:#d1d5db; line-height:1.6;">
Model calibrated on NIH ChestX-ray14. Historical AUC benchmarks:
<ul style="margin-top:8px; padding-left:18px; margin-bottom:8px;">
<li><b style="color:#34d399;">Cardiomegaly:</b> ~0.81 AUC (Highly reliable)</li>
<li><b style="color:#fbbf24;">Infiltration:</b> ~0.70 AUC (Historically lower reliability)</li>
<li><b style="color:#fbbf24;">Pneumonia:</b> ~0.68 AUC</li>
</ul>
<i>Note for Clinicians:</i> Predictions for "Infiltration" and "Pneumonia" carry higher uncertainty due to lower historical AUC. Radiologist verification is mandatory.
</div>
</div>
'''
def predict(image: Image.Image, api_url: str):
if image is None:
raise gr.Error("Please upload a chest X-ray image.")
try:
payload = call_backend(image, api_url)
except Exception as exc:
raise gr.Error(f"Backend error: {str(exc)}") from exc
arm_html, lat_html = render_top_metrics(payload)
qc_html = render_quality(payload)
top3_html = render_top3(payload)
classes_html = render_14_classes(payload)
calib_html = render_calibration_info()
ethics_html = """<div class="custom-card" style="border-top: 4px solid #ef4444; background: #450a0a;">
<div class="metric-title">ETHICAL & CLINICAL COMPLIANCE</div>
<div style="font-size:0.9rem; color:#fca5a5;">
<b>Data Privacy:</b> HIPAA-compliant processing. No PHI is stored.<br>
<b>Model Bias:</b> Regular audits for demographic parity across age and gender.<br>
<b>Clinical Intent:</b> Screening assistant only. Not for primary diagnosis.
</div>
</div>"""
summary_html = f'<div class="custom-card" style="border-left: 4px solid #3b82f6; background: #1e293b; padding: 15px; margin-bottom: 10px;"><div class="metric-title">CLINICAL SUMMARY</div><div style="font-size:1.1rem; font-weight:500; color:#f8fafc;">{payload.get("summary", "Screening complete.")}</div></div>'
return arm_html, lat_html, summary_html, ethics_html, qc_html, top3_html, classes_html, calib_html
CSS = """
body, .gradio-container {
background-color: #1e1e1e !important;
color: #e5e5e5 !important;
}
.custom-card {
background: #2b2b2b;
border-radius: 12px;
padding: 20px;
border: 1px solid #3f3f46;
margin-bottom: 16px;
}
.metric-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9ca3af;
margin-bottom: 6px;
font-weight: 600;
}
.metric-value {
font-size: 1.25rem;
font-weight: 500;
color: #f4f4f5;
}
"""
with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="PneumoOps Redesigned") as demo:
with gr.Accordion("βοΈ Backend Settings", open=False):
api_url = gr.Textbox(label="Backend URL", value=BACKEND_URL)
with gr.Row():
with gr.Column(scale=1):
image_input = gr.Image(type="pil", label="Upload Chest X-Ray")
submit_btn = gr.Button("π¬ Run Screening", variant="primary", size="lg")
with gr.Column(scale=2):
with gr.Row():
arm_out = gr.HTML()
lat_out = gr.HTML()
summary_out = gr.HTML()
ethics_out = gr.HTML()
qc_out = gr.HTML()
top3_out = gr.HTML()
classes_out = gr.HTML()
calib_out = gr.HTML()
gr.HTML('''
<div class="custom-card">
<div style="font-weight:600; font-size:0.95rem; margin-bottom:4px; display:flex; align-items:center; gap:8px;">
βοΈ Responsible AI notice
</div>
<div style="color:#9ca3af; font-size:0.9rem; line-height:1.5;">
This system <i>assists in prediction</i>, it does not confirm diagnoses. Results are statistical estimates and must be reviewed by a qualified radiologist or clinician before any medical decision is made. Do not use these results to self-diagnose.
</div>
</div>
''')
submit_btn.click(
fn=predict,
inputs=[image_input, api_url],
outputs=[arm_out, lat_out, summary_out, ethics_out, qc_out, top3_out, classes_out, calib_out],
)
if __name__ == "__main__":
port = int(os.getenv("GRADIO_PORT", "7860"))
demo.launch(server_name="0.0.0.0", server_port=port)
|