File size: 4,881 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
"""
One-off: turn WHU 'Satellite Dataset I' single-time building tiles into
semi-synthetic before/after change pairs, so the detection harness can be
smoke-tested against real satellite imagery + real building footprints
instead of purely synthetic geometry.

NOT a Delhi substitute — this is a real-data *pipeline* test set only (see
docs/whu_reference/README.md). Each pair is built from ONE real acquisition:
    after  = the original tile (buildings present)
    before = the same tile with building-mask pixels inpainted away
    gt     = the real building-footprint label (== the "change" region)

Usage:
    python scripts/build_whu_reference_pairs.py --src "data/whu_reference/extracted/Satellite dataset Ⅰ (global cities)" \
        --out data/whu_reference/pairs --count 5
"""
import argparse
import glob
import json
from pathlib import Path

import cv2
import numpy as np
from PIL import Image

ROOT = Path(__file__).resolve().parent.parent


def _building_pct(label: np.ndarray) -> float:
    return float(np.mean(label > 127)) * 100


def main():
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--src", default="", help="path to the extracted 'Satellite dataset I' folder; "
                        "auto-detected under data/whu_reference/extracted/ if omitted (zip entry names use "
                        "a Roman numeral that some unzip tools mangle)")
    parser.add_argument("--out", default="data/whu_reference/pairs")
    parser.add_argument("--count", type=int, default=5)
    parser.add_argument("--min-pct", type=float, default=3.0, help="min building coverage %% to consider a tile")
    parser.add_argument("--max-pct", type=float, default=25.0, help="max building coverage %% to consider a tile")
    args = parser.parse_args()

    if args.src:
        src = Path(args.src)
    else:
        extracted_root = ROOT / "data" / "whu_reference" / "extracted"
        subdirs = [p for p in extracted_root.iterdir() if p.is_dir()] if extracted_root.exists() else []
        if len(subdirs) != 1:
            raise SystemExit(f"Expected exactly one folder under {extracted_root}, found {len(subdirs)}. "
                              f"Pass --src explicitly.")
        src = subdirs[0]
        print(f"Auto-detected source: {src}")
    out_dir = ROOT / args.out
    out_dir.mkdir(parents=True, exist_ok=True)

    image_paths = sorted(glob.glob(str(src / "image" / "*.tif")))
    candidates = []
    for img_path in image_paths:
        tile_id = Path(img_path).stem
        label_path = src / "label" / f"{tile_id}.tif"
        if not label_path.exists():
            continue
        label = np.array(Image.open(label_path).convert("L"))
        pct = _building_pct(label)
        if args.min_pct <= pct <= args.max_pct:
            candidates.append((tile_id, img_path, str(label_path), pct))

    if not candidates:
        raise SystemExit(f"No tiles with building coverage in [{args.min_pct}, {args.max_pct}]%")

    candidates.sort(key=lambda c: c[3])  # spread across coverage levels
    step = max(1, len(candidates) // args.count)
    selected = candidates[::step][: args.count]

    manifest = {"pairs": []}
    for tile_id, img_path, label_path, pct in selected:
        image = np.array(Image.open(img_path).convert("RGB"))
        label = np.array(Image.open(label_path).convert("L"))
        building_mask = (label > 127).astype(np.uint8) * 255

        # Dilate slightly so inpainting also erases building shadows/edges.
        kernel = np.ones((7, 7), np.uint8)
        inpaint_mask = cv2.dilate(building_mask, kernel, iterations=1)
        before = cv2.inpaint(image, inpaint_mask, inpaintRadius=7, flags=cv2.INPAINT_TELEA)

        pair_dir = out_dir / tile_id
        pair_dir.mkdir(parents=True, exist_ok=True)
        Image.fromarray(before).save(pair_dir / "before.png")
        Image.fromarray(image).save(pair_dir / "after.png")
        Image.fromarray(building_mask).save(pair_dir / "gt.png")

        manifest["pairs"].append({
            "pair_id": f"whu_{tile_id}",
            "before_path": str((pair_dir / "before.png").relative_to(ROOT)),
            "after_path": str((pair_dir / "after.png").relative_to(ROOT)),
            "gt_mask": str((pair_dir / "gt.png").relative_to(ROOT)),
            "building_pct": round(pct, 2),
            "source": "WHU Satellite Dataset I (global cities) — semi-synthetic pair, real imagery + real building footprint, inpainted 'before'",
        })
        print(f"  {tile_id}: building_pct={pct:.1f}%  -> {pair_dir.relative_to(ROOT)}")

    manifest_path = out_dir / "manifest.json"
    manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
    print(f"\nWrote {len(manifest['pairs'])} pair(s) to {manifest_path.relative_to(ROOT)}")


if __name__ == "__main__":
    main()