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