satdetect-dev / scripts /eval_seed_conditions.py
coderuday21's picture
Cursor
Deploy satdetect-dev with Priyanka audit/exception logs.
d70361b
Raw
History Blame Contribute Delete
16.7 kB
"""Environment check + AdaptFormer eval on the real Delhi seed GT.
Reports overall F1/P/R/IoU and condition buckets (vegetation, roof, lighting,
alignment) wherever labels exist. Uses the same run_detection + binary_metrics
path as scripts/evaluate_finetuned_vs_baseline.py.
Usage (from change_detection_webapp):
python -u scripts/eval_seed_conditions.py
python -u scripts/eval_seed_conditions.py --weights models/adaptformer_delhi/wed_retrain
"""
from __future__ import annotations
import argparse
import json
import os
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
from app.evaluation.delhi_eval import _load_label, _load_rgb, load_manifest # noqa: E402
from app.evaluation.metrics import binary_metrics # noqa: E402
DEFAULT_WEIGHTS = (
ROOT / "runs" / "finetune_adaptformer" / "priyanka_gt" / "20260903_163459" / "best"
)
SYNTHETIC_CANDIDATES = (
Path(r"C:\Users\Priyanka\Downloads\Synthetic_CD_dataset"),
Path(r"C:\Users\udayb\Downloads\Synthetic_CD_dataset"),
ROOT / "data" / "synthetic_cd",
ROOT / "data" / "Synthetic_CD_dataset",
)
HELD_OUT_EMPTY = {
"delhi_0002", "delhi_0006", "delhi_0007", "delhi_0008",
"delhi_0010", "delhi_0013", "delhi_0014", "delhi_0015",
}
ROOF_PAIRS = {"dda_before5_after5_v2", "dda_before6_after6_v2"}
def _resolve_pair_file(rel: str, pair_id: str, kind: str) -> Path | None:
raw = Path(rel) if rel else Path()
candidates: list[Path] = []
if rel:
if raw.is_absolute():
candidates.append(raw)
candidates.append(Path(r"C:\Users\udayb\Downloads") / raw.name)
candidates.append(Path(r"C:\Users\udayb\Downloads") / raw.name.replace(" (1)", ""))
else:
candidates.append(ROOT / raw)
pack = ROOT / "docs" / "delhi_eval" / "dda_labeling" / pair_id
kind_name = {"before": "before.png", "after": "after.png", "gt": "gt_mask.png"}[kind]
candidates.append(pack / kind_name)
if kind == "gt":
candidates.append(ROOT / "docs" / "delhi_eval" / "labels" / f"{pair_id}.png")
for path in candidates:
if path.is_file():
return path
return None
def discover_synthetic_dir() -> Path | None:
for path in SYNTHETIC_CANDIDATES:
if (path / "before").is_dir() and (path / "after").is_dir() and (path / "mask").is_dir():
return path
return None
def _summarize(rows: list[dict]) -> dict | None:
if not rows:
return None
tp = sum(r["tp"] for r in rows)
fp = sum(r["fp"] for r in rows)
fn = sum(r["fn"] for r in rows)
prec = tp / (tp + fp) if (tp + fp) else 1.0
rec = tp / (tp + fn) if (tp + fn) else 1.0
f1 = (2 * prec * rec / (prec + rec)) if (prec + rec) else 1.0
iou = tp / (tp + fp + fn) if (tp + fp + fn) else 1.0
return {
"n": len(rows),
"mean_f1": round(float(np.mean([r["f1"] for r in rows])), 4),
"mean_precision": round(float(np.mean([r["precision"] for r in rows])), 4),
"mean_recall": round(float(np.mean([r["recall"] for r in rows])), 4),
"mean_iou": round(float(np.mean([r["iou"] for r in rows])), 4),
"mean_pixel_acc": round(float(np.mean([r["pixelAccuracy"] for r in rows])), 4),
"micro_f1": round(f1, 4),
"micro_precision": round(prec, 4),
"micro_recall": round(rec, 4),
"micro_iou": round(iou, 4),
"pair_ids": [r["pair_id"] for r in rows],
}
def env_check(weights: Path) -> dict:
import torch
from app.model_inference import get_model_status, predict_change_mask, preload_model
info = {
"torch": torch.__version__,
"cuda_available": bool(torch.cuda.is_available()),
"cuda_version": torch.version.cuda,
"device_name": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None,
"capability": list(torch.cuda.get_device_capability(0)) if torch.cuda.is_available() else None,
"weights": str(weights),
"weights_exist": weights.is_dir() and (weights / "model.safetensors").is_file(),
}
rng = np.random.default_rng(0)
a = rng.integers(40, 200, (256, 256, 3), dtype=np.uint8)
b = a.copy()
b[80:140, 90:160] = [210, 200, 190]
t0 = time.time()
ok = bool(preload_model())
mask, score = predict_change_mask(a, b)
status = get_model_status()
info.update({
"preload_ok": ok,
"forward_s": round(time.time() - t0, 2),
"mask_shape": list(mask.shape),
"score_min": round(float(np.min(score)), 4),
"score_max": round(float(np.max(score)), 4),
"pred_change_pct": round(100.0 * float((mask > 127).mean()), 3),
"adaptformer": {
"available": status.get("available"),
"loadedFrom": status.get("loadedFrom"),
"device": status.get("device"),
"detectionMode": status.get("detectionMode"),
"calibratedThreshold": status.get("calibratedThreshold"),
"error": status.get("error"),
},
})
return info
def pair_conditions(pair: dict, gt_frac: float, ncc: float | None) -> list[str]:
types = {str(t).lower() for t in (pair.get("change_types") or [])}
pid = pair["pair_id"]
notes = (pair.get("notes") or "").lower()
conds: list[str] = []
if "vegetation" in types:
conds.append("vegetation")
if pid in ROOF_PAIRS:
conds.append("roof")
if pid in HELD_OUT_EMPTY or "seasonal" in notes or "crop-texture" in notes:
conds.append("lighting")
if pid in ROOF_PAIRS or ncc is not None:
conds.append("alignment")
if gt_frac < 0.001 and pid in HELD_OUT_EMPTY:
# empty held-out pairs are the real-imagery lighting/seasonal FP gate
if "lighting" not in conds:
conds.append("lighting")
return conds
def eval_one(pair: dict, method: str) -> dict:
from app.detection_engine import run_detection
pid = pair["pair_id"]
before_p = _resolve_pair_file(pair.get("before_path") or "", pid, "before")
after_p = _resolve_pair_file(pair.get("after_path") or "", pid, "after")
gt_p = _resolve_pair_file(pair.get("gt_mask") or "", pid, "gt")
if not before_p or not after_p or not gt_p:
return {"pair_id": pid, "error": "missing_files",
"before": str(before_p), "after": str(after_p), "gt": str(gt_p)}
before = _load_rgb(before_p)
after = _load_rgb(after_p)
gt = _load_label(gt_p)
meta_path = ROOT / "docs" / "delhi_eval" / "dda_labeling" / pid / "meta.json"
ncc = None
aligned = None
if meta_path.is_file():
meta = json.loads(meta_path.read_text(encoding="utf-8"))
ncc = meta.get("ncc")
aligned = meta.get("aligned")
tif_b = str(before_p) if before_p.suffix.lower() in {".tif", ".tiff"} else None
tif_a = str(after_p) if after_p.suffix.lower() in {".tif", ".tiff"} else None
t0 = time.time()
mask, _img, stats, regions = run_detection(
Image.fromarray(before), Image.fromarray(after),
method=method,
enable_registration=True, enable_normalization=True,
detection_sensitivity=0.5,
before_path=tif_b, after_path=tif_a,
)
if mask.shape != gt.shape:
import cv2
mask = cv2.resize(mask, (gt.shape[1], gt.shape[0]), interpolation=cv2.INTER_NEAREST)
m = binary_metrics(mask, gt)
counts = m["counts"]
gt_frac = float((gt > 127).mean()) if gt.max() > 1 else float((gt > 0).mean())
params = stats.get("params") or {}
row = {
"pair_id": pid,
"f1": m["f1"],
"precision": m["precision"],
"recall": m["recall"],
"iou": m["iou"],
"pixelAccuracy": m["pixelAccuracy"],
"falsePositiveRate": m["falsePositiveRate"],
"tp": counts["tp"], "fp": counts["fp"], "fn": counts["fn"], "tn": counts["tn"],
"gt_change_frac": round(gt_frac, 6),
"pred_change_pct": round(float(stats.get("change_percentage") or 0.0), 4),
"elapsed_s": round(time.time() - t0, 2),
"n_regions": len(regions or []),
"change_types": list(pair.get("change_types") or []),
"split": pair.get("_split", "unspecified"),
"ncc": ncc,
"aligned": aligned,
"registration_ok": params.get("registration_ok"),
"alignment_warning": bool(stats.get("alignment_warning") or params.get("alignment_warning")),
"conditions": pair_conditions(pair, gt_frac, ncc),
}
return row
def _base_scene(size=384, seed=0):
rng = np.random.default_rng(seed)
img = rng.integers(40, 200, (size, size, 3), dtype=np.uint8)
img[:, size // 3: size // 3 + 6] = [90, 90, 90]
img[size // 2: size // 2 + 6, :] = [110, 100, 80]
return img
def synthetic_condition_cases():
size = 384
before = _base_scene(size, seed=1)
after = before.copy()
gt = np.zeros((size, size), dtype=np.uint8)
for (x, y, w, h) in [(60, 70, 50, 40), (220, 90, 60, 55), (150, 250, 70, 45)]:
after[y:y + h, x:x + w] = [205, 200, 190]
gt[y:y + h, x:x + w] = 255
yield before, after, gt, "inserted_buildings", "roof"
before = _base_scene(size, seed=2)
after = np.clip(before.astype(np.float32) * 1.18 + 12, 0, 255).astype(np.uint8)
gt = np.zeros((size, size), dtype=np.uint8)
yield before, after, gt, "brightness_only", "lighting"
before = _base_scene(size, seed=3)
shifted = np.roll(np.roll(before, 6, axis=0), 4, axis=1)
after = shifted.copy()
gt = np.zeros((size, size), dtype=np.uint8)
x, y, w, h = 180, 160, 80, 60
after[y:y + h, x:x + w] = [210, 60, 60]
gt[y:y + h, x:x + w] = 255
yield before, after, gt, "misaligned_change", "alignment"
before = _base_scene(size, seed=5)
after = before.copy()
gt = np.zeros((size, size), dtype=np.uint8)
before[40:140, 50:180] = [35, 120, 45]
after[40:140, 50:180] = [150, 130, 90]
gt[40:140, 50:180] = 255
yield before, after, gt, "vegetation_cleared", "vegetation"
def eval_synthetic_conditions(method: str) -> list[dict]:
from app.detection_engine import run_detection
rows = []
for before, after, gt, name, condition in synthetic_condition_cases():
t0 = time.time()
mask, _img, stats, regions = run_detection(
Image.fromarray(before), Image.fromarray(after),
method=method,
enable_registration=True, enable_normalization=True,
detection_sensitivity=0.5,
)
if mask.shape != gt.shape:
import cv2
mask = cv2.resize(mask, (gt.shape[1], gt.shape[0]), interpolation=cv2.INTER_NEAREST)
m = binary_metrics(mask, gt)
counts = m["counts"]
rows.append({
"pair_id": name,
"condition": condition,
"f1": m["f1"], "precision": m["precision"], "recall": m["recall"],
"iou": m["iou"], "pixelAccuracy": m["pixelAccuracy"],
"falsePositiveRate": m["falsePositiveRate"],
"tp": counts["tp"], "fp": counts["fp"], "fn": counts["fn"], "tn": counts["tn"],
"elapsed_s": round(time.time() - t0, 2),
"n_regions": len(regions or []),
"pred_change_pct": round(float(stats.get("change_percentage") or 0.0), 4),
})
print(f" [synth/{condition}] {name}: F1={m['f1']:.3f} IoU={m['iou']:.3f}", flush=True)
return rows
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--weights", default=str(DEFAULT_WEIGHTS))
parser.add_argument("--method", default="AI-Based Deep Learning")
parser.add_argument("--out", default="runs/eval_seed_conditions/report.json")
args = parser.parse_args()
weights = Path(args.weights)
if not weights.is_absolute():
weights = ROOT / weights
os.environ["ADAPTFORMER_WEIGHTS"] = str(weights)
thr_path = weights / "threshold.json"
if thr_path.is_file():
sidecar = json.loads(thr_path.read_text(encoding="utf-8"))
os.environ["ADAPTFORMER_THRESHOLD"] = str(sidecar["threshold"])
print("=== 1. Environment check ===", flush=True)
env = env_check(weights)
print(json.dumps(env, indent=2), flush=True)
if not env.get("cuda_available"):
print("WARNING: CUDA not available — eval will be slow/CPU", flush=True)
if not env.get("adaptformer", {}).get("available"):
raise SystemExit("AdaptFormer failed to load; aborting eval")
synth_dir = discover_synthetic_dir()
print("\n=== 2. Synthetic dataset ===", flush=True)
print(f" found: {synth_dir}" if synth_dir else " NOT FOUND — skip fine-tune", flush=True)
print("\n=== 3. Real seed evaluation ===", flush=True)
manifest = load_manifest()
split_path = ROOT / "data" / "delhi_cd" / "split.json"
split_ids = {"train": set(), "val": set(), "test": set()}
if split_path.is_file():
split = json.loads(split_path.read_text(encoding="utf-8"))
for key in ("train", "val", "test"):
split_ids[key] = set(split.get(key) or [])
test_ids = set(split_ids["test"])
if (ROOT / "data" / "delhi_cd" / "test" / "manifest.json").is_file():
test_man = json.loads((ROOT / "data" / "delhi_cd" / "test" / "manifest.json").read_text(encoding="utf-8"))
test_ids = {p["pair_id"] for p in test_man.get("pairs", [])}
rows = []
skipped = []
for pair in manifest.get("pairs", []):
pid = pair.get("pair_id") or ""
if pid in split_ids["train"]:
pair["_split"] = "train"
elif pid in split_ids["val"]:
pair["_split"] = "val"
elif pid in test_ids:
pair["_split"] = "test"
elif pid in HELD_OUT_EMPTY:
pair["_split"] = "heldout_empty"
else:
pair["_split"] = "other"
print(f" eval {pid} ...", flush=True)
row = eval_one(pair, args.method)
if row.get("error"):
skipped.append(row)
print(f" SKIP {row['error']}", flush=True)
continue
rows.append(row)
print(
f" F1={row['f1']:.3f} P={row['precision']:.3f} R={row['recall']:.3f} "
f"IoU={row['iou']:.3f} cond={row['conditions']} split={row['split']}",
flush=True,
)
print("\n=== 3b. Synthetic condition probes ===", flush=True)
synth_rows = eval_synthetic_conditions(args.method)
change_pos = [r for r in rows if r["gt_change_frac"] >= 0.001]
empty = [r for r in rows if r["gt_change_frac"] < 0.001]
buckets = {}
for name in ("vegetation", "roof", "lighting", "alignment"):
buckets[name] = _summarize([r for r in rows if name in r["conditions"]])
buckets[f"{name}_change_positive"] = _summarize(
[r for r in rows if name in r["conditions"] and r["gt_change_frac"] >= 0.001]
)
report = {
"created_unix": time.time(),
"weights": str(weights),
"threshold": os.environ.get("ADAPTFORMER_THRESHOLD"),
"method": args.method,
"synthetic_dir": str(synth_dir) if synth_dir else None,
"env": env,
"overall_all_labeled": _summarize(rows),
"overall_change_positive": _summarize(change_pos),
"overall_empty_gt": _summarize(empty),
"frozen_test": _summarize([r for r in rows if r["pair_id"] in test_ids]),
"heldout_empty_fp": _summarize([r for r in rows if r["pair_id"] in HELD_OUT_EMPTY]),
"conditions": buckets,
"synthetic_probes": synth_rows,
"skipped": skipped,
"pairs": rows,
}
out = ROOT / args.out
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(report, indent=2), encoding="utf-8")
print("\n=== Summary ===", flush=True)
for key in ("overall_all_labeled", "overall_change_positive", "frozen_test", "heldout_empty_fp"):
s = report[key]
if not s:
print(f" {key}: n/a", flush=True)
continue
print(
f" {key}: n={s['n']} mean_F1={s['mean_f1']:.4f} P={s['mean_precision']:.4f} "
f"R={s['mean_recall']:.4f} IoU={s['mean_iou']:.4f} micro_F1={s['micro_f1']:.4f}",
flush=True,
)
for name in ("vegetation", "roof", "lighting", "alignment"):
s = buckets.get(name)
print(f" {name}: {s}" if s else f" {name}: n/a", flush=True)
print(f"Wrote {out}", flush=True)
print("EVAL_DONE", flush=True)
if __name__ == "__main__":
main()