mmarquezsa commited on
Commit
04c9d0a
·
verified ·
1 Parent(s): cd8e4ff

feat: upgrade pipeline UI — binary seg + multiclass + PWAT raw vs adjusted

Browse files
Files changed (1) hide show
  1. app.py +310 -62
app.py CHANGED
@@ -1,102 +1,350 @@
1
- """Gradio app for WoundNetB7 DFU Analysis — Hugging Face Spaces deployment."""
 
 
 
 
 
 
 
 
 
 
2
  import gradio as gr
3
  import numpy as np
4
  import cv2
5
  import json
6
- import traceback
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- # Lazy loading — don't crash at import time
9
- pipe = None
 
 
 
 
10
 
 
 
 
11
 
12
- def get_pipeline():
13
- global pipe
14
- if pipe is None:
15
- from pipeline import WoundNetB7Pipeline
16
- pipe = WoundNetB7Pipeline(models_dir="models", use_tta=False)
17
- return pipe
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- def create_overlay(img_rgb, classmap):
21
- """Create segmentation overlay on RGB image."""
22
- colors = {1: (0, 255, 0), 2: (255, 165, 0), 3: (255, 0, 0)}
23
- overlay = img_rgb.astype(np.float32).copy()
24
- for cid, color in colors.items():
25
- mask = classmap == cid
26
- if np.any(mask):
27
- overlay[mask] = overlay[mask] * 0.5 + np.array(color, dtype=np.float32) * 0.5
28
- return overlay.astype(np.uint8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
 
31
  def analyze_image(image):
32
  """Main analysis function called by Gradio."""
33
  if image is None:
34
- return None, "Please upload an image.", "{}"
 
35
 
36
- try:
37
- pipeline = get_pipeline()
38
- img_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
39
- result = pipeline.analyze(img_bgr, use_tta=False)
40
 
41
- # Create overlay from the segmentation already done (no re-run)
42
- from src.segmentation import segment, CLASS_NAMES
43
- seg = segment(pipeline.seg_model, img_bgr, pipeline.device, use_tta=False)
44
- classmap = seg["classmap"]
45
 
46
- if classmap.shape[:2] != image.shape[:2]:
47
- classmap = cv2.resize(classmap, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_NEAREST)
 
48
 
49
- overlay = create_overlay(image, classmap)
50
- summary = result.summary()
51
- json_out = json.dumps(result.to_dict(), indent=2, ensure_ascii=False)
 
52
 
53
- return overlay, summary, json_out
 
54
 
55
- except Exception as e:
56
- error_msg = f"Error: {str(e)}\n\n{traceback.format_exc()}"
57
- return None, error_msg, "{}"
58
 
59
 
60
- with gr.Blocks(title="WoundNetB7 DFU Analysis", theme=gr.themes.Soft()) as demo:
61
- gr.Markdown(
62
- """
63
- # WoundNetB7 — Diabetic Foot Ulcer Analysis
64
 
65
- Upload a DFU image to get:
66
- 1. **Multiclass segmentation** (foot / perilesion / ulcer)
67
- 2. **Fitzpatrick skin type** via calibrated ITA (86.9% accuracy)
68
- 3. **PWAT scores** items 3-8 with Fitzpatrick debiasing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- > Model: EfficientNet-B7 + ASPP + CBAM + TAM | Ulcer Dice: 0.927
71
- """
72
- )
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  with gr.Row():
75
  with gr.Column(scale=1):
76
- input_image = gr.Image(label="DFU Image", type="numpy")
77
- analyze_btn = gr.Button("Analyze", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  with gr.Column(scale=1):
79
- output_overlay = gr.Image(label="Segmentation Overlay")
 
 
80
 
 
 
 
 
 
 
 
81
  with gr.Row():
82
  with gr.Column(scale=1):
83
- output_text = gr.Textbox(label="Analysis Summary", lines=25, max_lines=40)
84
  with gr.Column(scale=1):
85
- output_json = gr.Code(label="JSON Output", language="json")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
- analyze_btn.click(fn=analyze_image, inputs=[input_image], outputs=[output_overlay, output_text, output_json])
 
 
 
 
 
 
 
88
 
89
- gr.Markdown(
90
- """
91
- ---
92
- **Legend:** Green = Foot | Orange = Perilesion | Red = Ulcer
 
 
 
 
93
 
94
- **PWAT Items:** 3=Necrotic Type, 4=Necrotic Amount, 5=Granulation Type,
95
- 6=Granulation Amount, 7=Edges, 8=Periulcer Skin (0=best, 4=worst)
 
96
 
97
- **Debiasing:** Scores adjusted by Fitzpatrick type to reduce skin-tone bias.
98
- """
 
 
 
 
 
 
 
 
 
99
  )
100
 
 
 
 
 
 
 
 
 
101
  if __name__ == "__main__":
102
  demo.launch(share=False)
 
1
+ """Gradio app for WoundNetB7 DFU Analysis — Hugging Face Spaces deployment.
2
+
3
+ Pipeline visualization:
4
+ 1. Binary ulcer segmentation (WoundNetB7 + ASPP + CBAM + CoordAttention + TAM)
5
+ 2. Multiclass segmentation (background / foot / perilesion / ulcer)
6
+ 3. Fitzpatrick/ITA skin type estimation
7
+ 4. PWAT scores (raw) + PWAT adjusted by Fitzpatrick debiasing
8
+
9
+ Launch locally: python app.py
10
+ Deploy to HF: push this repo to a Hugging Face Space (GPU recommended).
11
+ """
12
  import gradio as gr
13
  import numpy as np
14
  import cv2
15
  import json
16
+ from pipeline import WoundNetB7Pipeline
17
+
18
+ pipe = WoundNetB7Pipeline(models_dir="models", use_tta=True)
19
+
20
+ FITZ_COLORS = {
21
+ "I": "#fef3c7", "II": "#fde68a", "III": "#fbbf24",
22
+ "IV": "#b45309", "V": "#78350f", "VI": "#451a03",
23
+ }
24
+
25
+ FITZ_TEXT_COLORS = {
26
+ "I": "#1f2937", "II": "#1f2937", "III": "#1f2937",
27
+ "IV": "#ffffff", "V": "#ffffff", "VI": "#ffffff",
28
+ }
29
+
30
+
31
+ def build_fitz_html(fitz):
32
+ """Build Fitzpatrick/ITA HTML card."""
33
+ if fitz is None or fitz.confidence == 0:
34
+ return "<p style='color:#6b7280;'>No se pudo estimar (insuficientes pixeles de piel sana).</p>"
35
+
36
+ bg = FITZ_COLORS.get(fitz.fitzpatrick_type, "#e5e7eb")
37
+ fg = FITZ_TEXT_COLORS.get(fitz.fitzpatrick_type, "#1f2937")
38
+
39
+ return f"""
40
+ <div style="display:flex; gap:16px; align-items:center; flex-wrap:wrap;">
41
+ <div style="background:{bg}; color:{fg}; border-radius:12px; padding:18px 28px;
42
+ font-size:1.5em; font-weight:700; min-width:120px; text-align:center;
43
+ border:2px solid rgba(0,0,0,0.1);">
44
+ Tipo {fitz.fitzpatrick_type}<br>
45
+ <span style="font-size:0.55em; font-weight:400;">{fitz.fitzpatrick_label}</span>
46
+ </div>
47
+ <div style="font-size:0.95em; line-height:1.8;">
48
+ <b>ITA:</b> {fitz.ita_angle:.1f}&deg; &plusmn; {fitz.ita_std:.1f}&deg;<br>
49
+ <b>L* medio:</b> {fitz.l_skin_mean:.1f}<br>
50
+ <b>Pixeles sanos:</b> {fitz.healthy_pixels:,}<br>
51
+ <b>Confianza:</b> {fitz.confidence:.0%}
52
+ </div>
53
+ </div>
54
+ """
55
+
56
+
57
+ def build_pwat_html(pwat):
58
+ """Build PWAT scores comparison table (raw vs adjusted)."""
59
+ if pwat is None or not pwat.scores_raw:
60
+ return "<p style='color:#6b7280;'>No se pudo estimar PWAT (ulcera no detectada o muy pequena).</p>"
61
+
62
+ from src.pwat_estimator import ITEM_NAMES
63
 
64
+ rows = ""
65
+ for item in [3, 4, 5, 6, 7, 8]:
66
+ name = ITEM_NAMES.get(item, f"Item {item}")
67
+ raw = pwat.scores_raw.get(item, 0)
68
+ adj = pwat.scores_adjusted.get(item, 0.0)
69
+ diff = adj - raw
70
 
71
+ # Color code: green if adjusted lower (debiased), neutral otherwise
72
+ diff_color = "#059669" if diff < -0.05 else "#6b7280"
73
+ diff_str = f"{diff:+.1f}" if abs(diff) > 0.01 else "0.0"
74
 
75
+ # Bar visualization (0-4 scale)
76
+ raw_pct = raw / 4 * 100
77
+ adj_pct = adj / 4 * 100
 
 
 
78
 
79
+ rows += f"""
80
+ <tr>
81
+ <td style="padding:8px 12px; font-weight:500;">{name}</td>
82
+ <td style="padding:8px 12px; text-align:center;">
83
+ <div style="display:flex; align-items:center; gap:8px;">
84
+ <div style="background:#e5e7eb; border-radius:4px; height:14px; width:80px; overflow:hidden;">
85
+ <div style="background:#ef4444; height:100%; width:{raw_pct}%; border-radius:4px;"></div>
86
+ </div>
87
+ <span style="font-weight:600; min-width:20px;">{raw}</span>
88
+ </div>
89
+ </td>
90
+ <td style="padding:8px 12px; text-align:center;">
91
+ <div style="display:flex; align-items:center; gap:8px;">
92
+ <div style="background:#e5e7eb; border-radius:4px; height:14px; width:80px; overflow:hidden;">
93
+ <div style="background:#3b82f6; height:100%; width:{adj_pct}%; border-radius:4px;"></div>
94
+ </div>
95
+ <span style="font-weight:600; min-width:30px;">{adj:.1f}</span>
96
+ </div>
97
+ </td>
98
+ <td style="padding:8px 12px; text-align:center; color:{diff_color}; font-weight:600;">
99
+ {diff_str}
100
+ </td>
101
+ </tr>"""
102
 
103
+ total_diff = pwat.total_adjusted - pwat.total_raw
104
+ total_color = "#059669" if total_diff < -0.05 else "#6b7280"
105
+ total_diff_str = f"{total_diff:+.1f}" if abs(total_diff) > 0.01 else "0.0"
106
+
107
+ return f"""
108
+ <table style="width:100%; border-collapse:collapse; font-size:0.92em;">
109
+ <thead>
110
+ <tr style="border-bottom:2px solid #d1d5db;">
111
+ <th style="padding:10px 12px; text-align:left;">Item PWAT</th>
112
+ <th style="padding:10px 12px; text-align:center;">Score Raw</th>
113
+ <th style="padding:10px 12px; text-align:center;">Score Ajustado</th>
114
+ <th style="padding:10px 12px; text-align:center;">&Delta;</th>
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ {rows}
119
+ <tr style="border-top:2px solid #374151; font-weight:700; font-size:1.05em;">
120
+ <td style="padding:10px 12px;">TOTAL</td>
121
+ <td style="padding:10px 12px; text-align:center;">{pwat.total_raw}</td>
122
+ <td style="padding:10px 12px; text-align:center;">{pwat.total_adjusted:.1f}</td>
123
+ <td style="padding:10px 12px; text-align:center; color:{total_color};">{total_diff_str}</td>
124
+ </tr>
125
+ </tbody>
126
+ </table>
127
+ <p style="font-size:0.82em; color:#6b7280; margin-top:8px;">
128
+ Escala: 0 (mejor) &mdash; 4 (peor) por item &bull;
129
+ Correccion Fitzpatrick tipo {pwat.fitzpatrick_type} aplicada &bull;
130
+ Items: 3=Tipo necrotico, 4=Cantidad necrotica, 5=Tipo granulacion,
131
+ 6=Cantidad granulacion, 7=Bordes, 8=Piel periulceral
132
+ </p>
133
+ """
134
+
135
+
136
+ def build_seg_stats_html(result):
137
+ """Build segmentation statistics HTML."""
138
+ dist = result.class_distribution
139
+ colors = {"background": "#374151", "foot": "#22c55e", "perilesion": "#f97316", "ulcer": "#ef4444"}
140
+
141
+ bars = ""
142
+ for cls_name in ["foot", "perilesion", "ulcer"]:
143
+ pct = dist.get(cls_name, 0)
144
+ color = colors.get(cls_name, "#6b7280")
145
+ label = {"foot": "Pie", "perilesion": "Perilesion", "ulcer": "Ulcera"}.get(cls_name, cls_name)
146
+ bars += f"""
147
+ <div style="margin-bottom:6px;">
148
+ <div style="display:flex; justify-content:space-between; font-size:0.9em; margin-bottom:2px;">
149
+ <span style="color:{color}; font-weight:600;">{label}</span>
150
+ <span>{pct:.1f}%</span>
151
+ </div>
152
+ <div style="background:#e5e7eb; border-radius:4px; height:12px; overflow:hidden;">
153
+ <div style="background:{color}; height:100%; width:{pct}%; border-radius:4px;"></div>
154
+ </div>
155
+ </div>"""
156
+
157
+ return f"""
158
+ <div style="padding:4px 0;">
159
+ <p style="font-size:0.85em; color:#6b7280; margin-bottom:10px;">
160
+ Imagen: {result.image_size[1]}x{result.image_size[0]} &bull; Device: {result.device}
161
+ </p>
162
+ {bars}
163
+ </div>
164
+ """
165
 
166
 
167
  def analyze_image(image):
168
  """Main analysis function called by Gradio."""
169
  if image is None:
170
+ empty = np.zeros((100, 100, 3), dtype=np.uint8)
171
+ return empty, empty, "", "", "", "{}"
172
 
173
+ img_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
 
 
 
174
 
175
+ result = pipe.analyze(img_bgr, use_tta=True)
 
 
 
176
 
177
+ # Visualizations
178
+ binary_overlay = pipe.visualize_binary(img_bgr, result)
179
+ multiclass_overlay = pipe.visualize_multiclass(img_bgr, result)
180
 
181
+ # HTML outputs
182
+ seg_stats = build_seg_stats_html(result)
183
+ fitz_html = build_fitz_html(result.fitzpatrick)
184
+ pwat_html = build_pwat_html(result.pwat)
185
 
186
+ # JSON
187
+ json_out = json.dumps(result.to_dict(), indent=2, ensure_ascii=False)
188
 
189
+ return binary_overlay, multiclass_overlay, seg_stats, fitz_html, pwat_html, json_out
 
 
190
 
191
 
192
+ # ── Gradio UI ────────────────────────────────────────────────────────────────
 
 
 
193
 
194
+ css = """
195
+ .pipeline-step {
196
+ border: 1px solid #e5e7eb;
197
+ border-radius: 12px;
198
+ padding: 16px;
199
+ margin-bottom: 8px;
200
+ }
201
+ .step-header {
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 10px;
205
+ margin-bottom: 12px;
206
+ }
207
+ .step-number {
208
+ background: #1f2937;
209
+ color: white;
210
+ border-radius: 50%;
211
+ width: 30px;
212
+ height: 30px;
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ font-weight: 700;
217
+ font-size: 0.9em;
218
+ flex-shrink: 0;
219
+ }
220
+ .step-title {
221
+ font-weight: 600;
222
+ font-size: 1.1em;
223
+ }
224
+ """
225
 
226
+ with gr.Blocks(
227
+ title="WoundNetB7 DFU Analysis Pipeline",
228
+ theme=gr.themes.Soft(),
229
+ css=css,
230
+ ) as demo:
231
+
232
+ gr.HTML("""
233
+ <div style="text-align:center; padding:20px 0 10px;">
234
+ <h1 style="font-size:1.8em; margin:0;">WoundNetB7 — DFU Analysis Pipeline</h1>
235
+ <p style="color:#6b7280; font-size:1em; margin-top:6px;">
236
+ EfficientNet-B7 + ASPP + CBAM + CoordAttention + TAM &bull; Ulcer Dice: 0.927
237
+ </p>
238
+ </div>
239
+ """)
240
 
241
  with gr.Row():
242
  with gr.Column(scale=1):
243
+ input_image = gr.Image(label="Imagen DFU", type="numpy")
244
+ analyze_btn = gr.Button("Analizar", variant="primary", size="lg")
245
+ gr.HTML("""
246
+ <div style="font-size:0.82em; color:#6b7280; margin-top:8px; line-height:1.6;">
247
+ <b>Pipeline:</b> La imagen pasa por 4 etapas secuenciales.<br>
248
+ <b>Modelo:</b> WoundNetB7 entrenado con Combo Loss + Small Object Loss
249
+ para ulceras pequenas. Mecanismos de atencion: CBAM (canal+espacial),
250
+ CoordAttention (posicional), TAM (topologico con dimension fractal
251
+ y caracteristica de Euler).<br>
252
+ <b>TTA:</b> 6 augmentaciones en inferencia (flips + rotaciones).
253
+ </div>
254
+ """)
255
+
256
+ # ── Step 1: Binary Segmentation ──
257
+ gr.HTML("""
258
+ <div class="step-header">
259
+ <div class="step-number">1</div>
260
+ <div class="step-title">Segmentacion Binaria de la Ulcera</div>
261
+ </div>
262
+ """)
263
+ with gr.Row():
264
  with gr.Column(scale=1):
265
+ output_binary = gr.Image(label="Mascara Binaria Ulcera (WoundNetB7)")
266
+ with gr.Column(scale=1):
267
+ output_seg_stats = gr.HTML(label="Estadisticas de Segmentacion")
268
 
269
+ # ── Step 2: Multiclass Segmentation ──
270
+ gr.HTML("""
271
+ <div class="step-header" style="margin-top:12px;">
272
+ <div class="step-number">2</div>
273
+ <div class="step-title">Segmentacion Multiclase (4 clases)</div>
274
+ </div>
275
+ """)
276
  with gr.Row():
277
  with gr.Column(scale=1):
278
+ output_multiclass = gr.Image(label="Overlay Multiclase")
279
  with gr.Column(scale=1):
280
+ gr.HTML("""
281
+ <div style="padding:12px;">
282
+ <p style="font-weight:600; margin-bottom:10px;">Leyenda de clases:</p>
283
+ <div style="display:flex; flex-direction:column; gap:8px;">
284
+ <div style="display:flex; align-items:center; gap:8px;">
285
+ <div style="width:20px; height:20px; background:#22c55e; border-radius:4px;"></div>
286
+ <span><b>Pie</b> &mdash; tejido sano del pie</span>
287
+ </div>
288
+ <div style="display:flex; align-items:center; gap:8px;">
289
+ <div style="width:20px; height:20px; background:#f97316; border-radius:4px;"></div>
290
+ <span><b>Perilesion</b> &mdash; zona periulceral</span>
291
+ </div>
292
+ <div style="display:flex; align-items:center; gap:8px;">
293
+ <div style="width:20px; height:20px; background:#ef4444; border-radius:4px;"></div>
294
+ <span><b>Ulcera</b> &mdash; lecho de la herida</span>
295
+ </div>
296
+ </div>
297
+ <p style="font-size:0.82em; color:#6b7280; margin-top:12px;">
298
+ Modelo multiclase con Combo Loss (Dice + CE ponderado) +
299
+ Small Object Focal Loss para deteccion de ulceras pequenas.
300
+ Arquitectura con skip connections y ASPP (rates 6, 12, 18)
301
+ para capturar contexto multi-escala.
302
+ </p>
303
+ </div>
304
+ """)
305
 
306
+ # ── Step 3: Fitzpatrick/ITA ──
307
+ gr.HTML("""
308
+ <div class="step-header" style="margin-top:12px;">
309
+ <div class="step-number">3</div>
310
+ <div class="step-title">Estimacion Fitzpatrick / ITA</div>
311
+ </div>
312
+ """)
313
+ output_fitz = gr.HTML()
314
 
315
+ # ── Step 4: PWAT ──
316
+ gr.HTML("""
317
+ <div class="step-header" style="margin-top:12px;">
318
+ <div class="step-number">4</div>
319
+ <div class="step-title">PWAT &mdash; Scores Raw vs Ajustados por Fitzpatrick</div>
320
+ </div>
321
+ """)
322
+ output_pwat = gr.HTML()
323
 
324
+ # ── JSON (collapsible) ──
325
+ with gr.Accordion("JSON completo (para integracion)", open=False):
326
+ output_json = gr.Code(label="JSON Output", language="json")
327
 
328
+ analyze_btn.click(
329
+ fn=analyze_image,
330
+ inputs=[input_image],
331
+ outputs=[
332
+ output_binary,
333
+ output_multiclass,
334
+ output_seg_stats,
335
+ output_fitz,
336
+ output_pwat,
337
+ output_json,
338
+ ],
339
  )
340
 
341
+ gr.HTML("""
342
+ <div style="text-align:center; padding:16px 0; font-size:0.82em; color:#9ca3af; border-top:1px solid #e5e7eb; margin-top:20px;">
343
+ WoundNetB7 &bull; Tesis Doctoral &bull; Marcelo Marquez-Murillo &bull;
344
+ Ulcer Dice 0.927 (CI 95%: [0.917, 0.936]) &bull;
345
+ Debiasing: 46.6% max group gap reduction (p &lt; 10<sup>-55</sup>)
346
+ </div>
347
+ """)
348
+
349
  if __name__ == "__main__":
350
  demo.launch(share=False)