Spaces:
Running
Running
| """ | |
| Prepare hand-labeling assets for the DDA pair Grid_54.tif vs H43X2E1.tif. | |
| Creates a paint/QGIS-friendly pack under ``docs/delhi_eval/dda_labeling/``: | |
| - before.png / after.png (downscaled RGB, default max_side=4096) | |
| - seed_mask.png (current AdaptFormer+recovery detection as a draft) | |
| - gt_mask_blank.png (empty mask — paint change as white on black) | |
| - LABELING.md (how to finish the label and ingest it) | |
| - meta.json | |
| Also registers the pair in ``docs/delhi_eval/manifest.json`` as ``dda_grid54_h43x2e1``. | |
| Usage (from repo root): | |
| python scripts/prepare_dda_gt_labeling.py | |
| python scripts/prepare_dda_gt_labeling.py --max-side 3072 --skip-seed | |
| """ | |
| 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)) | |
| BEFORE = ROOT / "data/library_sources/central_delhi/Images/Grid_54.tif" | |
| AFTER = ROOT / "data/library_sources/central_delhi/Images/H43X2E1.tif" | |
| OUT_DIR = ROOT / "docs/delhi_eval/dda_labeling/dda_grid54_h43x2e1" | |
| PAIR_ID = "dda_grid54_h43x2e1" | |
| MANIFEST = ROOT / "docs/delhi_eval/manifest.json" | |
| LABEL_PATH = ROOT / "docs/delhi_eval/labels" / f"{PAIR_ID}.png" | |
| def _load_rgb(path: Path, max_side: int) -> np.ndarray: | |
| from app.dda.geotiff_io import load_rgb_pil | |
| return np.array(load_rgb_pil(path, max_side=max_side).convert("RGB")) | |
| def _register_manifest(preview_w: int, preview_h: int) -> None: | |
| data = json.loads(MANIFEST.read_text(encoding="utf-8")) | |
| pairs = data.get("pairs") or [] | |
| entry = { | |
| "pair_id": PAIR_ID, | |
| "before_path": str(BEFORE.relative_to(ROOT)).replace("\\", "/"), | |
| "after_path": str(AFTER.relative_to(ROOT)).replace("\\", "/"), | |
| "date_before": None, | |
| "date_after": None, | |
| "gsd": 0.03, | |
| "zone": "Central Delhi / Grid 54", | |
| "change_types": ["building", "road", "vegetation", "mixed_gsd"], | |
| "gt_mask": None, | |
| "notes": ( | |
| "DDA production pair (Grid_54 vs H43X2E1). Hand-label in " | |
| f"docs/delhi_eval/dda_labeling/{PAIR_ID}/ then run " | |
| "scripts/ingest_dda_gt_label.py" | |
| ), | |
| "label_preview_size": [preview_w, preview_h], | |
| } | |
| # replace if exists | |
| pairs = [p for p in pairs if p.get("pair_id") != PAIR_ID] | |
| pairs.append(entry) | |
| data["pairs"] = pairs | |
| MANIFEST.write_text(json.dumps(data, indent=2), encoding="utf-8") | |
| print(f"Registered {PAIR_ID} in manifest ({len(pairs)} pairs total)") | |
| def _write_guide(out: Path) -> None: | |
| text = f"""# Labeling pack: Grid_54 vs H43X2E1 | |
| ## Goal | |
| Hand-draw **real permanent ground change** (new buildings, demolition, roads). | |
| Do **not** mark cars, shadows, seasonal tree canopy, or illumination shifts. | |
| ## Files | |
| | File | Use | | |
| |---|---| | |
| | `before.png` | earlier date | | |
| | `after.png` | later date | | |
| | `seed_mask.png` | draft from current detector (white=change) — **edit this** | | |
| | `gt_mask_blank.png` | empty alternative if you prefer starting from scratch | | |
| ## Paint / Photoshop | |
| 1. Open `after.png` and `seed_mask.png`. | |
| 2. Edit the mask so **white (255) = true change**, **black (0) = no change**. | |
| 3. Save as `gt_mask.png` in this folder (single-channel or RGB white/black). | |
| ## QGIS | |
| 1. Load `before.png` / `after.png` as rasters (same extent). | |
| 2. Digitize change polygons → rasterize to same size as the PNGs → export | |
| binary GeoTIFF/PNG as `gt_mask.png`. | |
| ## Ingest into the eval set | |
| ```bash | |
| python scripts/ingest_dda_gt_label.py | |
| ``` | |
| This copies `gt_mask.png` → `docs/delhi_eval/labels/{PAIR_ID}.png` and updates | |
| the manifest. Calibration / fine-tune scripts will then use it. | |
| ## Notes | |
| - Preview size is downscaled from native ~25k×28k for tractable labeling. | |
| - After ingest, re-run calibration: | |
| `python scripts/v3_error_analysis_and_thr_sweep.py` | |
| `python scripts/grid_search_calibration.py --methods "AI-Based Deep Learning" --sensitivities 0.4,0.5,0.6 --pair-ids {PAIR_ID}` | |
| """ | |
| (out / "LABELING.md").write_text(text, encoding="utf-8") | |
| def main() -> int: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--max-side", type=int, default=4096) | |
| ap.add_argument("--skip-seed", action="store_true", | |
| help="Skip AdaptFormer seed mask (faster pack-only)") | |
| args = ap.parse_args() | |
| if not BEFORE.is_file() or not AFTER.is_file(): | |
| print("Missing Grid_54.tif or H43X2E1.tif under data/library_sources/...") | |
| return 1 | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| print(f"Loading pair at max_side={args.max_side} ...") | |
| t0 = time.time() | |
| before = _load_rgb(BEFORE, args.max_side) | |
| after = _load_rgb(AFTER, args.max_side) | |
| print(f" loaded {before.shape} in {time.time()-t0:.1f}s") | |
| Image.fromarray(before).save(OUT_DIR / "before.png") | |
| Image.fromarray(after).save(OUT_DIR / "after.png") | |
| blank = np.zeros(before.shape[:2], dtype=np.uint8) | |
| Image.fromarray(blank).save(OUT_DIR / "gt_mask_blank.png") | |
| seed_px = 0 | |
| if not args.skip_seed: | |
| print("Running detection for seed mask (this can take several minutes)...") | |
| from dotenv import load_dotenv | |
| load_dotenv(ROOT / ".env", override=True) | |
| from app.detection_engine import run_detection | |
| mask, _vis, stats, regions = run_detection( | |
| Image.fromarray(before), | |
| Image.fromarray(after), | |
| method="AI-Based Deep Learning", | |
| enable_registration=True, | |
| enable_normalization=True, | |
| detection_sensitivity=0.5, | |
| max_size=args.max_side, | |
| before_path=str(BEFORE), | |
| after_path=str(AFTER), | |
| ) | |
| seed = np.array(mask) | |
| if seed.ndim == 3: | |
| seed = seed[:, :, 0] | |
| Image.fromarray(seed.astype(np.uint8)).save(OUT_DIR / "seed_mask.png") | |
| seed_px = int((seed > 127).sum()) | |
| print(f" seed change%={stats.get('change_percentage')} regions={len(regions)} px={seed_px}") | |
| else: | |
| Image.fromarray(blank).save(OUT_DIR / "seed_mask.png") | |
| meta = { | |
| "pair_id": PAIR_ID, | |
| "before": str(BEFORE), | |
| "after": str(AFTER), | |
| "preview_shape": list(before.shape[:2]), | |
| "max_side": args.max_side, | |
| "seed_changed_px": seed_px, | |
| "created_unix": time.time(), | |
| } | |
| (OUT_DIR / "meta.json").write_text(json.dumps(meta, indent=2), encoding="utf-8") | |
| _write_guide(OUT_DIR) | |
| _register_manifest(before.shape[1], before.shape[0]) | |
| print(f"Labeling pack ready: {OUT_DIR}") | |
| print("Next: edit seed_mask.png → save as gt_mask.png → run scripts/ingest_dda_gt_label.py") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |