Spaces:
Running on Zero
Running on Zero
File size: 5,635 Bytes
812b6ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | #!/usr/bin/env python3
"""Like-for-like attribute comparison of two cross-validation runs.
python scripts/compare_attributes.py --data-dir DATA --runs-a <folder with fold_*.pt> --runs-b <folder> [--device mps]
Pooled attribute accuracy in cv_report.md is computed over the nuclei each model
matched (IoU >= 0.5), so a model that matches more nuclei is scored on a harder
set. This script predicts every held-out image with both fold checkpoints and
reports the antibody / Nostril attribute accuracy on the nuclei matched by BOTH,
plus the per-label count errors. It makes no choice; it only reads out numbers.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from cellquant.annotations import discover, load_image, rasterize # noqa: E402
from cellquant.infer import Predictor # noqa: E402
from cellquant.labels import ALL_LABELS, cell_attributes # noqa: E402
from cellquant.metrics import match_by_iou # noqa: E402
from cellquant.postprocess import PostConfig # noqa: E402
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--data-dir", required=True)
ap.add_argument("--runs-a", required=True)
ap.add_argument("--runs-b", required=True)
ap.add_argument("--names", default="v1,v2")
ap.add_argument("--device", default="auto")
ap.add_argument("--out", default="")
a = ap.parse_args(argv)
na, nb = a.names.split(",")
samples, _ = discover(a.data_dir, verbose=False)
tot = {na: {"ab": 0, "nos": 0, "both": 0}, nb: {"ab": 0, "nos": 0, "both": 0}, "n": 0}
neg = {na: {"tp": 0, "fp": 0, "fn": 0}, nb: {"tp": 0, "fp": 0, "fn": 0}}
counts = {lab: {"gt": 0, na: 0, nb: 0, f"abs_{na}": 0, f"abs_{nb}": 0} for lab in ALL_LABELS}
rows = []
for s in samples:
img = load_image(s.image_path)
R = rasterize(s.annotation, img.shape[0], img.shape[1])
preds = {}
for name, d in ((na, a.runs_a), (nb, a.runs_b)):
ck = os.path.join(d, f"fold_{s.key}.pt")
res, _ = Predictor(ck, device=a.device, tta=1).predict(img, PostConfig())
preds[name] = res
for lab in ALL_LABELS:
counts[lab][name] += res.counts[lab]
counts[lab][f"abs_{name}"] += abs(res.counts[lab] - s.annotation.count(lab))
for lab in ALL_LABELS:
counts[lab]["gt"] += s.annotation.count(lab)
m = {name: {g: p for g, p, _ in match_by_iou(R.cell_inst, preds[name].cell_inst, 0.5)} for name in preds}
common = sorted(set(m[na]) & set(m[nb]))
row = {"image": s.key, "n_common": len(common), f"n_{na}": len(m[na]), f"n_{nb}": len(m[nb])}
for name in (na, nb):
ab = ns = both = 0
for g in common:
ga, gn = cell_attributes(R.cell_labels[g])
pa, pn = cell_attributes(preds[name].cell_labels[m[name][g]])
ab += ga == pa
ns += gn == pn
both += (ga == pa) and (gn == pn)
if not ga and not pa:
neg[name]["tp"] += 1
elif ga and not pa:
neg[name]["fp"] += 1
elif not ga and pa:
neg[name]["fn"] += 1
row[f"ab_{name}"] = ab / max(1, len(common))
row[f"nos_{name}"] = ns / max(1, len(common))
row[f"both_{name}"] = both / max(1, len(common))
tot[name]["ab"] += ab
tot[name]["nos"] += ns
tot[name]["both"] += both
tot["n"] += len(common)
rows.append(row)
print(f"[{s.key}] common {len(common)}: antibody acc {na} {row[f'ab_{na}']:.3f} {nb} {row[f'ab_{nb}']:.3f} | "
f"nostril acc {row[f'nos_{na}']:.3f} {row[f'nos_{nb}']:.3f} | both {row[f'both_{na}']:.3f} {row[f'both_{nb}']:.3f}", flush=True)
n = max(1, tot["n"])
L = [f"## Attribute accuracy on the {tot['n']} held-out nuclei matched by both {na} and {nb} (IoU >= 0.5, base run)", "",
f"| | {na} | {nb} |", "|---|---:|---:|"]
for k, label in (("ab", "Antibody +/− correct"), ("nos", "Nostril attribute correct"), ("both", "Both correct")):
L.append(f"| {label} | {100*tot[na][k]/n:.1f}% | {100*tot[nb][k]/n:.1f}% |")
for name in (na, nb):
t = neg[name]
p = t["tp"] / max(1, t["tp"] + t["fp"])
r = t["tp"] / max(1, t["tp"] + t["fn"])
f = 2 * p * r / max(1e-9, p + r)
L.append(f"| Antibody − on this set ({name}): TP / FP / FN, precision, recall, F1 | {t['tp']} / {t['fp']} / {t['fn']}, {100*p:.1f}%, {100*r:.1f}%, {100*f:.1f}% | |")
L += ["", f"| Image | matched by both | {na} antibody acc | {nb} antibody acc | {na} Nostril acc | {nb} Nostril acc |", "|---|---:|---:|---:|---:|---:|"]
for r in rows:
L.append(f"| {r['image']} | {r['n_common']} | {100*r[f'ab_{na}']:.1f}% | {100*r[f'ab_{nb}']:.1f}% | {100*r[f'nos_{na}']:.1f}% | {100*r[f'nos_{nb}']:.1f}% |")
L += ["", "## Counts summed over the nine held-out images (and summed absolute per-image count error)", "",
f"| Label | GT | {na} | {nb} | abs error {na} | abs error {nb} |", "|---|---:|---:|---:|---:|---:|"]
for lab in ALL_LABELS:
c = counts[lab]
L.append(f"| {lab} | {c['gt']} | {c[na]} | {c[nb]} | {c[f'abs_{na}']} | {c[f'abs_{nb}']} |")
L.append("")
text = "\n".join(L)
print(text)
if a.out:
with open(a.out, "a") as f:
f.write("\n" + text)
print("appended to", a.out)
if __name__ == "__main__":
main()
|