satdetect-dev / scripts /build_whu_reference_pairs.py
coderuday21's picture
Cursor
Deploy satdetect-dev with Priyanka audit/exception logs.
d70361b
Raw
History Blame Contribute Delete
4.88 kB
"""
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()