MaybeRichard commited on
Commit
4e00077
·
verified ·
1 Parent(s): 5b3e6b8

aggregate.py: unified-512 intro + per-class + Wilcoxon

Browse files
Files changed (1) hide show
  1. code/framework/report/aggregate.py +143 -4
code/framework/report/aggregate.py CHANGED
@@ -188,12 +188,20 @@ def _intro_html():
188
  h.append("<h2>Project overview</h2>")
189
  h.append("<p>A unified benchmark of <b>8 2D medical-image segmentation methods</b> across "
190
  "<b>10 public datasets</b> spanning <b>7 imaging modalities</b> (endoscopy, retinal fundus, "
191
- "ultrasound, cardiac MRI, dermoscopy, histopathology, abdominal CT). Every method is trained "
192
- "and evaluated through one pipeline (bf16 AMP on A100, identical metrics). Reported as "
193
  "<b>mean±SD</b> over <b>3 seeds</b> for fixed-split datasets and over <b>folds</b> for "
194
  "cross-validation datasets (PanNuke: official 3-fold). Each (dataset,method) cell aggregates "
195
  "~tens–thousands of test images; the suite totals ≈20k images. Per-method efficiency "
196
  "(params / FLOPs / throughput) is in <code>efficiency.md</code>.</p>")
 
 
 
 
 
 
 
 
197
 
198
  h.append("<h2>Datasets</h2>")
199
  h.append("<table><tr><th>#</th><th>Dataset</th><th>Modality</th><th>Target</th><th>Classes</th>"
@@ -238,7 +246,133 @@ def _intro_html():
238
  return "\n".join(h)
239
 
240
 
241
- def to_html(rows, title="SegGen baselines"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  cell, dslist = {}, []
243
  for r in rows:
244
  ds = f"{r['dataset']}/{r['protocol']}"
@@ -282,6 +416,11 @@ def to_html(rows, title="SegGen baselines"):
282
  tds.append(f"<td>{nseeds.get(ds,'')}</td>")
283
  h.append("<tr>" + "".join(tds) + "</tr>")
284
  h.append("</table>")
 
 
 
 
 
285
  h.append("</body></html>")
286
  return "\n".join(h)
287
 
@@ -301,7 +440,7 @@ def main():
301
  open(os.path.join(base, "summary.csv"), "w").write(to_csv(rows))
302
  open(os.path.join(base, "summary.md"), "w").write(to_markdown(rows))
303
  open(os.path.join(base, "summary.tex"), "w").write(to_latex(rows))
304
- open(os.path.join(base, "summary.html"), "w").write(to_html(rows, title=f"SegGen baselines ({args.exp_name})"))
305
  print(to_markdown(rows))
306
  print(f"{len(runs)} runs -> {len(rows)} (dataset,arch) cells; written {base}/summary.{{csv,md,tex,html}}")
307
 
 
188
  h.append("<h2>Project overview</h2>")
189
  h.append("<p>A unified benchmark of <b>8 2D medical-image segmentation methods</b> across "
190
  "<b>10 public datasets</b> spanning <b>7 imaging modalities</b> (endoscopy, retinal fundus, "
191
+ "ultrasound, cardiac MRI, dermoscopy, histopathology, abdominal CT). Every method runs through "
192
+ "one pipeline with identical metrics. Reported as "
193
  "<b>mean±SD</b> over <b>3 seeds</b> for fixed-split datasets and over <b>folds</b> for "
194
  "cross-validation datasets (PanNuke: official 3-fold). Each (dataset,method) cell aggregates "
195
  "~tens–thousands of test images; the suite totals ≈20k images. Per-method efficiency "
196
  "(params / FLOPs / throughput) is in <code>efficiency.md</code>.</p>")
197
+ h.append("<p style='background:#eef7f0;border-left:3px solid #0a6;padding:8px 12px'>"
198
+ "<b>Resolution-fair evaluation (unified 512).</b> Convolutional methods are trained at "
199
+ "512×512; the fixed-input transformers (Swin-UNet 224, TransUNet 256) run at their native "
200
+ "size; nnU-Net / U-Mamba predict natively. <b>For scoring, every method's prediction and the "
201
+ "ground truth are resized to a common 512×512</b> before computing all 7 metrics — so the "
202
+ "boundary metrics (HD95 / ASSD), which are measured in pixels, are <b>directly comparable "
203
+ "across methods</b> regardless of each method's native working resolution. (Earlier 256-px "
204
+ "tables mixed pixel scales and were not boundary-comparable.)</p>")
205
 
206
  h.append("<h2>Datasets</h2>")
207
  h.append("<table><tr><th>#</th><th>Dataset</th><th>Modality</th><th>Target</th><th>Classes</th>"
 
246
  return "\n".join(h)
247
 
248
 
249
+ # ---- (3) per-class Dice tables for the multi-class datasets ----
250
+ # Class names follow each dataset's standard foreground labelling (0=background excluded).
251
+ _PERCLASS_NAMES = {
252
+ "acdc_png": {"1": "RV", "2": "Myocardium", "3": "LV"},
253
+ "refuge2": {"1": "Optic Disc", "2": "Optic Cup"},
254
+ "idridd_segmentation": {"1": "MA", "2": "Haemorrhage", "3": "Hard Exudate", "4": "Soft Exudate", "5": "Optic Disc"},
255
+ "pannuke_semantic": {"1": "Neoplastic", "2": "Inflammatory", "3": "Connective", "4": "Dead", "5": "Epithelial"},
256
+ }
257
+
258
+
259
+ def _collect_perclass(runs):
260
+ acc = defaultdict(lambda: defaultdict(list)) # (dataset,arch)->class->[dice]
261
+ for d in runs:
262
+ key = (d.get("dataset"), d.get("arch"))
263
+ for pi in d.get("per_image", []):
264
+ for c, m in (pi.get("per_class") or {}).items():
265
+ v = (m or {}).get("dice")
266
+ if v is not None and v == v:
267
+ acc[key][c].append(v)
268
+ return {k: {c: float(np.mean(v)) for c, v in cd.items() if v} for k, cd in acc.items()}
269
+
270
+
271
+ def _perclass_section(runs):
272
+ pc = _collect_perclass(runs)
273
+ h = ["<h1 style='font-size:18px'>Per-class Dice — multi-class datasets</h1>",
274
+ "<div class='cap'>Mean per-class Dice (%) over all test images and all runs (0=background "
275
+ "excluded). Reveals which structures/types drive a method's macro-Dice. Class names follow each "
276
+ "dataset's standard labelling (IDRiD: MA=microaneurysm, exudates hard/soft, optic disc).</div>"]
277
+ for ds, names in _PERCLASS_NAMES.items():
278
+ methods = [a for a in _ARCH_ORDER if (ds, a) in pc and pc[(ds, a)]]
279
+ if not methods:
280
+ continue
281
+ classes = sorted(names, key=int)
282
+ h.append(f"<h2>{ds}</h2>")
283
+ h.append("<table><tr><th>Method</th>" + "".join(f"<th>{names[c]}</th>" for c in classes)
284
+ + "<th>macro</th></tr>")
285
+ # best per class (column) for bolding
286
+ colbest = {c: max((pc[(ds, a)].get(c, float('nan')) for a in methods),
287
+ default=float('nan')) for c in classes}
288
+ for a in methods:
289
+ cd = pc[(ds, a)]
290
+ cells = []
291
+ present = []
292
+ for c in classes:
293
+ v = cd.get(c)
294
+ if v is None:
295
+ cells.append("<td>—</td>")
296
+ else:
297
+ present.append(v)
298
+ txt = f"{v*100:.1f}"
299
+ cells.append(f"<td>{'<b>'+txt+'</b>' if v == colbest[c] else txt}</td>")
300
+ macro = (sum(present) / len(present) * 100) if present else float("nan")
301
+ h.append(f"<tr><td class='l'>{a}</td>{''.join(cells)}<td>{macro:.1f}</td></tr>")
302
+ h.append("</table>")
303
+ return "\n".join(h)
304
+
305
+
306
+ # ---- (4) paired Wilcoxon significance on per-image Dice ----
307
+ def _per_image_dice_vec(runs_for_da):
308
+ """Aligned per-image Dice for one (dataset,arch): mean over seeds within each
309
+ protocol/fold, concatenated over sorted protocols. Length matches across methods."""
310
+ by_proto = defaultdict(list)
311
+ for d in runs_for_da:
312
+ by_proto[d.get("protocol")].append(d)
313
+ parts = []
314
+ for proto in sorted(by_proto):
315
+ arrs = [np.array([pi.get("dice", np.nan) for pi in d.get("per_image", [])], float)
316
+ for d in by_proto[proto]]
317
+ arrs = [a for a in arrs if a.size]
318
+ if not arrs:
319
+ continue
320
+ L = min(a.size for a in arrs)
321
+ parts.append(np.nanmean(np.stack([a[:L] for a in arrs]), axis=0))
322
+ return np.concatenate(parts) if parts else np.array([])
323
+
324
+
325
+ def _wilcoxon_section(runs):
326
+ try:
327
+ from scipy.stats import wilcoxon
328
+ except Exception:
329
+ return "<p>(scipy unavailable — significance section skipped)</p>"
330
+ by_da = defaultdict(list)
331
+ for d in runs:
332
+ by_da[(d.get("dataset"), d.get("arch"))].append(d)
333
+
334
+ def pval(a, b):
335
+ L = min(a.size, b.size)
336
+ if L < 6:
337
+ return float("nan")
338
+ x, y = a[:L], b[:L]
339
+ m = ~(np.isnan(x) | np.isnan(y))
340
+ if m.sum() < 6 or np.allclose(x[m], y[m]):
341
+ return 1.0
342
+ try:
343
+ return float(wilcoxon(x[m], y[m]).pvalue)
344
+ except Exception:
345
+ return 1.0
346
+
347
+ h = ["<h1 style='font-size:18px'>Statistical significance — paired Wilcoxon (per-image Dice)</h1>",
348
+ "<div class='cap'>Per dataset, the best method by mean Dice is marked ★; a paired Wilcoxon "
349
+ "signed-rank test on per-image Dice (paired by image) compares it against every other method. "
350
+ "Methods <u>not</u> significantly worse than the best (p≥0.05) are <u>underlined</u> = statistically "
351
+ "tied for best. The last column is the best-vs-2nd-best p-value. Pairing is exact for the in-framework "
352
+ "methods (shared dataloader order); nnU-Net/U-Mamba use cached predictions paired by index where "
353
+ "image counts match.</div>"]
354
+ h.append("<table><tr><th>Dataset</th><th>Best ★ and tied-for-best (underlined)</th>"
355
+ "<th>best vs 2nd p</th></tr>")
356
+ for ds in sorted({k[0] for k in by_da}):
357
+ vecs = {a: _per_image_dice_vec(by_da[(ds, a)]) for a in _ARCH_ORDER if (ds, a) in by_da}
358
+ vecs = {a: v for a, v in vecs.items() if v.size}
359
+ if len(vecs) < 2:
360
+ continue
361
+ means = {a: float(np.nanmean(v)) for a, v in vecs.items()}
362
+ ranked = sorted(means, key=means.get, reverse=True)
363
+ best = ranked[0]
364
+ tied = [a for a in ranked[1:] if not (pval(vecs[best], vecs[a]) < 0.05)]
365
+ p2 = pval(vecs[best], vecs[ranked[1]])
366
+ marks = f"<b>★ {best} ({means[best]*100:.1f})</b>"
367
+ if tied:
368
+ marks += " &nbsp;·&nbsp; tied: " + ", ".join(f"<u>{a}</u>" for a in tied)
369
+ h.append(f"<tr><td class='l'>{ds}</td><td class='l'>{marks}</td>"
370
+ f"<td>{('%.2g' % p2) if p2 == p2 else '—'}</td></tr>")
371
+ h.append("</table>")
372
+ return "\n".join(h)
373
+
374
+
375
+ def to_html(rows, runs=None, title="SegGen baselines"):
376
  cell, dslist = {}, []
377
  for r in rows:
378
  ds = f"{r['dataset']}/{r['protocol']}"
 
416
  tds.append(f"<td>{nseeds.get(ds,'')}</td>")
417
  h.append("<tr>" + "".join(tds) + "</tr>")
418
  h.append("</table>")
419
+ if runs:
420
+ h.append("<hr style='margin:22px 0;border:none;border-top:2px solid #0a6'>")
421
+ h.append(_perclass_section(runs))
422
+ h.append("<hr style='margin:22px 0;border:none;border-top:2px solid #0a6'>")
423
+ h.append(_wilcoxon_section(runs))
424
  h.append("</body></html>")
425
  return "\n".join(h)
426
 
 
440
  open(os.path.join(base, "summary.csv"), "w").write(to_csv(rows))
441
  open(os.path.join(base, "summary.md"), "w").write(to_markdown(rows))
442
  open(os.path.join(base, "summary.tex"), "w").write(to_latex(rows))
443
+ open(os.path.join(base, "summary.html"), "w").write(to_html(rows, runs, title=f"SegGen baselines ({args.exp_name})"))
444
  print(to_markdown(rows))
445
  print(f"{len(runs)} runs -> {len(rows)} (dataset,arch) cells; written {base}/summary.{{csv,md,tex,html}}")
446