mmarquezsa commited on
Commit
33a4b40
Β·
verified Β·
1 Parent(s): ffa8f7b

Add integrated DFU dashboard + Fitzpatrick adjustment (English UI)

Browse files
Files changed (1) hide show
  1. app.py +145 -125
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, "Posicione el pie dentro de la guia",
142
  (w // 2 - 200, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 220, 120), 2, cv2.LINE_AA)
143
- cv2.putText(result, "Distancia: 30-40 cm | Iluminacion uniforme",
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), "Guia de Captura DFU", fill=(31, 41, 55), font=font_title)
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.", "Planta del pie hacia la camara (vista plantar)"),
180
- ("2.", "Distancia: 30-40 cm del lente"),
181
- ("3.", "Iluminacion uniforme, sin sombras directas"),
182
- ("4.", "Fondo neutro (sabana blanca o azul)"),
183
- ("5.", "Incluir toda la ulcera + 3-5 cm de piel sana"),
184
- ("6.", "Sin flash directo (causa reflejos)"),
185
- ("7.", "Pie centrado dentro de la silueta verde"),
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: Para mejor resultado, capture con luz natural",
196
  fill=(107, 114, 128), font=font_small)
197
  draw.text((30, y + 32),
198
- "difusa. Evite luces cenitales que generan sombra.",
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 - Informe de Analisis DFU", 0, 0, "L")
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 | Tesis Doctoral | Marcelo Marquez-Murillo | "
281
- "Dice 0.927 (CI 95%: [0.917, 0.936]) | Debiasing: 46.6% gap reduction (p < 1e-55)", 0, 0, "C")
282
- self.cell(0, 5, f"Pag {self.page_no()}/{{nb}}", 0, 0, "R")
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, "Segmentacion")
329
- pdf.add_image_pair(orig_path, "Imagen Original", binary_path, "Segmentacion Binaria (Ulcera)")
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, "Segmentacion Multiclase", 0, 0, "C")
338
  pdf.cell(5, 4, "", 0, 0)
339
- pdf.cell(90, 4, "Distribucion de Clases", 0, 1, "C")
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
- ("Pie", result.class_distribution.get("foot", 0), (34, 197, 94)),
349
- ("Perilesion", result.class_distribution.get("perilesion", 0), (249, 115, 22)),
350
- ("Ulcera", result.class_distribution.get("ulcer", 0), (239, 68, 68)),
351
- ("Fondo", result.class_distribution.get("background", 0), (107, 114, 128)),
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"Resolucion: {w_img}x{h_img} px | Device: {result.device} | "
381
- f"Area ulcera: {result.class_distribution.get('ulcer', 0):.1f}%", 0, 1)
382
  pdf.ln(4)
383
 
384
  # ── Section 2: Fitzpatrick ──
385
- pdf.section_title(2, "Estimacion Fitzpatrick / ITA")
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, "ADVERTENCIA: Iluminacion insuficiente - tipo Fitzpatrick puede estar sobreestimado", 0, 1)
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, "PRECAUCION: Iluminacion suboptima - resultado puede tener 1-2 niveles de error", 0, 1)
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"Tipo {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,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} grados"),
444
- ("L* medio (piel sana)", f"{fitz.l_skin_mean:.1f}"),
445
- ("L* escena (global)", f"{l_scene:.1f}"),
446
- ("b* medio (piel sana)", f"{fitz.b_skin_mean:.1f}"),
447
- ("Pixeles sanos", f"{fitz.healthy_pixels:,}"),
448
- ("Confianza", f"{fitz.confidence:.0%}"),
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, "No estimable (insuficientes pixeles de piel sana).", 0, 1)
463
  pdf.ln(4)
464
 
465
  # ── Section 3: PWAT ──
466
- pdf.section_title(3, "PWAT - Scores Raw vs Ajustados por Fitzpatrick")
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 = ["Item PWAT", "Raw", "Ajust.", "Delta", "Escala", ""]
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: "Leve", 2: "Moderado", 3: "Severo", 4: "Extremo"}
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, "Escala: 0 (normal) - 4 (extremo) por item. Total: 0-24.", 0, 1)
537
- pdf.cell(0, 4, f"Correccion de sesgo aplicada segun Fitzpatrick tipo {ftype_str} "
538
- "(calibrada en 61 imagenes, 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, "Interpretacion del puntaje total:", 0, 1)
545
  pdf._font("", 8)
546
  ranges = [
547
- ("0-6:", "Herida en buen estado de cicatrizacion", (34, 197, 94)),
548
- ("7-12:", "Herida con compromiso moderado, requiere seguimiento", (249, 115, 22)),
549
- ("13-18:", "Herida con compromiso severo, ajustar tratamiento", (239, 68, 68)),
550
- ("19-24:", "Herida critica, evaluacion urgente", (180, 30, 30)),
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, "No estimable (ulcera no detectada o area insuficiente).", 0, 1)
567
 
568
  # Save PDF
569
- pdf_path = os.path.join(tmpdir, "WoundNetB7_Informe_DFU.pdf")
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, "WoundNetB7_Informe_DFU.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,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;'>No se pudo estimar (insuficientes pixeles de piel sana).</p>"
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;">&#9888; Iluminacion insuficiente</span><br>
672
  <span style="color:#991b1b;">{lighting_warning}</span><br>
673
- <span style="color:#6b7280; font-size:0.85em;">L* escena: {l_scene:.0f} (minimo recomendado: 35)</span>
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;">&#9888; Iluminacion suboptima</span><br>
680
  <span style="color:#92400e;">{lighting_warning}</span><br>
681
- <span style="color:#6b7280; font-size:0.85em;">L* escena: {l_scene:.0f} (recomendado: >50)</span>
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
- Tipo {fitz.fitzpatrick_type}<br>
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}&deg; &plusmn; {fitz.ita_std:.1f}&deg;<br>
695
- <b>L* medio piel:</b> {fitz.l_skin_mean:.1f}<br>
696
- <b>L* escena:</b> {l_scene:.1f}<br>
697
- <b>Pixeles sanos:</b> {fitz.healthy_pixels:,}<br>
698
- <b>Confianza:</b> {fitz.confidence:.0%}
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;'>No se pudo estimar PWAT (ulcera no detectada o muy pequena).</p>"
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;">Item PWAT</th>
745
- <th style="padding:10px 12px; text-align:center;">Score Raw</th>
746
- <th style="padding:10px 12px; text-align:center;">Score Ajustado</th>
747
  <th style="padding:10px 12px; text-align:center;">&Delta;</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
- Escala: 0 (mejor) - 4 (peor) por item |
761
- Correccion Fitzpatrick tipo {pwat.fitzpatrick_type} aplicada |
762
- Items: 3=Tipo necrotico, 4=Cantidad necrotica, 5=Tipo granulacion,
763
- 6=Cantidad granulacion, 7=Bordes, 8=Piel periulceral
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": "Pie", "perilesion": "Perilesion", "ulcer": "Ulcera"}.get(cls_name, cls_name)
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
- Imagen: {result.image_size[1]}x{result.image_size[0]} | Device: {result.device}
789
  </p>
790
  {bars}
791
  </div>"""
@@ -822,99 +824,110 @@ with gr.Blocks(
822
 
823
  with gr.Tabs():
824
  # ══════════════════════════════════════════════════════════════════════
825
- # TAB 1: Analisis (upload o galeria)
826
  # ══════════════════════════════════════════════════════════════════════
827
- with gr.Tab("Analisis DFU"):
828
  with gr.Row():
829
  with gr.Column(scale=1):
830
- input_image = gr.Image(label="Imagen DFU", type="numpy",
831
  sources=["upload", "clipboard"])
832
- analyze_btn = gr.Button("Analizar", variant="primary", size="lg")
833
  gr.HTML("""
834
  <div style="font-size:0.82em; color:#6b7280; margin-top:8px; line-height:1.6;">
835
- <b>Pipeline:</b> La imagen pasa por 4 etapas secuenciales.<br>
836
- <b>Modelo:</b> WoundNetB7 con Combo Loss + Small Object Loss.
837
- Atencion: CBAM, CoordAttention, TAM (fractal + Euler).<br>
838
- <b>TTA:</b> 6 augmentaciones en inferencia.
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">Segmentacion Binaria de la Ulcera</div></div>""")
845
  with gr.Row():
846
  with gr.Column(scale=1):
847
- output_binary = gr.Image(label="Mascara Binaria Ulcera (WoundNetB7)")
848
  with gr.Column(scale=1):
