satdetect-dev / scripts /analyze_adaptformer_fn.py
coderuday21's picture
Cursor
Deploy satdetect-dev with Priyanka audit/exception logs.
d70361b
Raw
History Blame Contribute Delete
5.98 kB
"""Analyze false negatives from a fine-tuned AdaptFormer checkpoint.
Categorizes missed change components (small / large / thin) and writes
Before|GT|Pred|TP-green/FN-red/FP-blue panels.
Usage:
python scripts/analyze_adaptformer_fn.py --ckpt runs/finetune_v2/20260716_210208/best
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ckpt", required=True)
parser.add_argument("--delhi-cd", default="data/delhi_cd")
parser.add_argument("--thr", type=float, default=None)
parser.add_argument("--out", default="")
args = parser.parse_args()
import torch
from transformers import AutoImageProcessor, AutoModel
from app.model_inference import _logits_to_change_prob
from app.evaluation.delhi_eval import _load_label, _load_rgb
from app.evaluation.metrics import binary_metrics
ckpt = Path(args.ckpt)
if not ckpt.is_dir():
raise SystemExit(f"Missing checkpoint {ckpt}")
thr = args.thr
if thr is None:
thr_path = ckpt / "threshold.json"
thr = float(json.loads(thr_path.read_text()).get("threshold", 0.5)) if thr_path.is_file() else 0.5
out = Path(args.out) if args.out else ckpt.parent / "fn_analysis"
out.mkdir(parents=True, exist_ok=True)
proc = AutoImageProcessor.from_pretrained(ckpt, trust_remote_code=True)
model = AutoModel.from_pretrained(ckpt, trust_remote_code=True).eval()
cats = {k: 0 for k in ("small_blob", "large_blob", "thin_linear", "missed_almost_all", "partial", "ok")}
summary = []
delhi_cd = Path(args.delhi_cd)
for split in ("train", "val", "test"):
man = delhi_cd / split / "manifest.json"
if not man.is_file():
continue
for p in json.loads(man.read_text()).get("pairs", []):
before = _load_rgb(ROOT / p["before_path"])
after = _load_rgb(ROOT / p["after_path"])
gt = _load_label(ROOT / p["gt_mask"])
b256 = np.array(Image.fromarray(before).resize((256, 256)))
a256 = np.array(Image.fromarray(after).resize((256, 256)))
inputs = proc(images=(Image.fromarray(b256), Image.fromarray(a256)), return_tensors="pt")
with torch.no_grad():
prob = _logits_to_change_prob(model(**inputs).logits, torch).cpu().numpy()
if prob.shape != gt.shape[:2]:
pred = cv2.resize((prob >= thr).astype(np.uint8), (gt.shape[1], gt.shape[0]),
interpolation=cv2.INTER_NEAREST).astype(bool)
else:
pred = prob >= thr
g = gt > 127
m = binary_metrics((pred.astype(np.uint8) * 255), gt)
fn = g & ~pred
fp = pred & ~g
tp = pred & g
n_labels, lab, stats, _ = cv2.connectedComponentsWithStats(g.astype(np.uint8), 8)
fn_small = fn_large = fn_thin = missed_comp = 0
for i in range(1, n_labels):
area = int(stats[i, cv2.CC_STAT_AREA])
w = int(stats[i, cv2.CC_STAT_WIDTH])
h = int(stats[i, cv2.CC_STAT_HEIGHT])
comp = lab == i
recall_c = float((pred & comp).sum()) / max(area, 1)
aspect = max(w, h) / max(min(w, h), 1)
if recall_c < 0.2:
missed_comp += 1
if area < 40:
fn_small += 1
elif aspect >= 3:
fn_thin += 1
else:
fn_large += 1
miss_ratio = float(fn.sum()) / max(int(g.sum()), 1)
if miss_ratio < 0.25:
cat = "ok"
elif miss_ratio > 0.75:
cat = "missed_almost_all"
elif fn_small >= fn_large and fn_small >= fn_thin:
cat = "small_blob"
elif fn_thin > fn_large:
cat = "thin_linear"
else:
cat = "partial"
cats[cat] += 1
row = {
"split": split,
"pair_id": p["pair_id"],
"f1": round(m["f1"], 4),
"precision": round(m["precision"], 4),
"recall": round(m["recall"], 4),
"gt_frac": round(float(g.mean()), 4),
"miss_ratio": round(miss_ratio, 3),
"n_gt_comp": max(0, n_labels - 1),
"missed_comp": missed_comp,
"fn_small": fn_small,
"fn_large": fn_large,
"fn_thin": fn_thin,
"category": cat,
}
summary.append(row)
if split in ("test", "val") or miss_ratio > 0.5:
overlay = np.zeros((*g.shape, 3), np.uint8)
overlay[tp] = (0, 200, 0)
overlay[fn] = (255, 0, 0)
overlay[fp] = (0, 0, 255)
before_r = np.array(Image.fromarray(before).resize((g.shape[1], g.shape[0])))
gt_rgb = np.stack([gt, gt, gt], axis=-1)
pred_rgb = np.stack([pred.astype(np.uint8) * 255] * 3, axis=-1)
panel = np.concatenate([before_r, gt_rgb, pred_rgb, overlay], axis=1)
Image.fromarray(panel).save(out / f"{split}_{p['pair_id']}_{cat}.png")
report = {"threshold": thr, "category_counts": cats, "pairs": summary}
(out / "fn_summary.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print("FN categories:", cats)
print("Test pairs:")
for r in summary:
if r["split"] == "test":
print(f" {r['pair_id']}: F1={r['f1']} P={r['precision']} R={r['recall']} cat={r['category']}")
print(f"Wrote {out}")
if __name__ == "__main__":
main()