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()