849
- output_seg_stats = gr.HTML(label="Estadisticas de Segmentacion")
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">Segmentacion Multiclase (4 clases)</div></div>""")
854
  with gr.Row():
855
  with gr.Column(scale=1):
856
- output_multiclass = gr.Image(label="Overlay Multiclase")
857
  with gr.Column(scale=1):
858
  gr.HTML("""
859
  <div style="padding:12px;">
860
- <p style="font-weight:600; margin-bottom:10px;">Leyenda de clases:</p>
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>Pie</b> β€” tejido sano</span>
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>Perilesion</b> β€” zona periulceral</span>
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>Ulcera</b> β€” lecho de la herida</span>
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">Estimacion Fitzpatrick / ITA</div></div>""")
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 β€” Scores Raw vs Ajustados por Fitzpatrick</div></div>""")
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;">&#8681;</div>
890
- <div class="step-title">Descargar Informe Clinico</div></div>
891
  <p style="font-size:0.88em; color:#6b7280; margin-bottom:8px;">
892
- Genera un informe PDF con todas las visualizaciones y datos estructurados.
893
- Primero analiza una imagen.</p>""")
894
- download_btn = gr.Button("Descargar Informe PDF", variant="secondary", size="lg")
895
- output_files = gr.File(label="Archivos del Informe (PDF + JSON)", file_count="multiple")
896
 
897
- with gr.Accordion("JSON completo (para integracion)", open=False):
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: Captura Guiada (webcam con guia de pie)
910
  # ══════════════════════════════════════════════════════════════════════
911
- with gr.Tab("Captura Guiada"):
912
  gr.HTML("""
913
  <div style="padding:16px 0;">
914
- <h2 style="font-size:1.4em; margin:0 0 8px;">Captura Guiada para Personal Sanitario</h2>
915
  <p style="color:#6b7280; line-height:1.6;">
916
- Use la camara del dispositivo para capturar una imagen del pie diabetico.
917
- La silueta verde guia la posicion correcta del pie para un analisis optimo.
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="Camara β€” Posicione el pie dentro de la guia",
926
  type="numpy",
927
  sources=["webcam"],
928
  )
929
- camera_analyze_btn = gr.Button("Capturar y Analizar", variant="primary", size="lg")
930
 
931
  with gr.Column(scale=2):
932
  guide_image = gr.Image(
933
- label="Guia de Posicionamiento",
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
- Instrucciones para el personal sanitario:
943
  </p>
944
  <div style="display:grid; grid-template-columns:1fr 1fr; gap:8px 24px; font-size:0.92em; color:#15803d;">
945
- <div>1. Planta del pie mirando a la camara</div>
946
- <div>2. Distancia: 30-40 cm del lente</div>
947
- <div>3. Iluminacion uniforme, sin sombras</div>
948
- <div>4. Fondo neutro (sabana blanca/azul)</div>
949
- <div>5. Incluir toda la ulcera + piel sana periferica</div>
950
- <div>6. Evitar flash directo (causa reflejos)</div>
951
- <div>7. Mantener el dispositivo estable</div>
952
- <div>8. Limpiar el lente antes de capturar</div>
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">Resultado de Segmentacion</div></div>""")
961
  with gr.Row():
962
- cam_binary = gr.Image(label="Mascara Binaria Ulcera")
963
- cam_multiclass = gr.Image(label="Overlay Multiclase")
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("Descargar Informe PDF", variant="secondary", size="lg")
974
- cam_files = gr.File(label="Archivos del Informe", file_count="multiple")
975
 
976
- with gr.Accordion("JSON completo", open=False):
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 &bull; Tesis Doctoral &bull; Marcelo Marquez-Murillo &bull;
991
- Ulcer Dice 0.927 (CI 95%: [0.917, 0.936]) &bull;
992
  Debiasing: 46.6% max group gap reduction (p &lt; 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;">&#9888; 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;">&#9888; 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}&deg; &plusmn; {fitz.ita_std:.1f}&deg;<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;">&Delta;</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;">&#9733;</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;">&#8681;</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;">&#9733;</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 &bull; Doctoral Thesis &bull; Marcelo Marquez-Murillo &bull;
1011
+ Ulcer Dice 0.927 (95% CI: [0.917, 0.936]) &bull;
1012
  Debiasing: 46.6% max group gap reduction (p &lt; 10<sup>-55</sup>)
1013
  </div>
1014
  """)