satdetect-dev / scripts /eval_synthetic_cd.py
coderuday21's picture
Cursor
Deploy satdetect-dev with Priyanka audit/exception logs.
d70361b
Raw
History Blame Contribute Delete
6.37 kB
"""Eval run_detection vs auto-generated GT on the synthetic before/after/mask dataset.
Generated by gen_synthetic.py (Priyanka, dataset-prep track): synthetic objects
(roofs/vehicles/vegetation) are composited onto real "before" tiles to make an
"after" image, with the exact pasted region saved as the GT mask - no manual
labeling involved. This script is the training-track's counterpart: point it at
the dataset once a fine-tuned model is available and it reports F1/precision/
recall/IoU the same way eval_drone_gt_packs.py does for the hand-labeled packs.
Usage:
python scripts/eval_synthetic_cd.py --tag baseline
python scripts/eval_synthetic_cd.py --tag baseline --limit 200
python scripts/eval_synthetic_cd.py --dataset-dir data/some_other_triplet_set --tag holdout
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
try:
from dotenv import load_dotenv
load_dotenv(ROOT / ".env")
except ImportError:
pass
DEFAULT_DATASET_DIR = Path(r"C:\Users\Priyanka\Downloads\Synthetic_CD_dataset")
def _metrics(pred: np.ndarray, gt: np.ndarray) -> dict:
p = pred.astype(bool).ravel()
g = gt.astype(bool).ravel()
tp = int(np.logical_and(p, g).sum())
fp = int(np.logical_and(p, ~g).sum())
fn = int(np.logical_and(~p, g).sum())
prec = tp / (tp + fp) if (tp + fp) else 0.0
rec = tp / (tp + fn) if (tp + fn) else 0.0
f1 = (2 * prec * rec / (prec + rec)) if (prec + rec) else 0.0
iou = tp / (tp + fp + fn) if (tp + fp + fn) else 0.0
return {
"f1": round(f1, 4),
"precision": round(prec, 4),
"recall": round(rec, 4),
"iou": round(iou, 4),
"tp": tp,
"fp": fp,
"fn": fn,
"pred_change_pct": round(100.0 * float(p.mean()), 4),
"gt_change_pct": round(100.0 * float(g.mean()), 4),
}
def eval_one(tile_id: str, dataset_dir: Path, method: str, enable_registration: bool = True) -> dict:
from app.detection_engine import run_detection
before_p = dataset_dir / "before" / f"{tile_id}.png"
after_p = dataset_dir / "after" / f"{tile_id}.png"
gt_p = dataset_dir / "mask" / f"{tile_id}.png"
if not (before_p.is_file() and after_p.is_file() and gt_p.is_file()):
return {"tile_id": tile_id, "error": "missing files"}
before_pil = Image.open(before_p).convert("RGB")
after_pil = Image.open(after_p).convert("RGB")
if after_pil.size != before_pil.size:
after_pil = after_pil.resize(before_pil.size, Image.Resampling.LANCZOS)
t0 = time.time()
change_mask, _vis, stats, regions = run_detection(
before_pil,
after_pil,
method=method,
enable_registration=enable_registration,
enable_normalization=True,
detection_sensitivity=0.5,
min_region_area=150,
max_size=max(before_pil.size),
before_path=None,
after_path=None,
)
elapsed = time.time() - t0
gt = np.array(Image.open(gt_p).convert("L")) > 127
pred = np.asarray(change_mask)
if pred.ndim == 3:
pred = pred[..., 0]
pred = pred > 127
if pred.shape != gt.shape:
gt_img = Image.fromarray((gt.astype(np.uint8) * 255))
gt_img = gt_img.resize((pred.shape[1], pred.shape[0]), Image.Resampling.NEAREST)
gt = np.array(gt_img) > 127
m = _metrics(pred, gt)
return {
"tile_id": tile_id,
"elapsed_s": round(elapsed, 2),
"n_regions": len(regions or []),
"report_change_pct": round(float(stats.get("change_percentage") or 0.0), 4),
**m,
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--tag", default="eval")
ap.add_argument("--dataset-dir", default=str(DEFAULT_DATASET_DIR))
ap.add_argument("--out-dir", default="data/synthetic_cd_eval")
ap.add_argument("--limit", type=int, default=None, help="Evaluate only the first N tiles (quick smoke test)")
ap.add_argument("--method", default="AI-Based Deep Learning",
help="Detection method passed to run_detection, e.g. 'Feature-Based' "
"(CPU-only, no torch) or 'AI-Based Deep Learning' (needs GPU/torch)")
ap.add_argument("--no-register", action="store_true")
args = ap.parse_args()
dataset_dir = Path(args.dataset_dir)
before_dir = dataset_dir / "before"
if not before_dir.is_dir():
print(f"ERROR: {before_dir} not found", flush=True)
sys.exit(1)
tile_ids = sorted(p.stem for p in before_dir.glob("*.png"))
if args.limit:
tile_ids = tile_ids[: args.limit]
print(f"Evaluating {len(tile_ids)} tiles from {dataset_dir}", flush=True)
out_dir = ROOT / args.out_dir
out_dir.mkdir(parents=True, exist_ok=True)
rows = []
for i, tid in enumerate(tile_ids):
row = eval_one(tid, dataset_dir, method=args.method, enable_registration=not args.no_register)
rows.append(row)
if "error" in row:
print(f" [{i+1}/{len(tile_ids)}] {tid}: ERROR {row['error']}", flush=True)
else:
print(
f" [{i+1}/{len(tile_ids)}] {tid}: F1={row['f1']:.3f} P={row['precision']:.3f} "
f"R={row['recall']:.3f} IoU={row['iou']:.3f}",
flush=True,
)
ok = [r for r in rows if "f1" in r]
summary = {
"tag": args.tag,
"dataset_dir": str(dataset_dir),
"n_tiles": len(tile_ids),
"n_ok": len(ok),
"created_unix": time.time(),
"mean_f1": round(float(np.mean([r["f1"] for r in ok])), 4) if ok else 0.0,
"mean_precision": round(float(np.mean([r["precision"] for r in ok])), 4) if ok else 0.0,
"mean_recall": round(float(np.mean([r["recall"] for r in ok])), 4) if ok else 0.0,
"mean_iou": round(float(np.mean([r["iou"] for r in ok])), 4) if ok else 0.0,
"tiles": rows,
}
out = out_dir / f"{args.tag}.json"
out.write_text(json.dumps(summary, indent=2), encoding="utf-8")
print(
f"\nSaved {out} | mean_F1={summary['mean_f1']:.4f} "
f"P={summary['mean_precision']:.3f} R={summary['mean_recall']:.3f} IoU={summary['mean_iou']:.3f}",
flush=True,
)
if __name__ == "__main__":
main()