File size: 7,220 Bytes
d70361b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""
Day 7 (Uday): compare fine-tuned AdaptFormer vs pretrained hub weights on
held-out Delhi pairs (docs/delhi_eval/test_split.json) and data/delhi_cd/test.

Usage:
    python scripts/evaluate_finetuned_vs_baseline.py
    python scripts/evaluate_finetuned_vs_baseline.py --methods AI
"""
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))

from app.detection_engine import run_detection  # noqa: E402
from app.evaluation.delhi_eval import iter_delhi_pairs  # noqa: E402
from app.evaluation.metrics import binary_metrics  # noqa: E402


def _load_held_out_ids() -> list[str]:
    path = ROOT / "docs" / "delhi_eval" / "test_split.json"
    if path.is_file():
        data = json.loads(path.read_text(encoding="utf-8"))
        return list(data.get("held_out_test_set", {}).get("pair_ids", []))
    # fallback: data/delhi_cd/test
    man = ROOT / "data" / "delhi_cd" / "test" / "manifest.json"
    if man.is_file():
        return [p["pair_id"] for p in json.loads(man.read_text())["pairs"]]
    return []


def _eval_weights(label: str, weights: str | None, pairs: list, method: str) -> dict:
    # Clear cached model so weights switch takes effect
    import app.model_inference as mi
    mi._MODEL = None
    mi._PROCESSOR = None
    mi._AVAILABLE = None
    mi._LOAD_FAILED = False
    mi._LOAD_ERROR = None
    mi._LOADED_FROM = None

    if weights:
        os.environ["ADAPTFORMER_WEIGHTS"] = weights
    else:
        os.environ.pop("ADAPTFORMER_WEIGHTS", None)
        # Force hub by pointing at a non-existent local so auto-dir is skipped
        # unless user has models/adaptformer_delhi — temporarily rename env.
        os.environ["ADAPTFORMER_WEIGHTS"] = "deepang/adaptformer-LEVIR-CD"

    f1s, ious = [], []
    per = {}
    t0 = time.time()
    for before, after, gt, pair_id, bp, ap in pairs:
        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=bp, after_path=ap,
        )
        if gt is None:
            # empty-GT FP test: IoU=1 if pred empty else 0
            pred_pos = int(np.sum(mask > 127))
            m = {
                "f1": 1.0 if pred_pos == 0 else 0.0,
                "iou": 1.0 if pred_pos == 0 else 0.0,
                "precision": 1.0 if pred_pos == 0 else 0.0,
                "recall": 1.0 if pred_pos == 0 else 0.0,
            }
        else:
            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)
        f1s.append(m["f1"])
        ious.append(m["iou"])
        per[pair_id] = {"f1": m["f1"], "iou": m["iou"],
                        "changePct": round(stats.get("change_percentage", 0), 3)}
        print(f"  [{label}] {pair_id}: F1={m['f1']:.3f} IoU={m['iou']:.3f}")

    return {
        "label": label,
        "weights": weights or "hub:deepang/adaptformer-LEVIR-CD",
        "n": len(pairs),
        "mean_f1": round(float(np.mean(f1s)), 4) if f1s else 0.0,
        "mean_iou": round(float(np.mean(ious)), 4) if ious else 0.0,
        "seconds": round(time.time() - t0, 1),
        "pairs": per,
    }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--manifest", default="docs/delhi_eval/manifest.json")
    parser.add_argument("--finetuned", default="models/adaptformer_delhi/best")
    parser.add_argument("--method", default="AI-Based Deep Learning")
    parser.add_argument("--out", default="runs/day7_heldout_comparison.json")
    parser.add_argument("--also-feature", action="store_true",
                        help="also report Feature-Based on the same held-out set")
    args = parser.parse_args()

    held_ids = set(_load_held_out_ids())
    if not held_ids:
        raise SystemExit("No held-out pair IDs found in test_split.json")

    all_pairs = list(iter_delhi_pairs(args.manifest, require_gt=False))
    pairs = [p for p in all_pairs if p[3] in held_ids]
    # Prefer labeled if available; empty GT still included for FP test
    if not pairs:
        raise SystemExit(f"No held-out pairs loaded from {args.manifest}")

    print(f"Held-out pairs: {len(pairs)} -> {[p[3] for p in pairs]}")

    ft_path = ROOT / args.finetuned
    rows = []
    rows.append(_eval_weights("pretrained_hub", None, pairs, args.method))
    if ft_path.is_dir():
        rows.append(_eval_weights("finetuned_delhi", str(ft_path), pairs, args.method))
    else:
        print(f"WARNING: {ft_path} missing — run scripts/export_adaptformer_delhi.py first")

    if args.also_feature:
        # Feature-Based doesn't use AdaptFormer weights
        import app.model_inference as mi
        mi._MODEL = None
        f1s, ious, per = [], [], {}
        for before, after, gt, pair_id, bp, ap in pairs:
            mask, _img, stats, _ = run_detection(
                Image.fromarray(before), Image.fromarray(after),
                method="Feature-Based",
                enable_registration=True, enable_normalization=True,
                detection_sensitivity=0.5, before_path=bp, after_path=ap,
            )
            if gt is None:
                pred_pos = int(np.sum(mask > 127))
                m = {"f1": 1.0 if pred_pos == 0 else 0.0,
                     "iou": 1.0 if pred_pos == 0 else 0.0}
            else:
                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)
            f1s.append(m["f1"]); ious.append(m["iou"])
            per[pair_id] = m
        rows.append({
            "label": "feature_based",
            "weights": "n/a",
            "n": len(pairs),
            "mean_f1": round(float(np.mean(f1s)), 4),
            "mean_iou": round(float(np.mean(ious)), 4),
            "pairs": per,
        })

    report = {
        "held_out_ids": sorted(held_ids),
        "method": args.method,
        "comparison": rows,
        "winner": max(rows, key=lambda r: r["mean_f1"])["label"] if rows else None,
        "rca_notes": [
            "Domain mismatch (LEVIR→Delhi) is Critical #2 — fine-tune may still underperform "
            "on empty-GT FP pairs; Feature-Based remains strong for precision gates.",
            "GeoTIFF fullres_tiled auto + veg/registration fixes applied from RCA PDF.",
        ],
    }
    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=== Day 7 comparison ===")
    for r in rows:
        print(f"  {r['label']:20s} mean_F1={r['mean_f1']:.4f} mean_IoU={r['mean_iou']:.4f} n={r['n']}")
    print(f"Wrote {out}")


if __name__ == "__main__":
    main()