Add integrated DFU dashboard + Fitzpatrick adjustment (English UI)
Browse files
app.py
CHANGED
|
@@ -138,9 +138,9 @@ def apply_foot_guide(frame):
|
|
| 138 |
result = blended[:, :, :3].astype(np.uint8)
|
| 139 |
|
| 140 |
# Add instruction text at top
|
| 141 |
-
cv2.putText(result, "
|
| 142 |
(w // 2 - 200, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 220, 120), 2, cv2.LINE_AA)
|
| 143 |
-
cv2.putText(result, "
|
| 144 |
(w // 2 - 230, h - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1, cv2.LINE_AA)
|
| 145 |
|
| 146 |
return result
|
|
@@ -157,7 +157,7 @@ def generate_static_guide():
|
|
| 157 |
font_small = _get_font_regular(14)
|
| 158 |
|
| 159 |
# Title
|
| 160 |
-
draw.text((W // 2 - 130, 15), "
|
| 161 |
|
| 162 |
# Draw foot silhouette (simplified)
|
| 163 |
foot_guide_rgba = generate_foot_guide(400, 350)
|
|
@@ -176,13 +176,13 @@ def generate_static_guide():
|
|
| 176 |
# Instructions
|
| 177 |
y = 425
|
| 178 |
instructions = [
|
| 179 |
-
("1.", "
|
| 180 |
-
("2.", "
|
| 181 |
-
("3.", "
|
| 182 |
-
("4.", "
|
| 183 |
-
("5.", "
|
| 184 |
-
("6.", "
|
| 185 |
-
("7.", "
|
| 186 |
]
|
| 187 |
for num, text in instructions:
|
| 188 |
draw.text((30, y), num, fill=(5, 150, 105), font=font_title)
|
|
@@ -192,10 +192,10 @@ def generate_static_guide():
|
|
| 192 |
# Bottom note
|
| 193 |
draw.line([(30, y + 5), (W - 30, y + 5)], fill=(229, 231, 235), width=1)
|
| 194 |
draw.text((30, y + 12),
|
| 195 |
-
"Tip:
|
| 196 |
fill=(107, 114, 128), font=font_small)
|
| 197 |
draw.text((30, y + 32),
|
| 198 |
-
"
|
| 199 |
fill=(107, 114, 128), font=font_small)
|
| 200 |
|
| 201 |
return np.array(img)
|
|
@@ -263,7 +263,7 @@ class DFUReport(FPDF):
|
|
| 263 |
self._font("B", 14)
|
| 264 |
self.set_text_color(255, 255, 255)
|
| 265 |
self.set_xy(10, 4)
|
| 266 |
-
self.cell(0, 8, "WoundNetB7 -
|
| 267 |
self._font("", 8)
|
| 268 |
self.set_text_color(156, 163, 175)
|
| 269 |
self.set_xy(10, 13)
|
|
@@ -277,9 +277,9 @@ class DFUReport(FPDF):
|
|
| 277 |
self.set_y(-12)
|
| 278 |
self._font("", 7)
|
| 279 |
self.set_text_color(156, 163, 175)
|
| 280 |
-
self.cell(0, 5, "WoundNetB7 |
|
| 281 |
-
"Dice 0.927 (
|
| 282 |
-
self.cell(0, 5, f"
|
| 283 |
|
| 284 |
def section_title(self, number, title):
|
| 285 |
self._font("B", 11)
|
|
@@ -325,8 +325,8 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 325 |
pdf.add_page()
|
| 326 |
|
| 327 |
# ββ Section 1: Images ββ
|
| 328 |
-
pdf.section_title(1, "
|
| 329 |
-
pdf.add_image_pair(orig_path, "
|
| 330 |
pdf.ln(2)
|
| 331 |
|
| 332 |
# Multiclass + legend
|
|
@@ -334,9 +334,9 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 334 |
pdf.set_text_color(107, 114, 128)
|
| 335 |
x_start = pdf.get_x()
|
| 336 |
y_start = pdf.get_y()
|
| 337 |
-
pdf.cell(90, 4, "
|
| 338 |
pdf.cell(5, 4, "", 0, 0)
|
| 339 |
-
pdf.cell(90, 4, "
|
| 340 |
|
| 341 |
pdf.image(multi_path, x=x_start, y=pdf.get_y(), w=90, h=60)
|
| 342 |
|
|
@@ -345,10 +345,10 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 345 |
legend_y = pdf.get_y() + 5
|
| 346 |
|
| 347 |
class_info = [
|
| 348 |
-
("
|
| 349 |
-
("
|
| 350 |
-
("
|
| 351 |
-
("
|
| 352 |
]
|
| 353 |
|
| 354 |
for cls_name, pct, (r, g, b) in class_info:
|
|
@@ -377,12 +377,12 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 377 |
h_img, w_img = result.image_size
|
| 378 |
pdf._font("", 8)
|
| 379 |
pdf.set_text_color(107, 114, 128)
|
| 380 |
-
pdf.cell(0, 4, f"
|
| 381 |
-
f"
|
| 382 |
pdf.ln(4)
|
| 383 |
|
| 384 |
# ββ Section 2: Fitzpatrick ββ
|
| 385 |
-
pdf.section_title(2, "
|
| 386 |
fitz = result.fitzpatrick
|
| 387 |
if fitz and fitz.confidence > 0:
|
| 388 |
ftype = fitz.fitzpatrick_type
|
|
@@ -400,7 +400,7 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 400 |
y_warn = pdf.get_y()
|
| 401 |
pdf.rect(pdf.get_x(), y_warn, 185, 10, "DF")
|
| 402 |
pdf.set_xy(pdf.get_x() + 2, y_warn + 1)
|
| 403 |
-
pdf.cell(0, 4, "
|
| 404 |
pdf._font("", 7)
|
| 405 |
pdf.set_text_color(153, 27, 27)
|
| 406 |
pdf.cell(0, 3, lighting_warning, 0, 1)
|
|
@@ -413,7 +413,7 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 413 |
y_warn = pdf.get_y()
|
| 414 |
pdf.rect(pdf.get_x(), y_warn, 185, 10, "DF")
|
| 415 |
pdf.set_xy(pdf.get_x() + 2, y_warn + 1)
|
| 416 |
-
pdf.cell(0, 4, "
|
| 417 |
pdf._font("", 7)
|
| 418 |
pdf.set_text_color(146, 64, 14)
|
| 419 |
pdf.cell(0, 3, lighting_warning, 0, 1)
|
|
@@ -428,7 +428,7 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 428 |
pdf._font("B", 16)
|
| 429 |
pdf.set_text_color(*fg)
|
| 430 |
pdf.set_xy(x_badge, y_badge + 2)
|
| 431 |
-
pdf.cell(35, 9, f"
|
| 432 |
pdf._font("", 8)
|
| 433 |
pdf.set_xy(x_badge, y_badge + 12)
|
| 434 |
pdf.cell(35, 6, fitz.fitzpatrick_label, 0, 0, "C")
|
|
@@ -440,12 +440,12 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 440 |
det_y = y_badge
|
| 441 |
l_scene = getattr(fitz, "l_scene_mean", 0)
|
| 442 |
details = [
|
| 443 |
-
("ITA", f"{fitz.ita_angle:.1f} +/- {fitz.ita_std:.1f}
|
| 444 |
-
("L*
|
| 445 |
-
("L*
|
| 446 |
-
("b*
|
| 447 |
-
("
|
| 448 |
-
("
|
| 449 |
]
|
| 450 |
for label, value in details:
|
| 451 |
pdf.set_xy(det_x, det_y)
|
|
@@ -459,11 +459,11 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 459 |
else:
|
| 460 |
pdf._font("", 9)
|
| 461 |
pdf.set_text_color(107, 114, 128)
|
| 462 |
-
pdf.cell(0, 5, "
|
| 463 |
pdf.ln(4)
|
| 464 |
|
| 465 |
# ββ Section 3: PWAT ββ
|
| 466 |
-
pdf.section_title(3, "PWAT
|
| 467 |
pwat = result.pwat
|
| 468 |
if pwat and pwat.scores_raw:
|
| 469 |
ftype_str = pwat.fitzpatrick_type or "III"
|
|
@@ -473,7 +473,7 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 473 |
pdf._font("B", 9)
|
| 474 |
pdf.set_text_color(55, 65, 81)
|
| 475 |
col_widths = [55, 25, 25, 25, 20, 35]
|
| 476 |
-
headers = ["
|
| 477 |
for w, h in zip(col_widths, headers):
|
| 478 |
pdf.cell(w, 6, h, 1, 0, "C", fill=True)
|
| 479 |
pdf.ln()
|
|
@@ -512,7 +512,7 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 512 |
|
| 513 |
# Severity label
|
| 514 |
pdf._font("", 7)
|
| 515 |
-
sev_labels = {0: "Normal", 1: "
|
| 516 |
pdf.set_text_color(107, 114, 128)
|
| 517 |
pdf.cell(col_widths[5], 6, sev_labels.get(raw, ""), "RB", 0, "L")
|
| 518 |
pdf.ln()
|
|
@@ -533,21 +533,21 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 533 |
# Score interpretation
|
| 534 |
pdf._font("", 8)
|
| 535 |
pdf.set_text_color(107, 114, 128)
|
| 536 |
-
pdf.cell(0, 4, "
|
| 537 |
-
pdf.cell(0, 4, f"
|
| 538 |
-
"(
|
| 539 |
|
| 540 |
# Interpretation ranges
|
| 541 |
pdf.ln(2)
|
| 542 |
pdf._font("B", 8)
|
| 543 |
pdf.set_text_color(55, 65, 81)
|
| 544 |
-
pdf.cell(0, 4, "
|
| 545 |
pdf._font("", 8)
|
| 546 |
ranges = [
|
| 547 |
-
("0-6:",
|
| 548 |
-
("7-12:",
|
| 549 |
-
("13-18:", "
|
| 550 |
-
("19-24:", "
|
| 551 |
]
|
| 552 |
for label, desc, (r, g, b) in ranges:
|
| 553 |
pdf.set_fill_color(r, g, b)
|
|
@@ -563,10 +563,10 @@ def generate_pdf_report(image_rgb, binary_overlay, multiclass_overlay, result):
|
|
| 563 |
else:
|
| 564 |
pdf._font("", 9)
|
| 565 |
pdf.set_text_color(107, 114, 128)
|
| 566 |
-
pdf.cell(0, 5, "
|
| 567 |
|
| 568 |
# Save PDF
|
| 569 |
-
pdf_path = os.path.join(tmpdir, "
|
| 570 |
pdf.output(pdf_path)
|
| 571 |
|
| 572 |
# Cleanup temp images
|
|
@@ -596,7 +596,7 @@ def generate_report_files(image_rgb, binary_overlay, multiclass_overlay, result)
|
|
| 596 |
"tta_folds": 6,
|
| 597 |
"debiasing": "Fitzpatrick-calibrated ITA (86.9% accuracy, r=0.975)",
|
| 598 |
}
|
| 599 |
-
json_path = os.path.join(tmpdir, "
|
| 600 |
with open(json_path, "w", encoding="utf-8") as f:
|
| 601 |
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
| 602 |
|
|
@@ -613,17 +613,19 @@ def analyze_image(image):
|
|
| 613 |
if image is None:
|
| 614 |
empty = np.zeros((100, 100, 3), dtype=np.uint8)
|
| 615 |
_last_analysis.clear()
|
| 616 |
-
return empty, empty, "", "", "", "{}"
|
| 617 |
|
| 618 |
img_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
| 619 |
result = pipe.analyze(img_bgr, use_tta=True)
|
| 620 |
|
| 621 |
binary_overlay = pipe.visualize_binary(img_bgr, result)
|
| 622 |
multiclass_overlay = pipe.visualize_multiclass(img_bgr, result)
|
|
|
|
| 623 |
|
| 624 |
_last_analysis["image_rgb"] = image
|
| 625 |
_last_analysis["binary"] = binary_overlay
|
| 626 |
_last_analysis["multiclass"] = multiclass_overlay
|
|
|
|
| 627 |
_last_analysis["result"] = result
|
| 628 |
|
| 629 |
seg_stats = build_seg_stats_html(result)
|
|
@@ -631,7 +633,7 @@ def analyze_image(image):
|
|
| 631 |
pwat_html = build_pwat_html(result.pwat)
|
| 632 |
json_out = json.dumps(result.to_dict(), indent=2, ensure_ascii=False)
|
| 633 |
|
| 634 |
-
return binary_overlay, multiclass_overlay, seg_stats, fitz_html, pwat_html, json_out
|
| 635 |
|
| 636 |
|
| 637 |
def analyze_from_camera(image):
|
|
@@ -654,7 +656,7 @@ def download_report():
|
|
| 654 |
|
| 655 |
def build_fitz_html(fitz):
|
| 656 |
if fitz is None or fitz.confidence == 0:
|
| 657 |
-
return "<p style='color:#6b7280;'>
|
| 658 |
bg = FITZ_COLORS.get(fitz.fitzpatrick_type, "#e5e7eb")
|
| 659 |
fg = FITZ_TEXT_COLORS.get(fitz.fitzpatrick_type, "#1f2937")
|
| 660 |
|
|
@@ -668,17 +670,17 @@ def build_fitz_html(fitz):
|
|
| 668 |
warning_html = f"""
|
| 669 |
<div style="background:#fef2f2; border:1px solid #fca5a5; border-radius:8px;
|
| 670 |
padding:12px 16px; margin-bottom:12px; font-size:0.9em;">
|
| 671 |
-
<span style="color:#dc2626; font-weight:700;">⚠
|
| 672 |
<span style="color:#991b1b;">{lighting_warning}</span><br>
|
| 673 |
-
<span style="color:#6b7280; font-size:0.85em;">L*
|
| 674 |
</div>"""
|
| 675 |
elif lighting_quality == "low":
|
| 676 |
warning_html = f"""
|
| 677 |
<div style="background:#fffbeb; border:1px solid #fcd34d; border-radius:8px;
|
| 678 |
padding:12px 16px; margin-bottom:12px; font-size:0.9em;">
|
| 679 |
-
<span style="color:#d97706; font-weight:700;">⚠
|
| 680 |
<span style="color:#92400e;">{lighting_warning}</span><br>
|
| 681 |
-
<span style="color:#6b7280; font-size:0.85em;">L*
|
| 682 |
</div>"""
|
| 683 |
|
| 684 |
return f"""
|
|
@@ -687,22 +689,22 @@ def build_fitz_html(fitz):
|
|
| 687 |
<div style="background:{bg}; color:{fg}; border-radius:12px; padding:18px 28px;
|
| 688 |
font-size:1.5em; font-weight:700; min-width:120px; text-align:center;
|
| 689 |
border:2px solid rgba(0,0,0,0.1);">
|
| 690 |
-
|
| 691 |
<span style="font-size:0.55em; font-weight:400;">{fitz.fitzpatrick_label}</span>
|
| 692 |
</div>
|
| 693 |
<div style="font-size:0.95em; line-height:1.8;">
|
| 694 |
<b>ITA:</b> {fitz.ita_angle:.1f}° ± {fitz.ita_std:.1f}°<br>
|
| 695 |
-
<b>L*
|
| 696 |
-
<b>L*
|
| 697 |
-
<b>
|
| 698 |
-
<b>
|
| 699 |
</div>
|
| 700 |
</div>"""
|
| 701 |
|
| 702 |
|
| 703 |
def build_pwat_html(pwat):
|
| 704 |
if pwat is None or not pwat.scores_raw:
|
| 705 |
-
return "<p style='color:#6b7280;'>
|
| 706 |
rows = ""
|
| 707 |
for item in [3, 4, 5, 6, 7, 8]:
|
| 708 |
name = ITEM_NAMES.get(item, f"Item {item}")
|
|
@@ -741,9 +743,9 @@ def build_pwat_html(pwat):
|
|
| 741 |
<table style="width:100%; border-collapse:collapse; font-size:0.92em;">
|
| 742 |
<thead>
|
| 743 |
<tr style="border-bottom:2px solid #d1d5db;">
|
| 744 |
-
<th style="padding:10px 12px; text-align:left;">
|
| 745 |
-
<th style="padding:10px 12px; text-align:center;">
|
| 746 |
-
<th style="padding:10px 12px; text-align:center;">
|
| 747 |
<th style="padding:10px 12px; text-align:center;">Δ</th>
|
| 748 |
</tr>
|
| 749 |
</thead>
|
|
@@ -757,10 +759,10 @@ def build_pwat_html(pwat):
|
|
| 757 |
</tbody>
|
| 758 |
</table>
|
| 759 |
<p style="font-size:0.82em; color:#6b7280; margin-top:8px;">
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
Items: 3=
|
| 763 |
-
6=
|
| 764 |
</p>"""
|
| 765 |
|
| 766 |
|
|
@@ -771,7 +773,7 @@ def build_seg_stats_html(result):
|
|
| 771 |
for cls_name in ["foot", "perilesion", "ulcer"]:
|
| 772 |
pct = dist.get(cls_name, 0)
|
| 773 |
color = colors.get(cls_name, "#6b7280")
|
| 774 |
-
label = {"foot": "
|
| 775 |
bars += f"""
|
| 776 |
<div style="margin-bottom:6px;">
|
| 777 |
<div style="display:flex; justify-content:space-between; font-size:0.9em; margin-bottom:2px;">
|
|
@@ -785,7 +787,7 @@ def build_seg_stats_html(result):
|
|
| 785 |
return f"""
|
| 786 |
<div style="padding:4px 0;">
|
| 787 |
<p style="font-size:0.85em; color:#6b7280; margin-bottom:10px;">
|
| 788 |
-
|
| 789 |
</p>
|
| 790 |
{bars}
|
| 791 |
</div>"""
|
|
@@ -822,99 +824,110 @@ with gr.Blocks(
|
|
| 822 |
|
| 823 |
with gr.Tabs():
|
| 824 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 825 |
-
# TAB 1:
|
| 826 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 827 |
-
with gr.Tab("
|
| 828 |
with gr.Row():
|
| 829 |
with gr.Column(scale=1):
|
| 830 |
-
input_image = gr.Image(label="
|
| 831 |
sources=["upload", "clipboard"])
|
| 832 |
-
analyze_btn = gr.Button("
|
| 833 |
gr.HTML("""
|
| 834 |
<div style="font-size:0.82em; color:#6b7280; margin-top:8px; line-height:1.6;">
|
| 835 |
-
<b>Pipeline:</b>
|
| 836 |
-
<b>
|
| 837 |
-
|
| 838 |
-
<b>TTA:</b> 6
|
| 839 |
</div>
|
| 840 |
""")
|
| 841 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
# Step 1
|
| 843 |
gr.HTML("""<div class="step-header"><div class="step-number">1</div>
|
| 844 |
-
<div class="step-title">
|
| 845 |
with gr.Row():
|
| 846 |
with gr.Column(scale=1):
|
| 847 |
-
output_binary = gr.Image(label="
|
| 848 |
with gr.Column(scale=1):
|
| 849 |
-
output_seg_stats = gr.HTML(label="
|
| 850 |
|
| 851 |
# Step 2
|
| 852 |
gr.HTML("""<div class="step-header" style="margin-top:12px;"><div class="step-number">2</div>
|
| 853 |
-
<div class="step-title">
|
| 854 |
with gr.Row():
|
| 855 |
with gr.Column(scale=1):
|
| 856 |
-
output_multiclass = gr.Image(label="
|
| 857 |
with gr.Column(scale=1):
|
| 858 |
gr.HTML("""
|
| 859 |
<div style="padding:12px;">
|
| 860 |
-
<p style="font-weight:600; margin-bottom:10px;">
|
| 861 |
<div style="display:flex; flex-direction:column; gap:8px;">
|
| 862 |
<div style="display:flex; align-items:center; gap:8px;">
|
| 863 |
<div style="width:20px; height:20px; background:#22c55e; border-radius:4px;"></div>
|
| 864 |
-
<span><b>
|
| 865 |
</div>
|
| 866 |
<div style="display:flex; align-items:center; gap:8px;">
|
| 867 |
<div style="width:20px; height:20px; background:#f97316; border-radius:4px;"></div>
|
| 868 |
-
<span><b>
|
| 869 |
</div>
|
| 870 |
<div style="display:flex; align-items:center; gap:8px;">
|
| 871 |
<div style="width:20px; height:20px; background:#ef4444; border-radius:4px;"></div>
|
| 872 |
-
<span><b>
|
| 873 |
</div>
|
| 874 |
</div>
|
| 875 |
</div>""")
|
| 876 |
|
| 877 |
# Step 3
|
| 878 |
gr.HTML("""<div class="step-header" style="margin-top:12px;"><div class="step-number">3</div>
|
| 879 |
-
<div class="step-title">
|
| 880 |
output_fitz = gr.HTML()
|
| 881 |
|
| 882 |
# Step 4
|
| 883 |
gr.HTML("""<div class="step-header" style="margin-top:12px;"><div class="step-number">4</div>
|
| 884 |
-
<div class="step-title">PWAT β
|
| 885 |
output_pwat = gr.HTML()
|
| 886 |
|
| 887 |
# Download
|
| 888 |
gr.HTML("""<div class="step-header" style="margin-top:16px;">
|
| 889 |
<div class="step-number" style="background:#059669;">⇩</div>
|
| 890 |
-
<div class="step-title">
|
| 891 |
<p style="font-size:0.88em; color:#6b7280; margin-bottom:8px;">
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
download_btn = gr.Button("
|
| 895 |
-
output_files = gr.File(label="
|
| 896 |
|
| 897 |
-
with gr.Accordion("JSON
|
| 898 |
output_json = gr.Code(label="JSON Output", language="json")
|
| 899 |
|
| 900 |
analyze_btn.click(
|
| 901 |
fn=analyze_image,
|
| 902 |
inputs=[input_image],
|
| 903 |
-
outputs=[output_binary, output_multiclass, output_seg_stats,
|
| 904 |
output_fitz, output_pwat, output_json],
|
| 905 |
)
|
| 906 |
download_btn.click(fn=download_report, inputs=[], outputs=[output_files])
|
| 907 |
|
| 908 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 909 |
-
# TAB 2:
|
| 910 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 911 |
-
with gr.Tab("
|
| 912 |
gr.HTML("""
|
| 913 |
<div style="padding:16px 0;">
|
| 914 |
-
<h2 style="font-size:1.4em; margin:0 0 8px;">
|
| 915 |
<p style="color:#6b7280; line-height:1.6;">
|
| 916 |
-
Use
|
| 917 |
-
|
| 918 |
</p>
|
| 919 |
</div>
|
| 920 |
""")
|
|
@@ -922,15 +935,15 @@ with gr.Blocks(
|
|
| 922 |
with gr.Row():
|
| 923 |
with gr.Column(scale=3):
|
| 924 |
camera_input = gr.Image(
|
| 925 |
-
label="
|
| 926 |
type="numpy",
|
| 927 |
sources=["webcam"],
|
| 928 |
)
|
| 929 |
-
camera_analyze_btn = gr.Button("
|
| 930 |
|
| 931 |
with gr.Column(scale=2):
|
| 932 |
guide_image = gr.Image(
|
| 933 |
-
label="
|
| 934 |
value=generate_static_guide(),
|
| 935 |
interactive=False,
|
| 936 |
)
|
|
@@ -939,28 +952,35 @@ with gr.Blocks(
|
|
| 939 |
<div style="background:#f0fdf4; border:1px solid #bbf7d0; border-radius:10px;
|
| 940 |
padding:16px; margin:12px 0;">
|
| 941 |
<p style="font-weight:700; color:#166534; margin:0 0 8px;">
|
| 942 |
-
|
| 943 |
</p>
|
| 944 |
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px 24px; font-size:0.92em; color:#15803d;">
|
| 945 |
-
<div>1.
|
| 946 |
-
<div>2.
|
| 947 |
-
<div>3.
|
| 948 |
-
<div>4.
|
| 949 |
-
<div>5.
|
| 950 |
-
<div>6.
|
| 951 |
-
<div>7.
|
| 952 |
-
<div>8.
|
| 953 |
</div>
|
| 954 |
</div>
|
| 955 |
""")
|
| 956 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
# Results from camera capture
|
| 958 |
gr.HTML("""<div class="step-header" style="margin-top:16px;">
|
| 959 |
<div class="step-number">1</div>
|
| 960 |
-
<div class="step-title">
|
| 961 |
with gr.Row():
|
| 962 |
-
cam_binary = gr.Image(label="
|
| 963 |
-
cam_multiclass = gr.Image(label="
|
| 964 |
|
| 965 |
with gr.Row():
|
| 966 |
cam_seg_stats = gr.HTML()
|
|
@@ -970,16 +990,16 @@ with gr.Blocks(
|
|
| 970 |
cam_fitz = gr.HTML()
|
| 971 |
cam_pwat = gr.HTML()
|
| 972 |
|
| 973 |
-
cam_download_btn = gr.Button("
|
| 974 |
-
cam_files = gr.File(label="
|
| 975 |
|
| 976 |
-
with gr.Accordion("
|
| 977 |
cam_json = gr.Code(label="JSON Output", language="json")
|
| 978 |
|
| 979 |
camera_analyze_btn.click(
|
| 980 |
fn=analyze_from_camera,
|
| 981 |
inputs=[camera_input],
|
| 982 |
-
outputs=[cam_binary, cam_multiclass, cam_seg_stats,
|
| 983 |
cam_fitz, cam_pwat, cam_json],
|
| 984 |
)
|
| 985 |
cam_download_btn.click(fn=download_report, inputs=[], outputs=[cam_files])
|
|
@@ -987,8 +1007,8 @@ with gr.Blocks(
|
|
| 987 |
gr.HTML("""
|
| 988 |
<div style="text-align:center; padding:16px 0; font-size:0.82em; color:#9ca3af;
|
| 989 |
border-top:1px solid #e5e7eb; margin-top:20px;">
|
| 990 |
-
WoundNetB7 •
|
| 991 |
-
Ulcer Dice 0.927 (
|
| 992 |
Debiasing: 46.6% max group gap reduction (p < 10<sup>-55</sup>)
|
| 993 |
</div>
|
| 994 |
""")
|
|
|
|
| 138 |
result = blended[:, :, :3].astype(np.uint8)
|
| 139 |
|
| 140 |
# Add instruction text at top
|
| 141 |
+
cv2.putText(result, "Position the foot inside the guide",
|
| 142 |
(w // 2 - 200, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 220, 120), 2, cv2.LINE_AA)
|
| 143 |
+
cv2.putText(result, "Distance: 30-40 cm | Uniform lighting",
|
| 144 |
(w // 2 - 230, h - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (200, 200, 200), 1, cv2.LINE_AA)
|
| 145 |
|
| 146 |
return result
|
|
|
|
| 157 |
font_small = _get_font_regular(14)
|
| 158 |
|
| 159 |
# Title
|
| 160 |
+
draw.text((W // 2 - 130, 15), "DFU Capture Guide", fill=(31, 41, 55), font=font_title)
|
| 161 |
|
| 162 |
# Draw foot silhouette (simplified)
|
| 163 |
foot_guide_rgba = generate_foot_guide(400, 350)
|
|
|
|
| 176 |
# Instructions
|
| 177 |
y = 425
|
| 178 |
instructions = [
|
| 179 |
+
("1.", "Plantar view of the foot facing the camera"),
|
| 180 |
+
("2.", "Distance: 30-40 cm from the lens"),
|
| 181 |
+
("3.", "Uniform lighting, no harsh shadows"),
|
| 182 |
+
("4.", "Neutral background (white or blue sheet)"),
|
| 183 |
+
("5.", "Include the full ulcer + 3-5 cm of healthy skin"),
|
| 184 |
+
("6.", "Avoid direct flash (causes glare)"),
|
| 185 |
+
("7.", "Center the foot inside the green silhouette"),
|
| 186 |
]
|
| 187 |
for num, text in instructions:
|
| 188 |
draw.text((30, y), num, fill=(5, 150, 105), font=font_title)
|
|
|
|
| 192 |
# Bottom note
|
| 193 |
draw.line([(30, y + 5), (W - 30, y + 5)], fill=(229, 231, 235), width=1)
|
| 194 |
draw.text((30, y + 12),
|
| 195 |
+
"Tip: For best results capture with diffuse natural",
|
| 196 |
fill=(107, 114, 128), font=font_small)
|
| 197 |
draw.text((30, y + 32),
|
| 198 |
+
"light. Avoid overhead lights that create shadows.",
|
| 199 |
fill=(107, 114, 128), font=font_small)
|
| 200 |
|
| 201 |
return np.array(img)
|
|
|
|
| 263 |
self._font("B", 14)
|
| 264 |
self.set_text_color(255, 255, 255)
|
| 265 |
self.set_xy(10, 4)
|
| 266 |
+
self.cell(0, 8, "WoundNetB7 - Integrated DFU Assessment Report", 0, 0, "L")
|
| 267 |
self._font("", 8)
|
| 268 |
self.set_text_color(156, 163, 175)
|
| 269 |
self.set_xy(10, 13)
|
|
|
|
| 277 |
self.set_y(-12)
|
| 278 |
self._font("", 7)
|
| 279 |
self.set_text_color(156, 163, 175)
|
| 280 |
+
self.cell(0, 5, "WoundNetB7 | Doctoral Thesis | Marcelo Marquez-Murillo | "
|
| 281 |
+
"Dice 0.927 (95% CI: [0.917, 0.936]) | Debiasing: 46.6% gap reduction (p < 1e-55)", 0, 0, "C")
|
| 282 |
+
self.cell(0, 5, f"Page {self.page_no()}/{{nb}}", 0, 0, "R")
|
| 283 |
|
| 284 |
def section_title(self, number, title):
|
| 285 |
self._font("B", 11)
|
|
|
|
| 325 |
pdf.add_page()
|
| 326 |
|
| 327 |
# ββ Section 1: Images ββ
|
| 328 |
+
pdf.section_title(1, "Segmentation")
|
| 329 |
+
pdf.add_image_pair(orig_path, "Original Image", binary_path, "Binary Ulcer Segmentation")
|
| 330 |
pdf.ln(2)
|
| 331 |
|
| 332 |
# Multiclass + legend
|
|
|
|
| 334 |
pdf.set_text_color(107, 114, 128)
|
| 335 |
x_start = pdf.get_x()
|
| 336 |
y_start = pdf.get_y()
|
| 337 |
+
pdf.cell(90, 4, "Multi-Class Segmentation", 0, 0, "C")
|
| 338 |
pdf.cell(5, 4, "", 0, 0)
|
| 339 |
+
pdf.cell(90, 4, "Class Area Distribution", 0, 1, "C")
|
| 340 |
|
| 341 |
pdf.image(multi_path, x=x_start, y=pdf.get_y(), w=90, h=60)
|
| 342 |
|
|
|
|
| 345 |
legend_y = pdf.get_y() + 5
|
| 346 |
|
| 347 |
class_info = [
|
| 348 |
+
("Foot", result.class_distribution.get("foot", 0), (34, 197, 94)),
|
| 349 |
+
("Perilesional", result.class_distribution.get("perilesion", 0), (249, 115, 22)),
|
| 350 |
+
("Ulcer", result.class_distribution.get("ulcer", 0), (239, 68, 68)),
|
| 351 |
+
("Background", result.class_distribution.get("background", 0), (107, 114, 128)),
|
| 352 |
]
|
| 353 |
|
| 354 |
for cls_name, pct, (r, g, b) in class_info:
|
|
|
|
| 377 |
h_img, w_img = result.image_size
|
| 378 |
pdf._font("", 8)
|
| 379 |
pdf.set_text_color(107, 114, 128)
|
| 380 |
+
pdf.cell(0, 4, f"Resolution: {w_img}x{h_img} px | Device: {result.device} | "
|
| 381 |
+
f"Ulcer area: {result.class_distribution.get('ulcer', 0):.1f}%", 0, 1)
|
| 382 |
pdf.ln(4)
|
| 383 |
|
| 384 |
# ββ Section 2: Fitzpatrick ββ
|
| 385 |
+
pdf.section_title(2, "Fitzpatrick / ITA Skin Type Estimation")
|
| 386 |
fitz = result.fitzpatrick
|
| 387 |
if fitz and fitz.confidence > 0:
|
| 388 |
ftype = fitz.fitzpatrick_type
|
|
|
|
| 400 |
y_warn = pdf.get_y()
|
| 401 |
pdf.rect(pdf.get_x(), y_warn, 185, 10, "DF")
|
| 402 |
pdf.set_xy(pdf.get_x() + 2, y_warn + 1)
|
| 403 |
+
pdf.cell(0, 4, "WARNING: Insufficient lighting β Fitzpatrick type may be overestimated", 0, 1)
|
| 404 |
pdf._font("", 7)
|
| 405 |
pdf.set_text_color(153, 27, 27)
|
| 406 |
pdf.cell(0, 3, lighting_warning, 0, 1)
|
|
|
|
| 413 |
y_warn = pdf.get_y()
|
| 414 |
pdf.rect(pdf.get_x(), y_warn, 185, 10, "DF")
|
| 415 |
pdf.set_xy(pdf.get_x() + 2, y_warn + 1)
|
| 416 |
+
pdf.cell(0, 4, "CAUTION: Suboptimal lighting β result may be off by 1-2 levels", 0, 1)
|
| 417 |
pdf._font("", 7)
|
| 418 |
pdf.set_text_color(146, 64, 14)
|
| 419 |
pdf.cell(0, 3, lighting_warning, 0, 1)
|
|
|
|
| 428 |
pdf._font("B", 16)
|
| 429 |
pdf.set_text_color(*fg)
|
| 430 |
pdf.set_xy(x_badge, y_badge + 2)
|
| 431 |
+
pdf.cell(35, 9, f"Type {ftype}", 0, 0, "C")
|
| 432 |
pdf._font("", 8)
|
| 433 |
pdf.set_xy(x_badge, y_badge + 12)
|
| 434 |
pdf.cell(35, 6, fitz.fitzpatrick_label, 0, 0, "C")
|
|
|
|
| 440 |
det_y = y_badge
|
| 441 |
l_scene = getattr(fitz, "l_scene_mean", 0)
|
| 442 |
details = [
|
| 443 |
+
("ITA", f"{fitz.ita_angle:.1f} +/- {fitz.ita_std:.1f} deg"),
|
| 444 |
+
("L* mean (healthy skin)", f"{fitz.l_skin_mean:.1f}"),
|
| 445 |
+
("L* scene (global)", f"{l_scene:.1f}"),
|
| 446 |
+
("b* mean (healthy skin)", f"{fitz.b_skin_mean:.1f}"),
|
| 447 |
+
("Healthy pixels", f"{fitz.healthy_pixels:,}"),
|
| 448 |
+
("Confidence", f"{fitz.confidence:.0%}"),
|
| 449 |
]
|
| 450 |
for label, value in details:
|
| 451 |
pdf.set_xy(det_x, det_y)
|
|
|
|
| 459 |
else:
|
| 460 |
pdf._font("", 9)
|
| 461 |
pdf.set_text_color(107, 114, 128)
|
| 462 |
+
pdf.cell(0, 5, "Not estimable (insufficient healthy-skin pixels).", 0, 1)
|
| 463 |
pdf.ln(4)
|
| 464 |
|
| 465 |
# ββ Section 3: PWAT ββ
|
| 466 |
+
pdf.section_title(3, "PWAT β Raw vs Fitzpatrick-Adjusted Scores")
|
| 467 |
pwat = result.pwat
|
| 468 |
if pwat and pwat.scores_raw:
|
| 469 |
ftype_str = pwat.fitzpatrick_type or "III"
|
|
|
|
| 473 |
pdf._font("B", 9)
|
| 474 |
pdf.set_text_color(55, 65, 81)
|
| 475 |
col_widths = [55, 25, 25, 25, 20, 35]
|
| 476 |
+
headers = ["PWAT Item", "Raw", "Adj.", "Delta", "Scale", ""]
|
| 477 |
for w, h in zip(col_widths, headers):
|
| 478 |
pdf.cell(w, 6, h, 1, 0, "C", fill=True)
|
| 479 |
pdf.ln()
|
|
|
|
| 512 |
|
| 513 |
# Severity label
|
| 514 |
pdf._font("", 7)
|
| 515 |
+
sev_labels = {0: "Normal", 1: "Mild", 2: "Moderate", 3: "Severe", 4: "Extreme"}
|
| 516 |
pdf.set_text_color(107, 114, 128)
|
| 517 |
pdf.cell(col_widths[5], 6, sev_labels.get(raw, ""), "RB", 0, "L")
|
| 518 |
pdf.ln()
|
|
|
|
| 533 |
# Score interpretation
|
| 534 |
pdf._font("", 8)
|
| 535 |
pdf.set_text_color(107, 114, 128)
|
| 536 |
+
pdf.cell(0, 4, "Scale: 0 (normal) β 4 (extreme) per item. Total: 0-24.", 0, 1)
|
| 537 |
+
pdf.cell(0, 4, f"Bias correction applied for Fitzpatrick type {ftype_str} "
|
| 538 |
+
"(calibrated on 61 images, r=0.975).", 0, 1)
|
| 539 |
|
| 540 |
# Interpretation ranges
|
| 541 |
pdf.ln(2)
|
| 542 |
pdf._font("B", 8)
|
| 543 |
pdf.set_text_color(55, 65, 81)
|
| 544 |
+
pdf.cell(0, 4, "Interpretation of total score:", 0, 1)
|
| 545 |
pdf._font("", 8)
|
| 546 |
ranges = [
|
| 547 |
+
("0-6:", "Wound healing well", (34, 197, 94)),
|
| 548 |
+
("7-12:", "Moderate compromise β clinical follow-up required", (249, 115, 22)),
|
| 549 |
+
("13-18:", "Severe compromise β adjust treatment", (239, 68, 68)),
|
| 550 |
+
("19-24:", "Critical wound β urgent reassessment", (180, 30, 30)),
|
| 551 |
]
|
| 552 |
for label, desc, (r, g, b) in ranges:
|
| 553 |
pdf.set_fill_color(r, g, b)
|
|
|
|
| 563 |
else:
|
| 564 |
pdf._font("", 9)
|
| 565 |
pdf.set_text_color(107, 114, 128)
|
| 566 |
+
pdf.cell(0, 5, "Not estimable (ulcer not detected or area too small).", 0, 1)
|
| 567 |
|
| 568 |
# Save PDF
|
| 569 |
+
pdf_path = os.path.join(tmpdir, "WoundNetB7_DFU_Report.pdf")
|
| 570 |
pdf.output(pdf_path)
|
| 571 |
|
| 572 |
# Cleanup temp images
|
|
|
|
| 596 |
"tta_folds": 6,
|
| 597 |
"debiasing": "Fitzpatrick-calibrated ITA (86.9% accuracy, r=0.975)",
|
| 598 |
}
|
| 599 |
+
json_path = os.path.join(tmpdir, "WoundNetB7_DFU_Report.json")
|
| 600 |
with open(json_path, "w", encoding="utf-8") as f:
|
| 601 |
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
| 602 |
|
|
|
|
| 613 |
if image is None:
|
| 614 |
empty = np.zeros((100, 100, 3), dtype=np.uint8)
|
| 615 |
_last_analysis.clear()
|
| 616 |
+
return empty, empty, empty, "", "", "", "{}"
|
| 617 |
|
| 618 |
img_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
| 619 |
result = pipe.analyze(img_bgr, use_tta=True)
|
| 620 |
|
| 621 |
binary_overlay = pipe.visualize_binary(img_bgr, result)
|
| 622 |
multiclass_overlay = pipe.visualize_multiclass(img_bgr, result)
|
| 623 |
+
dashboard = pipe.render_integrated_report(img_bgr, result)
|
| 624 |
|
| 625 |
_last_analysis["image_rgb"] = image
|
| 626 |
_last_analysis["binary"] = binary_overlay
|
| 627 |
_last_analysis["multiclass"] = multiclass_overlay
|
| 628 |
+
_last_analysis["dashboard"] = dashboard
|
| 629 |
_last_analysis["result"] = result
|
| 630 |
|
| 631 |
seg_stats = build_seg_stats_html(result)
|
|
|
|
| 633 |
pwat_html = build_pwat_html(result.pwat)
|
| 634 |
json_out = json.dumps(result.to_dict(), indent=2, ensure_ascii=False)
|
| 635 |
|
| 636 |
+
return dashboard, binary_overlay, multiclass_overlay, seg_stats, fitz_html, pwat_html, json_out
|
| 637 |
|
| 638 |
|
| 639 |
def analyze_from_camera(image):
|
|
|
|
| 656 |
|
| 657 |
def build_fitz_html(fitz):
|
| 658 |
if fitz is None or fitz.confidence == 0:
|
| 659 |
+
return "<p style='color:#6b7280;'>Not estimable (insufficient healthy-skin pixels).</p>"
|
| 660 |
bg = FITZ_COLORS.get(fitz.fitzpatrick_type, "#e5e7eb")
|
| 661 |
fg = FITZ_TEXT_COLORS.get(fitz.fitzpatrick_type, "#1f2937")
|
| 662 |
|
|
|
|
| 670 |
warning_html = f"""
|
| 671 |
<div style="background:#fef2f2; border:1px solid #fca5a5; border-radius:8px;
|
| 672 |
padding:12px 16px; margin-bottom:12px; font-size:0.9em;">
|
| 673 |
+
<span style="color:#dc2626; font-weight:700;">⚠ Insufficient lighting</span><br>
|
| 674 |
<span style="color:#991b1b;">{lighting_warning}</span><br>
|
| 675 |
+
<span style="color:#6b7280; font-size:0.85em;">L* scene: {l_scene:.0f} (recommended minimum: 35)</span>
|
| 676 |
</div>"""
|
| 677 |
elif lighting_quality == "low":
|
| 678 |
warning_html = f"""
|
| 679 |
<div style="background:#fffbeb; border:1px solid #fcd34d; border-radius:8px;
|
| 680 |
padding:12px 16px; margin-bottom:12px; font-size:0.9em;">
|
| 681 |
+
<span style="color:#d97706; font-weight:700;">⚠ Suboptimal lighting</span><br>
|
| 682 |
<span style="color:#92400e;">{lighting_warning}</span><br>
|
| 683 |
+
<span style="color:#6b7280; font-size:0.85em;">L* scene: {l_scene:.0f} (recommended: >50)</span>
|
| 684 |
</div>"""
|
| 685 |
|
| 686 |
return f"""
|
|
|
|
| 689 |
<div style="background:{bg}; color:{fg}; border-radius:12px; padding:18px 28px;
|
| 690 |
font-size:1.5em; font-weight:700; min-width:120px; text-align:center;
|
| 691 |
border:2px solid rgba(0,0,0,0.1);">
|
| 692 |
+
Type {fitz.fitzpatrick_type}<br>
|
| 693 |
<span style="font-size:0.55em; font-weight:400;">{fitz.fitzpatrick_label}</span>
|
| 694 |
</div>
|
| 695 |
<div style="font-size:0.95em; line-height:1.8;">
|
| 696 |
<b>ITA:</b> {fitz.ita_angle:.1f}° ± {fitz.ita_std:.1f}°<br>
|
| 697 |
+
<b>L* healthy skin:</b> {fitz.l_skin_mean:.1f}<br>
|
| 698 |
+
<b>L* scene:</b> {l_scene:.1f}<br>
|
| 699 |
+
<b>Healthy pixels:</b> {fitz.healthy_pixels:,}<br>
|
| 700 |
+
<b>Confidence:</b> {fitz.confidence:.0%}
|
| 701 |
</div>
|
| 702 |
</div>"""
|
| 703 |
|
| 704 |
|
| 705 |
def build_pwat_html(pwat):
|
| 706 |
if pwat is None or not pwat.scores_raw:
|
| 707 |
+
return "<p style='color:#6b7280;'>PWAT not estimable (ulcer not detected or area too small).</p>"
|
| 708 |
rows = ""
|
| 709 |
for item in [3, 4, 5, 6, 7, 8]:
|
| 710 |
name = ITEM_NAMES.get(item, f"Item {item}")
|
|
|
|
| 743 |
<table style="width:100%; border-collapse:collapse; font-size:0.92em;">
|
| 744 |
<thead>
|
| 745 |
<tr style="border-bottom:2px solid #d1d5db;">
|
| 746 |
+
<th style="padding:10px 12px; text-align:left;">PWAT Item</th>
|
| 747 |
+
<th style="padding:10px 12px; text-align:center;">Raw Score</th>
|
| 748 |
+
<th style="padding:10px 12px; text-align:center;">Adjusted Score</th>
|
| 749 |
<th style="padding:10px 12px; text-align:center;">Δ</th>
|
| 750 |
</tr>
|
| 751 |
</thead>
|
|
|
|
| 759 |
</tbody>
|
| 760 |
</table>
|
| 761 |
<p style="font-size:0.82em; color:#6b7280; margin-top:8px;">
|
| 762 |
+
Scale: 0 (best) β 4 (worst) per item |
|
| 763 |
+
Fitzpatrick type {pwat.fitzpatrick_type} correction applied |
|
| 764 |
+
Items: 3=Necrotic Type, 4=Necrotic Amount, 5=Granulation Type,
|
| 765 |
+
6=Granulation Amount, 7=Edges, 8=Periulcer Skin
|
| 766 |
</p>"""
|
| 767 |
|
| 768 |
|
|
|
|
| 773 |
for cls_name in ["foot", "perilesion", "ulcer"]:
|
| 774 |
pct = dist.get(cls_name, 0)
|
| 775 |
color = colors.get(cls_name, "#6b7280")
|
| 776 |
+
label = {"foot": "Foot", "perilesion": "Perilesional", "ulcer": "Ulcer"}.get(cls_name, cls_name)
|
| 777 |
bars += f"""
|
| 778 |
<div style="margin-bottom:6px;">
|
| 779 |
<div style="display:flex; justify-content:space-between; font-size:0.9em; margin-bottom:2px;">
|
|
|
|
| 787 |
return f"""
|
| 788 |
<div style="padding:4px 0;">
|
| 789 |
<p style="font-size:0.85em; color:#6b7280; margin-bottom:10px;">
|
| 790 |
+
Image: {result.image_size[1]}x{result.image_size[0]} | Device: {result.device}
|
| 791 |
</p>
|
| 792 |
{bars}
|
| 793 |
</div>"""
|
|
|
|
| 824 |
|
| 825 |
with gr.Tabs():
|
| 826 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 827 |
+
# TAB 1: DFU Analysis (upload or gallery)
|
| 828 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 829 |
+
with gr.Tab("DFU Analysis"):
|
| 830 |
with gr.Row():
|
| 831 |
with gr.Column(scale=1):
|
| 832 |
+
input_image = gr.Image(label="DFU Image", type="numpy",
|
| 833 |
sources=["upload", "clipboard"])
|
| 834 |
+
analyze_btn = gr.Button("Analyze", variant="primary", size="lg")
|
| 835 |
gr.HTML("""
|
| 836 |
<div style="font-size:0.82em; color:#6b7280; margin-top:8px; line-height:1.6;">
|
| 837 |
+
<b>Pipeline:</b> the image goes through 4 sequential stages.<br>
|
| 838 |
+
<b>Model:</b> WoundNetB7 with Combo Loss + Small Object Loss.
|
| 839 |
+
Attention modules: CBAM, CoordAttention, TAM (fractal + Euler).<br>
|
| 840 |
+
<b>TTA:</b> 6-fold test-time augmentation.
|
| 841 |
</div>
|
| 842 |
""")
|
| 843 |
|
| 844 |
+
# Integrated clinical dashboard (primary nurse-facing output)
|
| 845 |
+
gr.HTML("""<div class="step-header" style="margin-top:8px;">
|
| 846 |
+
<div class="step-number" style="background:#0e7490;">★</div>
|
| 847 |
+
<div class="step-title">Integrated Clinical Assessment Report</div></div>
|
| 848 |
+
<p style="font-size:0.88em; color:#6b7280; margin-bottom:8px;">
|
| 849 |
+
Single-page summary combining segmentation, Fitzpatrick / ITA estimation
|
| 850 |
+
and Fitzpatrick-adjusted PWAT scoring. Designed for clinical staff.
|
| 851 |
+
</p>""")
|
| 852 |
+
output_dashboard = gr.Image(label="Integrated DFU Assessment Report",
|
| 853 |
+
show_download_button=True, height=720)
|
| 854 |
+
|
| 855 |
# Step 1
|
| 856 |
gr.HTML("""<div class="step-header"><div class="step-number">1</div>
|
| 857 |
+
<div class="step-title">Binary Ulcer Segmentation</div></div>""")
|
| 858 |
with gr.Row():
|
| 859 |
with gr.Column(scale=1):
|
| 860 |
+
output_binary = gr.Image(label="Binary Ulcer Mask (WoundNetB7)")
|
| 861 |
with gr.Column(scale=1):
|
| 862 |
+
output_seg_stats = gr.HTML(label="Segmentation Statistics")
|
| 863 |
|
| 864 |
# Step 2
|
| 865 |
gr.HTML("""<div class="step-header" style="margin-top:12px;"><div class="step-number">2</div>
|
| 866 |
+
<div class="step-title">Multi-Class Segmentation (4 classes)</div></div>""")
|
| 867 |
with gr.Row():
|
| 868 |
with gr.Column(scale=1):
|
| 869 |
+
output_multiclass = gr.Image(label="Multi-Class Overlay")
|
| 870 |
with gr.Column(scale=1):
|
| 871 |
gr.HTML("""
|
| 872 |
<div style="padding:12px;">
|
| 873 |
+
<p style="font-weight:600; margin-bottom:10px;">Class legend:</p>
|
| 874 |
<div style="display:flex; flex-direction:column; gap:8px;">
|
| 875 |
<div style="display:flex; align-items:center; gap:8px;">
|
| 876 |
<div style="width:20px; height:20px; background:#22c55e; border-radius:4px;"></div>
|
| 877 |
+
<span><b>Foot</b> β healthy tissue</span>
|
| 878 |
</div>
|
| 879 |
<div style="display:flex; align-items:center; gap:8px;">
|
| 880 |
<div style="width:20px; height:20px; background:#f97316; border-radius:4px;"></div>
|
| 881 |
+
<span><b>Perilesional</b> β periulcer area</span>
|
| 882 |
</div>
|
| 883 |
<div style="display:flex; align-items:center; gap:8px;">
|
| 884 |
<div style="width:20px; height:20px; background:#ef4444; border-radius:4px;"></div>
|
| 885 |
+
<span><b>Ulcer</b> β wound bed</span>
|
| 886 |
</div>
|
| 887 |
</div>
|
| 888 |
</div>""")
|
| 889 |
|
| 890 |
# Step 3
|
| 891 |
gr.HTML("""<div class="step-header" style="margin-top:12px;"><div class="step-number">3</div>
|
| 892 |
+
<div class="step-title">Fitzpatrick / ITA Skin Type Estimation</div></div>""")
|
| 893 |
output_fitz = gr.HTML()
|
| 894 |
|
| 895 |
# Step 4
|
| 896 |
gr.HTML("""<div class="step-header" style="margin-top:12px;"><div class="step-number">4</div>
|
| 897 |
+
<div class="step-title">PWAT β Raw vs Fitzpatrick-Adjusted Scores</div></div>""")
|
| 898 |
output_pwat = gr.HTML()
|
| 899 |
|
| 900 |
# Download
|
| 901 |
gr.HTML("""<div class="step-header" style="margin-top:16px;">
|
| 902 |
<div class="step-number" style="background:#059669;">⇩</div>
|
| 903 |
+
<div class="step-title">Download Clinical Report</div></div>
|
| 904 |
<p style="font-size:0.88em; color:#6b7280; margin-bottom:8px;">
|
| 905 |
+
Generates a PDF report with all visualizations and structured data.
|
| 906 |
+
Run an analysis first.</p>""")
|
| 907 |
+
download_btn = gr.Button("Download PDF Report", variant="secondary", size="lg")
|
| 908 |
+
output_files = gr.File(label="Report Files (PDF + JSON)", file_count="multiple")
|
| 909 |
|
| 910 |
+
with gr.Accordion("Full JSON (for integration)", open=False):
|
| 911 |
output_json = gr.Code(label="JSON Output", language="json")
|
| 912 |
|
| 913 |
analyze_btn.click(
|
| 914 |
fn=analyze_image,
|
| 915 |
inputs=[input_image],
|
| 916 |
+
outputs=[output_dashboard, output_binary, output_multiclass, output_seg_stats,
|
| 917 |
output_fitz, output_pwat, output_json],
|
| 918 |
)
|
| 919 |
download_btn.click(fn=download_report, inputs=[], outputs=[output_files])
|
| 920 |
|
| 921 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 922 |
+
# TAB 2: Guided Capture (webcam with foot guide overlay)
|
| 923 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 924 |
+
with gr.Tab("Guided Capture"):
|
| 925 |
gr.HTML("""
|
| 926 |
<div style="padding:16px 0;">
|
| 927 |
+
<h2 style="font-size:1.4em; margin:0 0 8px;">Guided Capture for Clinical Staff</h2>
|
| 928 |
<p style="color:#6b7280; line-height:1.6;">
|
| 929 |
+
Use the device camera to capture an image of the diabetic foot.
|
| 930 |
+
The green silhouette guides correct foot positioning for optimal analysis.
|
| 931 |
</p>
|
| 932 |
</div>
|
| 933 |
""")
|
|
|
|
| 935 |
with gr.Row():
|
| 936 |
with gr.Column(scale=3):
|
| 937 |
camera_input = gr.Image(
|
| 938 |
+
label="Camera β Position the foot inside the guide",
|
| 939 |
type="numpy",
|
| 940 |
sources=["webcam"],
|
| 941 |
)
|
| 942 |
+
camera_analyze_btn = gr.Button("Capture and Analyze", variant="primary", size="lg")
|
| 943 |
|
| 944 |
with gr.Column(scale=2):
|
| 945 |
guide_image = gr.Image(
|
| 946 |
+
label="Positioning Guide",
|
| 947 |
value=generate_static_guide(),
|
| 948 |
interactive=False,
|
| 949 |
)
|
|
|
|
| 952 |
<div style="background:#f0fdf4; border:1px solid #bbf7d0; border-radius:10px;
|
| 953 |
padding:16px; margin:12px 0;">
|
| 954 |
<p style="font-weight:700; color:#166534; margin:0 0 8px;">
|
| 955 |
+
Instructions for clinical staff:
|
| 956 |
</p>
|
| 957 |
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px 24px; font-size:0.92em; color:#15803d;">
|
| 958 |
+
<div>1. Plantar surface facing the camera</div>
|
| 959 |
+
<div>2. Distance: 30-40 cm from the lens</div>
|
| 960 |
+
<div>3. Uniform lighting, no shadows</div>
|
| 961 |
+
<div>4. Neutral background (white/blue sheet)</div>
|
| 962 |
+
<div>5. Include the full ulcer + surrounding healthy skin</div>
|
| 963 |
+
<div>6. Avoid direct flash (causes glare)</div>
|
| 964 |
+
<div>7. Keep the device steady</div>
|
| 965 |
+
<div>8. Clean the lens before capturing</div>
|
| 966 |
</div>
|
| 967 |
</div>
|
| 968 |
""")
|
| 969 |
|
| 970 |
+
# Integrated dashboard for camera capture
|
| 971 |
+
gr.HTML("""<div class="step-header" style="margin-top:16px;">
|
| 972 |
+
<div class="step-number" style="background:#0e7490;">★</div>
|
| 973 |
+
<div class="step-title">Integrated Clinical Assessment Report</div></div>""")
|
| 974 |
+
cam_dashboard = gr.Image(label="Integrated DFU Assessment Report",
|
| 975 |
+
show_download_button=True, height=720)
|
| 976 |
+
|
| 977 |
# Results from camera capture
|
| 978 |
gr.HTML("""<div class="step-header" style="margin-top:16px;">
|
| 979 |
<div class="step-number">1</div>
|
| 980 |
+
<div class="step-title">Segmentation Result</div></div>""")
|
| 981 |
with gr.Row():
|
| 982 |
+
cam_binary = gr.Image(label="Binary Ulcer Mask")
|
| 983 |
+
cam_multiclass = gr.Image(label="Multi-Class Overlay")
|
| 984 |
|
| 985 |
with gr.Row():
|
| 986 |
cam_seg_stats = gr.HTML()
|
|
|
|
| 990 |
cam_fitz = gr.HTML()
|
| 991 |
cam_pwat = gr.HTML()
|
| 992 |
|
| 993 |
+
cam_download_btn = gr.Button("Download PDF Report", variant="secondary", size="lg")
|
| 994 |
+
cam_files = gr.File(label="Report Files", file_count="multiple")
|
| 995 |
|
| 996 |
+
with gr.Accordion("Full JSON", open=False):
|
| 997 |
cam_json = gr.Code(label="JSON Output", language="json")
|
| 998 |
|
| 999 |
camera_analyze_btn.click(
|
| 1000 |
fn=analyze_from_camera,
|
| 1001 |
inputs=[camera_input],
|
| 1002 |
+
outputs=[cam_dashboard, cam_binary, cam_multiclass, cam_seg_stats,
|
| 1003 |
cam_fitz, cam_pwat, cam_json],
|
| 1004 |
)
|
| 1005 |
cam_download_btn.click(fn=download_report, inputs=[], outputs=[cam_files])
|
|
|
|
| 1007 |
gr.HTML("""
|
| 1008 |
<div style="text-align:center; padding:16px 0; font-size:0.82em; color:#9ca3af;
|
| 1009 |
border-top:1px solid #e5e7eb; margin-top:20px;">
|
| 1010 |
+
WoundNetB7 • Doctoral Thesis • Marcelo Marquez-Murillo •
|
| 1011 |
+
Ulcer Dice 0.927 (95% CI: [0.917, 0.936]) •
|
| 1012 |
Debiasing: 46.6% max group gap reduction (p < 10<sup>-55</sup>)
|
| 1013 |
</div>
|
| 1014 |
""")
|