Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -130,41 +130,88 @@ CLINICAL_GUIDANCE = (
|
|
| 130 |
# ============================================================
|
| 131 |
WELCOME_HTML = f"""
|
| 132 |
<div style="max-width:980px;margin:0 auto;">
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
</div>
|
| 137 |
|
| 138 |
-
<div style="
|
| 139 |
-
|
| 140 |
</div>
|
| 141 |
|
| 142 |
-
<div style="margin-top:10px;display:
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
<
|
| 150 |
-
|
| 151 |
-
<
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
</div>
|
| 159 |
|
| 160 |
-
<div style="margin-top:
|
| 161 |
-
<b>
|
|
|
|
|
|
|
| 162 |
</div>
|
|
|
|
| 163 |
</div>
|
| 164 |
|
| 165 |
-
<div style="margin-top:
|
| 166 |
-
<b>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
</div>
|
|
|
|
| 168 |
</div>
|
| 169 |
"""
|
| 170 |
|
|
@@ -198,6 +245,153 @@ def badge_color_for_state(state: str) -> str:
|
|
| 198 |
return "rgba(148,163,184,0.12)" # gray
|
| 199 |
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
# ============================================================
|
| 202 |
# LUNG U-NET (INFERENCE)
|
| 203 |
# ============================================================
|
|
@@ -1063,14 +1257,28 @@ def run_analysis(
|
|
| 1063 |
gallery_items = []
|
| 1064 |
details_md: List[str] = []
|
| 1065 |
|
| 1066 |
-
# Top banner
|
| 1067 |
summary_md.append(f"""
|
| 1068 |
-
<div style="border:1px solid rgba(255,255,255,0.
|
| 1069 |
-
<div style="font-size:
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
<b>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
</div>
|
| 1075 |
</div>
|
| 1076 |
""")
|
|
@@ -1099,7 +1307,7 @@ def run_analysis(
|
|
| 1099 |
img_size=224,
|
| 1100 |
)
|
| 1101 |
|
| 1102 |
-
# RADIO (optional)
|
| 1103 |
radio_text_long = f"{MODEL_NAME_RADIO} disabled."
|
| 1104 |
radio_raw_overlay = None
|
| 1105 |
radio_masked_overlay = None
|
|
@@ -1147,7 +1355,7 @@ def run_analysis(
|
|
| 1147 |
radio_band = None
|
| 1148 |
radio_masked_ran = False
|
| 1149 |
|
| 1150 |
-
# Consensus
|
| 1151 |
consensus_label, consensus_detail, tb_state, radio_state = build_consensus(
|
| 1152 |
tb_prob=out["prob"],
|
| 1153 |
tb_band=out["band"],
|
|
@@ -1163,84 +1371,106 @@ def run_analysis(
|
|
| 1163 |
attention = "Diffuse / non-focal" if out.get("diffuse_risk", False) else "Focal / localized"
|
| 1164 |
|
| 1165 |
warns = out.get("warnings", [])
|
| 1166 |
-
top_warns = warns[:3] if warns else []
|
| 1167 |
-
top_warn_line = " β’ ".join([html_escape(w) for w in top_warns]) if top_warns else "None"
|
| 1168 |
-
|
| 1169 |
radio_primary_line = "N/A" if radio_primary_val is None else f"{radio_primary_val:.4f}"
|
| 1170 |
radio_raw_line = "N/A" if radio_raw_val is None else f"{radio_raw_val:.4f}"
|
| 1171 |
radio_masked_line = "Not run" if radio_masked_val is None else f"{radio_masked_val:.4f}"
|
| 1172 |
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1198 |
</div>
|
| 1199 |
-
|
| 1200 |
-
<div style="
|
| 1201 |
-
{
|
| 1202 |
</div>
|
| 1203 |
-
</div>
|
| 1204 |
-
"""
|
| 1205 |
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
<div style="border:
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
</
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
<div style="border:
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1222 |
</div>
|
| 1223 |
-
<div style="margin-top:6px; opacity:0.85;">{html_escape(gate_info)}</div>
|
| 1224 |
-
</div>
|
| 1225 |
-
"""
|
| 1226 |
|
| 1227 |
-
|
| 1228 |
-
<div style="
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1232 |
</div>
|
| 1233 |
-
|
| 1234 |
-
<div style="
|
| 1235 |
-
|
| 1236 |
-
{next_step}
|
| 1237 |
</div>
|
|
|
|
| 1238 |
</div>
|
| 1239 |
"""
|
| 1240 |
|
| 1241 |
-
summary_md.append(
|
| 1242 |
-
summary_md.append(radio_card)
|
| 1243 |
-
summary_md.append(consensus_card)
|
| 1244 |
|
| 1245 |
# Gallery
|
| 1246 |
orig_rgb = cv2.cvtColor(cv2.resize(out["orig_gray"], (512, 512)), cv2.COLOR_GRAY2RGB)
|
|
@@ -1248,27 +1478,27 @@ def run_analysis(
|
|
| 1248 |
mask_overlay = cv2.resize(out["mask_overlay"], (512, 512))
|
| 1249 |
overlay_big = cv2.resize(out["overlay"], (512, 512))
|
| 1250 |
|
| 1251 |
-
gallery_items.append((orig_rgb, f"{name} β’
|
| 1252 |
-
gallery_items.append((vis_rgb, f"{name} β’
|
| 1253 |
-
gallery_items.append((mask_overlay, f"{name} β’ Lung
|
| 1254 |
if out["proc_gray"] is not None:
|
| 1255 |
proc_rgb = cv2.cvtColor(cv2.resize(out["proc_gray"], (512, 512)), cv2.COLOR_GRAY2RGB)
|
| 1256 |
-
gallery_items.append((proc_rgb, f"{name} β’
|
| 1257 |
-
gallery_items.append((overlay_big, f"{name} β’
|
| 1258 |
|
| 1259 |
if radio_raw_overlay is not None:
|
| 1260 |
-
gallery_items.append((cv2.resize(radio_raw_overlay, (512, 512)), f"{name} β’
|
| 1261 |
if radio_masked_overlay is not None:
|
| 1262 |
-
gallery_items.append((cv2.resize(radio_masked_overlay, (512, 512)), f"{name} β’
|
| 1263 |
|
| 1264 |
-
#
|
| 1265 |
warn_txt = "\n".join([f"- {w}" for w in out["warnings"]]) if out["warnings"] else "- None"
|
| 1266 |
details_md.append(
|
| 1267 |
f"""
|
| 1268 |
<details>
|
| 1269 |
-
<summary><b>{html_escape(name)}</b> β
|
| 1270 |
|
| 1271 |
-
**TBNet**
|
| 1272 |
- Result: **{html_escape(tb_label)}**
|
| 1273 |
- Probability: {tb_prob_line}
|
| 1274 |
- Band: {out.get("band", "YELLOW")}
|
|
@@ -1276,16 +1506,23 @@ def run_analysis(
|
|
| 1276 |
- Lung mask coverage: {cov*100:.1f}%
|
| 1277 |
- Attention: {attention}
|
| 1278 |
|
| 1279 |
-
**
|
| 1280 |
-
-
|
| 1281 |
-
-
|
| 1282 |
- Consensus label: **{html_escape(consensus_label)}**
|
| 1283 |
-
-
|
| 1284 |
|
| 1285 |
-
**
|
| 1286 |
{warn_txt}
|
| 1287 |
|
| 1288 |
-
**RADIO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
{radio_text_long}
|
| 1290 |
|
| 1291 |
</details>
|
|
@@ -1309,99 +1546,129 @@ def build_ui():
|
|
| 1309 |
.card {border:1px solid rgba(255,255,255,0.12); border-radius:14px; padding:14px; margin:10px 0;}
|
| 1310 |
"""
|
| 1311 |
|
| 1312 |
-
with gr.Blocks(title="
|
| 1313 |
|
| 1314 |
-
#
|
| 1315 |
-
# Welcome screen (shown first)
|
| 1316 |
-
# ---------------------------
|
| 1317 |
with gr.Column(visible=True) as welcome_screen:
|
| 1318 |
-
gr.Markdown('<div class="title">Welcome β
|
| 1319 |
gr.HTML(WELCOME_HTML)
|
| 1320 |
-
continue_btn = gr.Button("
|
| 1321 |
|
| 1322 |
-
#
|
| 1323 |
-
# Main app UI (hidden initially)
|
| 1324 |
-
# ---------------------------
|
| 1325 |
with gr.Column(visible=False) as main_app:
|
| 1326 |
-
gr.Markdown('<div class="title">
|
|
|
|
| 1327 |
gr.Markdown(
|
| 1328 |
-
|
| 1329 |
-
|
|
|
|
|
|
|
| 1330 |
)
|
| 1331 |
|
| 1332 |
gr.Markdown(
|
| 1333 |
-
"<div class='warnbox'><b>
|
| 1334 |
-
"If TB is clinically suspected,
|
|
|
|
| 1335 |
)
|
| 1336 |
|
| 1337 |
with gr.Row():
|
| 1338 |
-
with gr.Column(scale=
|
| 1339 |
-
gr.Markdown("###
|
| 1340 |
-
|
| 1341 |
-
tb_weights = gr.Textbox(label="TBNet weights (.pt)", value=DEFAULT_TB_WEIGHTS)
|
| 1342 |
-
lung_weights = gr.Textbox(label="Lung U-Net weights (.pt)", value=DEFAULT_LUNG_WEIGHTS)
|
| 1343 |
-
|
| 1344 |
-
backbone = gr.Dropdown(
|
| 1345 |
-
choices=["efficientnet_b0"],
|
| 1346 |
-
value="efficientnet_b0",
|
| 1347 |
-
label="TBNet backbone"
|
| 1348 |
-
)
|
| 1349 |
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
)
|
| 1354 |
|
| 1355 |
phone_mode = gr.Checkbox(
|
| 1356 |
value=False,
|
| 1357 |
-
label="
|
| 1358 |
)
|
|
|
|
| 1359 |
gr.Markdown(
|
| 1360 |
-
"<div class='subtitle'>
|
| 1361 |
-
"
|
|
|
|
|
|
|
| 1362 |
)
|
| 1363 |
|
| 1364 |
-
use_radio = gr.Checkbox(
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
label="RADIO masked gate (run masked head if lung coverage β₯ gate)"
|
| 1368 |
)
|
| 1369 |
|
| 1370 |
-
gr.
|
| 1371 |
-
|
| 1372 |
-
f"{MODEL_NAME_TBNET} scoring is disabled to avoid unsafe outputs.</div>"
|
| 1373 |
-
)
|
| 1374 |
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1378 |
|
| 1379 |
back_btn = gr.Button("β Back to Welcome", variant="secondary")
|
| 1380 |
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
status = gr.Textbox(label="Status", value="Ready.", interactive=False)
|
| 1389 |
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1394 |
""")
|
| 1395 |
|
| 1396 |
-
gr.
|
| 1397 |
-
summary = gr.Markdown("Upload images and click <b>Run Analysis</b>.")
|
| 1398 |
-
gallery = gr.Gallery(label="Visual outputs", columns=3, height=560)
|
| 1399 |
|
| 1400 |
with gr.Row():
|
| 1401 |
with gr.Column(scale=1):
|
| 1402 |
disclaimer_box = gr.Markdown(CLINICAL_DISCLAIMER)
|
| 1403 |
with gr.Column(scale=2):
|
| 1404 |
-
gr.Markdown("###
|
|
|
|
|
|
|
|
|
|
| 1405 |
details = gr.Markdown("")
|
| 1406 |
|
| 1407 |
run_btn.click(
|
|
@@ -1419,9 +1686,7 @@ def build_ui():
|
|
| 1419 |
outputs=[summary, gallery, details, disclaimer_box, status]
|
| 1420 |
)
|
| 1421 |
|
| 1422 |
-
# ---------------------------
|
| 1423 |
# Transitions
|
| 1424 |
-
# ---------------------------
|
| 1425 |
continue_btn.click(
|
| 1426 |
fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 1427 |
inputs=[],
|
|
@@ -1443,7 +1708,7 @@ if __name__ == "__main__":
|
|
| 1443 |
server_name="0.0.0.0",
|
| 1444 |
server_port=int(os.environ.get("PORT", 7860)),
|
| 1445 |
show_error=True,
|
| 1446 |
-
ssr_mode=False,
|
| 1447 |
css="""
|
| 1448 |
.title {font-size: 28px; font-weight: 900; margin-bottom: 6px;}
|
| 1449 |
.subtitle {font-size: 14px; opacity: 0.88; margin-bottom: 14px;}
|
|
|
|
| 130 |
# ============================================================
|
| 131 |
WELCOME_HTML = f"""
|
| 132 |
<div style="max-width:980px;margin:0 auto;">
|
| 133 |
+
|
| 134 |
+
<div style="padding:20px;border-radius:18px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);">
|
| 135 |
+
|
| 136 |
+
<div style="font-size:24px;font-weight:950;margin-bottom:8px;">
|
| 137 |
+
Chest X-ray TB Screening Assistant
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div style="font-size:15px;line-height:1.55;opacity:0.92;">
|
| 141 |
+
This app helps review chest X-ray images for signs that may be seen with pulmonary tuberculosis,
|
| 142 |
+
also called TB. It is meant to support medical review, not replace a doctor, radiologist, or lab test.
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
<div style="margin-top:18px;padding:14px;border-radius:14px;background:rgba(59,130,246,0.10);border:1px solid rgba(59,130,246,0.22);">
|
| 146 |
+
<div style="font-size:17px;font-weight:900;margin-bottom:8px;">How the app works</div>
|
| 147 |
+
|
| 148 |
+
<div style="font-size:14px;line-height:1.55;">
|
| 149 |
+
<b>Step 1 β Upload a chest X-ray image.</b><br/>
|
| 150 |
+
You can upload a normal X-ray image, phone photo, WhatsApp image, or screenshot.
|
| 151 |
+
<br/><br/>
|
| 152 |
+
|
| 153 |
+
<b>Step 2 β The app checks the lung area.</b><br/>
|
| 154 |
+
It tries to find the lung region first, so the AI focuses on the chest instead of borders,
|
| 155 |
+
labels, or background.
|
| 156 |
+
<br/><br/>
|
| 157 |
+
|
| 158 |
+
<b>Step 3 β The AI checks for TB-like patterns.</b><br/>
|
| 159 |
+
The app gives a simple result and shows image views that help explain what was checked.
|
| 160 |
+
</div>
|
| 161 |
</div>
|
| 162 |
|
| 163 |
+
<div style="margin-top:18px;font-size:17px;font-weight:900;">
|
| 164 |
+
What the result means
|
| 165 |
</div>
|
| 166 |
|
| 167 |
+
<div style="margin-top:10px;display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:10px;">
|
| 168 |
+
|
| 169 |
+
<div style="padding:12px;border-radius:14px;background:rgba(34,197,94,0.12);border:1px solid rgba(34,197,94,0.25);">
|
| 170 |
+
<div style="font-weight:900;">β
Low</div>
|
| 171 |
+
<div style="font-size:13px;line-height:1.45;margin-top:4px;">
|
| 172 |
+
The AI did not find a strong TB-like pattern in the image.
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<div style="padding:12px;border-radius:14px;background:rgba(245,158,11,0.12);border:1px solid rgba(245,158,11,0.25);">
|
| 177 |
+
<div style="font-weight:900;">β οΈ Unclear</div>
|
| 178 |
+
<div style="font-size:13px;line-height:1.45;margin-top:4px;">
|
| 179 |
+
The image or AI result is not reliable enough. Medical review is advised.
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<div style="padding:12px;border-radius:14px;background:rgba(245,158,11,0.18);border:1px solid rgba(245,158,11,0.32);">
|
| 184 |
+
<div style="font-weight:900;">β οΈ Needs review</div>
|
| 185 |
+
<div style="font-size:13px;line-height:1.45;margin-top:4px;">
|
| 186 |
+
The AI found a possible TB-like pattern. A clinician or radiologist should review it.
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div style="padding:12px;border-radius:14px;background:rgba(239,68,68,0.16);border:1px solid rgba(239,68,68,0.32);">
|
| 191 |
+
<div style="font-weight:900;">π© TB-like pattern seen</div>
|
| 192 |
+
<div style="font-size:13px;line-height:1.45;margin-top:4px;">
|
| 193 |
+
The AI found stronger TB-like features. Prompt medical review is recommended.
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
|
| 197 |
</div>
|
| 198 |
|
| 199 |
+
<div style="margin-top:18px;padding:13px;border-radius:14px;background:rgba(148,163,184,0.10);border:1px solid rgba(148,163,184,0.22);font-size:14px;line-height:1.5;">
|
| 200 |
+
<b>When to use Phone/WhatsApp Mode:</b><br/>
|
| 201 |
+
Turn it on if the X-ray is a phone photo, WhatsApp-forwarded image, screenshot, cropped image,
|
| 202 |
+
or has black/white borders. It helps the app handle common image-quality issues.
|
| 203 |
</div>
|
| 204 |
+
|
| 205 |
</div>
|
| 206 |
|
| 207 |
+
<div style="margin-top:14px;padding:14px 16px;border-left:6px solid #f59e0b;border-radius:14px;background:rgba(245,158,11,0.12);font-size:14px;line-height:1.55;">
|
| 208 |
+
<b>Important medical notice:</b><br/>
|
| 209 |
+
This app is not a diagnostic test. A low AI result does <b>not</b> rule out TB, especially early,
|
| 210 |
+
subtle, diffuse, or miliary TB. If symptoms or clinical suspicion are present, please seek
|
| 211 |
+
clinician/radiologist review and consider CBNAAT/GeneXpert, sputum testing, and/or CT chest
|
| 212 |
+
regardless of the AI result.
|
| 213 |
</div>
|
| 214 |
+
|
| 215 |
</div>
|
| 216 |
"""
|
| 217 |
|
|
|
|
| 245 |
return "rgba(148,163,184,0.12)" # gray
|
| 246 |
|
| 247 |
|
| 248 |
+
# ============================================================
|
| 249 |
+
# PATIENT-FRIENDLY RESULT HELPERS
|
| 250 |
+
# ============================================================
|
| 251 |
+
def patient_state_label(state: str) -> str:
|
| 252 |
+
return {
|
| 253 |
+
"LOW": "β
Low TB-like pattern",
|
| 254 |
+
"INDET": "β οΈ Unclear result",
|
| 255 |
+
"SCREEN+": "β οΈ Needs medical review",
|
| 256 |
+
"TB+": "π© TB-like pattern seen",
|
| 257 |
+
"N/A": "β οΈ Not available",
|
| 258 |
+
}.get(state, "β οΈ Unclear result")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def patient_state_meaning(state: str) -> str:
|
| 262 |
+
return {
|
| 263 |
+
"LOW": (
|
| 264 |
+
"The AI did not find a strong TB-like pattern in this X-ray. "
|
| 265 |
+
"This does not completely rule out TB."
|
| 266 |
+
),
|
| 267 |
+
"INDET": (
|
| 268 |
+
"The result is not clear enough to rely on. This may happen because of image quality, "
|
| 269 |
+
"cropping, unusual positioning, or disagreement between AI checks."
|
| 270 |
+
),
|
| 271 |
+
"SCREEN+": (
|
| 272 |
+
"The AI found a possible TB-like pattern. This should be reviewed by a clinician or radiologist."
|
| 273 |
+
),
|
| 274 |
+
"TB+": (
|
| 275 |
+
"The AI found stronger TB-like features. This is not a diagnosis, but prompt medical review is recommended."
|
| 276 |
+
),
|
| 277 |
+
"N/A": (
|
| 278 |
+
"The app could not safely calculate a result for this image."
|
| 279 |
+
),
|
| 280 |
+
}.get(state, "The result is unclear and should be reviewed clinically.")
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def patient_quality_text(q: float) -> Tuple[str, str]:
|
| 284 |
+
if q >= 75:
|
| 285 |
+
return "Good image quality", "The image looks clear enough for AI screening."
|
| 286 |
+
if q >= 55:
|
| 287 |
+
return "Acceptable image quality", "The image can be checked, but some quality limits may affect accuracy."
|
| 288 |
+
return "Low image quality", "The image may be blurry, dark, overexposed, cropped, or compressed. The result is less reliable."
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def patient_lung_mask_text(cov: float) -> Tuple[str, str]:
|
| 292 |
+
if cov < FAIL_COV:
|
| 293 |
+
return "Lung area not found safely", "The app could not confidently find enough lung area, so TB scoring may be disabled."
|
| 294 |
+
if cov < WARN_COV:
|
| 295 |
+
return "Lung area partly found", "The app found only part of the lung area. The result may be less reliable."
|
| 296 |
+
return "Lung area found", "The app found enough lung area to run the AI screening."
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def patient_attention_text(diffuse: bool) -> str:
|
| 300 |
+
if diffuse:
|
| 301 |
+
return "The AI attention was spread out and not focused in one clear area. This makes the result less certain."
|
| 302 |
+
return "The AI attention was focused enough to create a useful heatmap."
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def patient_score_text(prob: Optional[float]) -> str:
|
| 306 |
+
if prob is None:
|
| 307 |
+
return "No TB score was calculated because the safety check stopped the AI from giving a possibly unreliable result."
|
| 308 |
+
return (
|
| 309 |
+
f"For transparency, the AI screening score was <b>{prob:.3f}</b>. "
|
| 310 |
+
"A higher score means the AI saw more TB-like pattern, but the score alone is not a diagnosis."
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def patient_radio_text(use_radio: bool, radio_state: str, radio_primary_val: Optional[float]) -> str:
|
| 315 |
+
if not use_radio:
|
| 316 |
+
return "The second AI check was turned off."
|
| 317 |
+
if radio_state == "N/A" or radio_primary_val is None:
|
| 318 |
+
return "The second AI check was not available for this image."
|
| 319 |
+
return (
|
| 320 |
+
f"The second AI check gave: <b>{patient_state_label(radio_state)}</b>. "
|
| 321 |
+
f"For transparency, its score was <b>{radio_primary_val:.3f}</b>."
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def patient_agreement_text(tb_state: str, radio_state: str, use_radio: bool) -> str:
|
| 326 |
+
if not use_radio:
|
| 327 |
+
return "Only the main AI check was used for this image."
|
| 328 |
+
|
| 329 |
+
if radio_state == "N/A":
|
| 330 |
+
return "Only the main AI check was available for this image."
|
| 331 |
+
|
| 332 |
+
if tb_state == radio_state:
|
| 333 |
+
return "Both AI checks gave a similar result."
|
| 334 |
+
|
| 335 |
+
if tb_state == "LOW" and radio_state in ("SCREEN+", "TB+"):
|
| 336 |
+
return "The two AI checks disagreed. Because one check found a possible TB-like pattern, treat this as unclear and review medically."
|
| 337 |
+
|
| 338 |
+
if radio_state == "LOW" and tb_state in ("SCREEN+", "TB+"):
|
| 339 |
+
return "The two AI checks disagreed. Because one check found a possible TB-like pattern, treat this as unclear and review medically."
|
| 340 |
+
|
| 341 |
+
return "The two AI checks were not fully aligned, so medical review is advised."
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def patient_final_state(tb_state: str, radio_state: str, use_radio: bool, consensus_label: str) -> str:
|
| 345 |
+
if tb_state == "TB+" or radio_state == "TB+" or consensus_label == "AGREE: TB+":
|
| 346 |
+
return "TB+"
|
| 347 |
+
|
| 348 |
+
if tb_state == "SCREEN+" or radio_state == "SCREEN+" or consensus_label == "AGREE: SCREEN+":
|
| 349 |
+
return "SCREEN+"
|
| 350 |
+
|
| 351 |
+
if tb_state == "INDET" or radio_state == "INDET":
|
| 352 |
+
return "INDET"
|
| 353 |
+
|
| 354 |
+
if tb_state == "LOW" and (not use_radio or radio_state in ("LOW", "N/A")):
|
| 355 |
+
return "LOW"
|
| 356 |
+
|
| 357 |
+
return "INDET"
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def patient_next_step_text(final_state: str) -> str:
|
| 361 |
+
if final_state == "TB+":
|
| 362 |
+
return (
|
| 363 |
+
"Please arrange prompt clinician/radiologist review. If TB is clinically suspected, "
|
| 364 |
+
"microbiological confirmation such as CBNAAT/GeneXpert and sputum testing is recommended."
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
if final_state == "SCREEN+":
|
| 368 |
+
return (
|
| 369 |
+
"Please get clinician/radiologist review. This is not a diagnosis, but the image should not be ignored."
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
if final_state == "INDET":
|
| 373 |
+
return (
|
| 374 |
+
"Do not rely on this AI result alone. Repeat with a better-quality image if possible and seek medical review "
|
| 375 |
+
"if symptoms or TB risk factors are present."
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
return (
|
| 379 |
+
"No strong TB-like pattern was found by the AI. However, if there are symptoms, TB exposure, weight loss, fever, "
|
| 380 |
+
"cough, immunosuppression, or strong clinical concern, medical review and testing are still advised."
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def patient_warning_html(warnings: List[str]) -> str:
|
| 385 |
+
if not warnings:
|
| 386 |
+
return "No major image-quality warnings were detected."
|
| 387 |
+
|
| 388 |
+
items = "".join([f"<li>{html_escape(w)}</li>" for w in warnings[:4]])
|
| 389 |
+
extra = "" if len(warnings) <= 4 else f"<li>{len(warnings) - 4} more note(s) in the technical details.</li>"
|
| 390 |
+
return f"<ul style='margin:6px 0 0 18px;padding:0;'>{items}{extra}</ul>"
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
|
| 395 |
# ============================================================
|
| 396 |
# LUNG U-NET (INFERENCE)
|
| 397 |
# ============================================================
|
|
|
|
| 1257 |
gallery_items = []
|
| 1258 |
details_md: List[str] = []
|
| 1259 |
|
|
|
|
| 1260 |
summary_md.append(f"""
|
| 1261 |
+
<div style="border:1px solid rgba(255,255,255,0.14);border-radius:18px;padding:16px;margin:12px 0;background:rgba(255,255,255,0.035);">
|
| 1262 |
+
<div style="font-size:20px;font-weight:950;margin-bottom:8px;">Your X-ray screening results</div>
|
| 1263 |
+
|
| 1264 |
+
<div style="font-size:14px;line-height:1.55;opacity:0.94;">
|
| 1265 |
+
The app checks each image and gives a simple result. Please read the result together with the
|
| 1266 |
+
<b>image reliability</b> and <b>what to do next</b> sections.
|
| 1267 |
+
</div>
|
| 1268 |
+
|
| 1269 |
+
<div style="margin-top:12px;display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:8px;">
|
| 1270 |
+
<div style="padding:10px;border-radius:12px;background:rgba(34,197,94,0.12);border:1px solid rgba(34,197,94,0.24);font-size:13px;">
|
| 1271 |
+
β
<b>Low</b><br/>No strong TB-like pattern found.
|
| 1272 |
+
</div>
|
| 1273 |
+
<div style="padding:10px;border-radius:12px;background:rgba(245,158,11,0.12);border:1px solid rgba(245,158,11,0.24);font-size:13px;">
|
| 1274 |
+
β οΈ <b>Unclear</b><br/>Result is not reliable enough.
|
| 1275 |
+
</div>
|
| 1276 |
+
<div style="padding:10px;border-radius:12px;background:rgba(245,158,11,0.18);border:1px solid rgba(245,158,11,0.30);font-size:13px;">
|
| 1277 |
+
β οΈ <b>Needs review</b><br/>Possible TB-like pattern.
|
| 1278 |
+
</div>
|
| 1279 |
+
<div style="padding:10px;border-radius:12px;background:rgba(239,68,68,0.16);border:1px solid rgba(239,68,68,0.30);font-size:13px;">
|
| 1280 |
+
π© <b>TB-like pattern seen</b><br/>Prompt review advised.
|
| 1281 |
+
</div>
|
| 1282 |
</div>
|
| 1283 |
</div>
|
| 1284 |
""")
|
|
|
|
| 1307 |
img_size=224,
|
| 1308 |
)
|
| 1309 |
|
| 1310 |
+
# RADIO / second AI check (optional)
|
| 1311 |
radio_text_long = f"{MODEL_NAME_RADIO} disabled."
|
| 1312 |
radio_raw_overlay = None
|
| 1313 |
radio_masked_overlay = None
|
|
|
|
| 1355 |
radio_band = None
|
| 1356 |
radio_masked_ran = False
|
| 1357 |
|
| 1358 |
+
# Consensus / final result
|
| 1359 |
consensus_label, consensus_detail, tb_state, radio_state = build_consensus(
|
| 1360 |
tb_prob=out["prob"],
|
| 1361 |
tb_band=out["band"],
|
|
|
|
| 1371 |
attention = "Diffuse / non-focal" if out.get("diffuse_risk", False) else "Focal / localized"
|
| 1372 |
|
| 1373 |
warns = out.get("warnings", [])
|
|
|
|
|
|
|
|
|
|
| 1374 |
radio_primary_line = "N/A" if radio_primary_val is None else f"{radio_primary_val:.4f}"
|
| 1375 |
radio_raw_line = "N/A" if radio_raw_val is None else f"{radio_raw_val:.4f}"
|
| 1376 |
radio_masked_line = "Not run" if radio_masked_val is None else f"{radio_masked_val:.4f}"
|
| 1377 |
|
| 1378 |
+
final_state = patient_final_state(
|
| 1379 |
+
tb_state=tb_state,
|
| 1380 |
+
radio_state=radio_state,
|
| 1381 |
+
use_radio=use_radio,
|
| 1382 |
+
consensus_label=consensus_label,
|
| 1383 |
+
)
|
| 1384 |
+
|
| 1385 |
+
final_label = patient_state_label(final_state)
|
| 1386 |
+
final_meaning = patient_state_meaning(final_state)
|
| 1387 |
+
|
| 1388 |
+
quality_title, quality_text = patient_quality_text(q)
|
| 1389 |
+
mask_title, mask_text = patient_lung_mask_text(cov)
|
| 1390 |
+
|
| 1391 |
+
tb_score_text = patient_score_text(out["prob"])
|
| 1392 |
+
radio_user_text = patient_radio_text(use_radio, radio_state, radio_primary_val)
|
| 1393 |
+
agreement_text = patient_agreement_text(tb_state, radio_state, use_radio)
|
| 1394 |
+
next_step_user = patient_next_step_text(final_state)
|
| 1395 |
+
warning_html = patient_warning_html(warns)
|
| 1396 |
+
|
| 1397 |
+
final_badge = f"""
|
| 1398 |
+
<span style="display:inline-block;padding:7px 13px;border-radius:999px;background:{badge_color_for_state(final_state)};font-weight:900;font-size:15px;">
|
| 1399 |
+
{final_label}
|
| 1400 |
+
</span>
|
| 1401 |
+
"""
|
| 1402 |
+
|
| 1403 |
+
patient_card = f"""
|
| 1404 |
+
<div style="border:1px solid rgba(255,255,255,0.16);border-radius:18px;padding:16px;margin:14px 0;background:rgba(255,255,255,0.035);">
|
| 1405 |
+
|
| 1406 |
+
<div style="font-size:18px;font-weight:950;margin-bottom:8px;">
|
| 1407 |
+
Result for: {html_escape(name)}
|
| 1408 |
+
</div>
|
| 1409 |
+
|
| 1410 |
+
<div style="margin:8px 0 12px 0;">
|
| 1411 |
+
{final_badge}
|
| 1412 |
</div>
|
| 1413 |
+
|
| 1414 |
+
<div style="font-size:14px;line-height:1.55;opacity:0.95;margin-bottom:14px;">
|
| 1415 |
+
{html_escape(final_meaning)}
|
| 1416 |
</div>
|
|
|
|
|
|
|
| 1417 |
|
| 1418 |
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px;margin-top:10px;">
|
| 1419 |
+
|
| 1420 |
+
<div style="padding:12px;border-radius:14px;background:rgba(59,130,246,0.10);border:1px solid rgba(59,130,246,0.22);">
|
| 1421 |
+
<div style="font-weight:900;margin-bottom:5px;">1) What the main AI check found</div>
|
| 1422 |
+
<div style="font-size:13.5px;line-height:1.5;">
|
| 1423 |
+
<b>{patient_state_label(tb_state)}</b><br/>
|
| 1424 |
+
{tb_score_text}
|
| 1425 |
+
</div>
|
| 1426 |
+
</div>
|
| 1427 |
+
|
| 1428 |
+
<div style="padding:12px;border-radius:14px;background:rgba(99,102,241,0.10);border:1px solid rgba(99,102,241,0.22);">
|
| 1429 |
+
<div style="font-weight:900;margin-bottom:5px;">2) Second AI check</div>
|
| 1430 |
+
<div style="font-size:13.5px;line-height:1.5;">
|
| 1431 |
+
{radio_user_text}
|
| 1432 |
+
</div>
|
| 1433 |
+
</div>
|
| 1434 |
+
|
| 1435 |
+
<div style="padding:12px;border-radius:14px;background:rgba(148,163,184,0.10);border:1px solid rgba(148,163,184,0.22);">
|
| 1436 |
+
<div style="font-weight:900;margin-bottom:5px;">3) Image reliability</div>
|
| 1437 |
+
<div style="font-size:13.5px;line-height:1.5;">
|
| 1438 |
+
<b>{quality_title}</b><br/>
|
| 1439 |
+
{quality_text}<br/><br/>
|
| 1440 |
+
<b>{mask_title}</b><br/>
|
| 1441 |
+
{mask_text}
|
| 1442 |
+
</div>
|
| 1443 |
+
</div>
|
| 1444 |
+
|
| 1445 |
+
</div>
|
| 1446 |
+
|
| 1447 |
+
<div style="margin-top:12px;padding:12px;border-radius:14px;background:rgba(245,158,11,0.12);border:1px solid rgba(245,158,11,0.28);">
|
| 1448 |
+
<div style="font-weight:950;margin-bottom:5px;">What to do next</div>
|
| 1449 |
+
<div style="font-size:14px;line-height:1.55;">
|
| 1450 |
+
{next_step_user}
|
| 1451 |
+
</div>
|
| 1452 |
</div>
|
|
|
|
|
|
|
|
|
|
| 1453 |
|
| 1454 |
+
<div style="margin-top:12px;padding:12px;border-radius:14px;background:rgba(255,255,255,0.045);border:1px solid rgba(255,255,255,0.12);">
|
| 1455 |
+
<div style="font-weight:900;margin-bottom:5px;">Why this result may or may not be reliable</div>
|
| 1456 |
+
<div style="font-size:13.5px;line-height:1.5;">
|
| 1457 |
+
{html_escape(agreement_text)}
|
| 1458 |
+
<br/><br/>
|
| 1459 |
+
{html_escape(patient_attention_text(out.get("diffuse_risk", False)))}
|
| 1460 |
+
<br/><br/>
|
| 1461 |
+
<b>Image notes:</b>
|
| 1462 |
+
{warning_html}
|
| 1463 |
+
</div>
|
| 1464 |
</div>
|
| 1465 |
+
|
| 1466 |
+
<div style="margin-top:12px;font-size:12.5px;line-height:1.45;opacity:0.82;">
|
| 1467 |
+
This is an AI screening output only. It cannot confirm or exclude TB. Clinical symptoms and doctor/radiologist review remain important.
|
|
|
|
| 1468 |
</div>
|
| 1469 |
+
|
| 1470 |
</div>
|
| 1471 |
"""
|
| 1472 |
|
| 1473 |
+
summary_md.append(patient_card)
|
|
|
|
|
|
|
| 1474 |
|
| 1475 |
# Gallery
|
| 1476 |
orig_rgb = cv2.cvtColor(cv2.resize(out["orig_gray"], (512, 512)), cv2.COLOR_GRAY2RGB)
|
|
|
|
| 1478 |
mask_overlay = cv2.resize(out["mask_overlay"], (512, 512))
|
| 1479 |
overlay_big = cv2.resize(out["overlay"], (512, 512))
|
| 1480 |
|
| 1481 |
+
gallery_items.append((orig_rgb, f"{name} β’ Original image"))
|
| 1482 |
+
gallery_items.append((vis_rgb, f"{name} β’ Image checked by the app" if not phone_mode else f"{name} β’ Phone/WhatsApp cleaned image"))
|
| 1483 |
+
gallery_items.append((mask_overlay, f"{name} β’ Lung area found by the app"))
|
| 1484 |
if out["proc_gray"] is not None:
|
| 1485 |
proc_rgb = cv2.cvtColor(cv2.resize(out["proc_gray"], (512, 512)), cv2.COLOR_GRAY2RGB)
|
| 1486 |
+
gallery_items.append((proc_rgb, f"{name} β’ Lung-only image checked by AI"))
|
| 1487 |
+
gallery_items.append((overlay_big, f"{name} β’ Main AI heatmap"))
|
| 1488 |
|
| 1489 |
if radio_raw_overlay is not None:
|
| 1490 |
+
gallery_items.append((cv2.resize(radio_raw_overlay, (512, 512)), f"{name} β’ Second AI heatmap"))
|
| 1491 |
if radio_masked_overlay is not None:
|
| 1492 |
+
gallery_items.append((cv2.resize(radio_masked_overlay, (512, 512)), f"{name} β’ Second AI lung-only heatmap"))
|
| 1493 |
|
| 1494 |
+
# Technical details (collapsed per image)
|
| 1495 |
warn_txt = "\n".join([f"- {w}" for w in out["warnings"]]) if out["warnings"] else "- None"
|
| 1496 |
details_md.append(
|
| 1497 |
f"""
|
| 1498 |
<details>
|
| 1499 |
+
<summary><b>{html_escape(name)}</b> β technical details</summary>
|
| 1500 |
|
| 1501 |
+
**Main AI check / TBNet**
|
| 1502 |
- Result: **{html_escape(tb_label)}**
|
| 1503 |
- Probability: {tb_prob_line}
|
| 1504 |
- Band: {out.get("band", "YELLOW")}
|
|
|
|
| 1506 |
- Lung mask coverage: {cov*100:.1f}%
|
| 1507 |
- Attention: {attention}
|
| 1508 |
|
| 1509 |
+
**Final comparison**
|
| 1510 |
+
- Main AI state: {pretty_state(tb_state)}
|
| 1511 |
+
- Second AI state: {pretty_state(radio_state)}
|
| 1512 |
- Consensus label: **{html_escape(consensus_label)}**
|
| 1513 |
+
- Technical detail: {html_escape(consensus_detail)}
|
| 1514 |
|
| 1515 |
+
**Image / safety warnings**
|
| 1516 |
{warn_txt}
|
| 1517 |
|
| 1518 |
+
**Second AI / RADIO full output**
|
| 1519 |
+
- Short result: {html_escape(radio_result_short)}
|
| 1520 |
+
- Primary score: {radio_primary_line}
|
| 1521 |
+
- Raw score: {radio_raw_line}
|
| 1522 |
+
- Masked score: {radio_masked_line}
|
| 1523 |
+
- Masked ran: {radio_masked_ran}
|
| 1524 |
+
- Band: {radio_band}
|
| 1525 |
+
|
| 1526 |
{radio_text_long}
|
| 1527 |
|
| 1528 |
</details>
|
|
|
|
| 1546 |
.card {border:1px solid rgba(255,255,255,0.12); border-radius:14px; padding:14px; margin:10px 0;}
|
| 1547 |
"""
|
| 1548 |
|
| 1549 |
+
with gr.Blocks(title="Chest X-ray TB Screening Assistant") as demo:
|
| 1550 |
|
| 1551 |
+
# Welcome screen
|
|
|
|
|
|
|
| 1552 |
with gr.Column(visible=True) as welcome_screen:
|
| 1553 |
+
gr.Markdown('<div class="title">Welcome β Chest X-ray TB Screening Assistant</div>')
|
| 1554 |
gr.HTML(WELCOME_HTML)
|
| 1555 |
+
continue_btn = gr.Button("Start checking X-ray images β", variant="primary")
|
| 1556 |
|
| 1557 |
+
# Main app UI
|
|
|
|
|
|
|
| 1558 |
with gr.Column(visible=False) as main_app:
|
| 1559 |
+
gr.Markdown('<div class="title">Upload chest X-ray images</div>')
|
| 1560 |
+
|
| 1561 |
gr.Markdown(
|
| 1562 |
+
"<div class='subtitle'>"
|
| 1563 |
+
"Upload one or more chest X-ray images. The app will check the image, show a simple AI screening result, "
|
| 1564 |
+
"and suggest what to do next. This is not a diagnosis."
|
| 1565 |
+
"</div>"
|
| 1566 |
)
|
| 1567 |
|
| 1568 |
gr.Markdown(
|
| 1569 |
+
"<div class='warnbox'><b>Important:</b> This app supports medical review only. "
|
| 1570 |
+
"If TB is clinically suspected, please seek clinician/radiologist review and consider CBNAAT/GeneXpert, "
|
| 1571 |
+
"sputum testing, and/or CT chest regardless of the AI result.</div>"
|
| 1572 |
)
|
| 1573 |
|
| 1574 |
with gr.Row():
|
| 1575 |
+
with gr.Column(scale=2):
|
| 1576 |
+
gr.Markdown("### 1) Upload X-ray image(s)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1577 |
|
| 1578 |
+
files = gr.Files(
|
| 1579 |
+
label="Upload chest X-ray images",
|
| 1580 |
+
file_types=[".png", ".jpg", ".jpeg", ".bmp"]
|
| 1581 |
)
|
| 1582 |
|
| 1583 |
phone_mode = gr.Checkbox(
|
| 1584 |
value=False,
|
| 1585 |
+
label="This image is a phone photo, WhatsApp image, screenshot, or has borders"
|
| 1586 |
)
|
| 1587 |
+
|
| 1588 |
gr.Markdown(
|
| 1589 |
+
"<div class='subtitle'>"
|
| 1590 |
+
"Turn this on for images taken from a phone camera, WhatsApp, screenshots, cropped images, "
|
| 1591 |
+
"or images with black/white borders."
|
| 1592 |
+
"</div>"
|
| 1593 |
)
|
| 1594 |
|
| 1595 |
+
use_radio = gr.Checkbox(
|
| 1596 |
+
value=True,
|
| 1597 |
+
label="Use a second AI check for comparison"
|
|
|
|
| 1598 |
)
|
| 1599 |
|
| 1600 |
+
run_btn = gr.Button("Check X-ray image(s)", variant="primary")
|
| 1601 |
+
status = gr.Textbox(label="Status", value="Ready to check images.", interactive=False)
|
|
|
|
|
|
|
| 1602 |
|
| 1603 |
+
with gr.Column(scale=1):
|
| 1604 |
+
gr.Markdown("### 2) What you will get")
|
| 1605 |
+
|
| 1606 |
+
gr.Markdown("""
|
| 1607 |
+
<div class='legend'>
|
| 1608 |
+
<b>The report will show:</b><br/><br/>
|
| 1609 |
+
β
A simple result: Low / Unclear / Needs review / TB-like pattern seen<br/>
|
| 1610 |
+
β
Image reliability: whether the image was clear enough<br/>
|
| 1611 |
+
β
Lung area check: whether the app found the lung region<br/>
|
| 1612 |
+
β
Heatmap images: where the AI focused<br/>
|
| 1613 |
+
β
Suggested next step
|
| 1614 |
+
</div>
|
| 1615 |
+
""")
|
| 1616 |
+
|
| 1617 |
+
with gr.Accordion("Advanced settings β usually leave unchanged", open=False):
|
| 1618 |
+
tb_weights = gr.Textbox(label="Main AI model weights", value=DEFAULT_TB_WEIGHTS)
|
| 1619 |
+
lung_weights = gr.Textbox(label="Lung area model weights", value=DEFAULT_LUNG_WEIGHTS)
|
| 1620 |
+
|
| 1621 |
+
backbone = gr.Dropdown(
|
| 1622 |
+
choices=["efficientnet_b0"],
|
| 1623 |
+
value="efficientnet_b0",
|
| 1624 |
+
label="Main AI backbone"
|
| 1625 |
+
)
|
| 1626 |
+
|
| 1627 |
+
threshold = gr.Slider(
|
| 1628 |
+
0.01, 0.99, value=TBNET_SCREEN_THR, step=0.01,
|
| 1629 |
+
label=f"Screening threshold = {TBNET_SCREEN_THR:.2f}"
|
| 1630 |
+
)
|
| 1631 |
+
|
| 1632 |
+
radio_gate = gr.Slider(
|
| 1633 |
+
0.10, 0.40, value=RADIO_GATE_DEFAULT, step=0.01,
|
| 1634 |
+
label="Second AI lung-mask gate"
|
| 1635 |
+
)
|
| 1636 |
+
|
| 1637 |
+
gr.Markdown(
|
| 1638 |
+
f"<div class='subtitle'>Device: <b>{DEVICE}</b> | FORCE_CPU={FORCE_CPU}</div>"
|
| 1639 |
+
)
|
| 1640 |
|
| 1641 |
back_btn = gr.Button("β Back to Welcome", variant="secondary")
|
| 1642 |
|
| 1643 |
+
gr.Markdown("### Results")
|
| 1644 |
+
summary = gr.HTML(
|
| 1645 |
+
"<div style='padding:14px;border-radius:14px;background:rgba(255,255,255,0.04);"
|
| 1646 |
+
"border:1px solid rgba(255,255,255,0.10);'>"
|
| 1647 |
+
"Upload one or more images, then click <b>Check X-ray image(s)</b>."
|
| 1648 |
+
"</div>"
|
| 1649 |
+
)
|
|
|
|
| 1650 |
|
| 1651 |
+
gr.Markdown("### Image views")
|
| 1652 |
+
gr.Markdown("""
|
| 1653 |
+
<div class='legend'>
|
| 1654 |
+
<b>How to read the image views:</b><br/>
|
| 1655 |
+
<b>Original</b> = your uploaded image<br/>
|
| 1656 |
+
<b>Image checked by the app</b> = image after basic cleanup if selected<br/>
|
| 1657 |
+
<b>Lung area found by the app</b> = area the app thinks is lung<br/>
|
| 1658 |
+
<b>AI heatmap</b> = area the AI focused on most
|
| 1659 |
+
</div>
|
| 1660 |
""")
|
| 1661 |
|
| 1662 |
+
gallery = gr.Gallery(label="Image views", columns=3, height=560)
|
|
|
|
|
|
|
| 1663 |
|
| 1664 |
with gr.Row():
|
| 1665 |
with gr.Column(scale=1):
|
| 1666 |
disclaimer_box = gr.Markdown(CLINICAL_DISCLAIMER)
|
| 1667 |
with gr.Column(scale=2):
|
| 1668 |
+
gr.Markdown("### Technical details")
|
| 1669 |
+
gr.Markdown(
|
| 1670 |
+
"This section is mainly for clinicians, researchers, or developers."
|
| 1671 |
+
)
|
| 1672 |
details = gr.Markdown("")
|
| 1673 |
|
| 1674 |
run_btn.click(
|
|
|
|
| 1686 |
outputs=[summary, gallery, details, disclaimer_box, status]
|
| 1687 |
)
|
| 1688 |
|
|
|
|
| 1689 |
# Transitions
|
|
|
|
| 1690 |
continue_btn.click(
|
| 1691 |
fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 1692 |
inputs=[],
|
|
|
|
| 1708 |
server_name="0.0.0.0",
|
| 1709 |
server_port=int(os.environ.get("PORT", 7860)),
|
| 1710 |
show_error=True,
|
| 1711 |
+
ssr_mode=False,
|
| 1712 |
css="""
|
| 1713 |
.title {font-size: 28px; font-weight: 900; margin-bottom: 6px;}
|
| 1714 |
.subtitle {font-size: 14px; opacity: 0.88; margin-bottom: 14px;}
|