satdetect-dev / app /dda /training_pack.py
coderuday21's picture
Cursor
Deploy satdetect-dev with Priyanka audit/exception logs.
d70361b
Raw
History Blame Contribute Delete
6.2 kB
"""Write a paint-ready GT-labeling pack for a before/after pair (optionally an ROI).
Produces ``docs/delhi_eval/dda_labeling/<pair_id>/`` with:
- before.png / after.png (RGB previews)
- seed_mask.png (draft change mask to edit; blank if none)
- gt_mask_blank.png (empty mask to paint from scratch)
- LABELING.md / meta.json
and registers the pair in ``docs/delhi_eval/manifest.json``.
Shared by the offline ``scripts/prepare_dda_gt_labeling.py`` and the
``POST /api/dda/training/pack`` endpoint so both write identical packs that
``scripts/ingest_dda_gt_label.py --pair-id <id>`` can pull back in.
"""
from __future__ import annotations
import hashlib
import json
import re
import time
from pathlib import Path
from typing import Optional
import cv2
import numpy as np
from PIL import Image
_ROOT = Path(__file__).resolve().parent.parent.parent
LABELING_DIR = _ROOT / "docs" / "delhi_eval" / "dda_labeling"
MANIFEST = _ROOT / "docs" / "delhi_eval" / "manifest.json"
def make_pair_id(before_name: str, after_name: str, roi: Optional[dict] = None) -> str:
"""Stable, filesystem-safe pair id from the two filenames (+ ROI hash)."""
base = f"{Path(before_name).stem}_{Path(after_name).stem}".lower()
base = re.sub(r"[^a-z0-9]+", "_", base).strip("_")
if roi:
tag = hashlib.md5(json.dumps(roi, sort_keys=True).encode()).hexdigest()[:6]
return f"dda_{base}_roi_{tag}"
return f"dda_{base}"
def rasterize_regions(regions, src_w: int, src_h: int, dst_w: int, dst_h: int) -> np.ndarray:
"""Coarse seed mask: fill each region's bbox, scaled from the detection grid
(``src_w`` x ``src_h``) to the pack preview grid (``dst_w`` x ``dst_h``)."""
mask = np.zeros((dst_h, dst_w), np.uint8)
sx = dst_w / max(1, src_w)
sy = dst_h / max(1, src_h)
for r in regions or []:
bb = r.get("bbox") or {}
x = int(round(float(bb.get("x", 0)) * sx))
y = int(round(float(bb.get("y", 0)) * sy))
w = int(round(float(bb.get("w", 0)) * sx))
h = int(round(float(bb.get("h", 0)) * sy))
if w > 0 and h > 0:
mask[max(0, y):min(dst_h, y + h), max(0, x):min(dst_w, x + w)] = 255
return mask
def _guide_text(pair_id: str) -> str:
return f"""# Labeling pack: {pair_id}
## 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 |
## Finish the label
1. Edit the mask so **white (255) = true change**, **black (0) = no change**.
2. Save as `gt_mask.png` in this folder (single-channel or RGB white/black).
## Ingest into the eval set
```bash
python scripts/ingest_dda_gt_label.py --pair-id {pair_id}
python scripts/build_delhi_cd_splits.py
```
"""
def _register_manifest(pair_id, before_path, after_path, w, h, *,
roi=None, zone="", gsd=None, change_types=None) -> int:
"""Add/replace the pair in the eval manifest. Returns total pair count."""
MANIFEST.parent.mkdir(parents=True, exist_ok=True)
if MANIFEST.is_file():
data = json.loads(MANIFEST.read_text(encoding="utf-8"))
else:
data = {"pairs": []}
def _rel(p):
try:
return str(Path(p).resolve().relative_to(_ROOT)).replace("\\", "/")
except Exception:
return str(p).replace("\\", "/")
pairs = [p for p in (data.get("pairs") or []) if p.get("pair_id") != pair_id]
pairs.append({
"pair_id": pair_id,
"before_path": _rel(before_path),
"after_path": _rel(after_path),
"date_before": None,
"date_after": None,
"gsd": gsd,
"zone": zone or "",
"change_types": change_types or ["building", "road", "vegetation", "mixed"],
"gt_mask": None,
"roi": roi,
"notes": (
f"Labeling pack. Paint docs/delhi_eval/dda_labeling/{pair_id}/gt_mask.png "
f"then run scripts/ingest_dda_gt_label.py --pair-id {pair_id}"
),
"label_preview_size": [w, h],
})
data["pairs"] = pairs
MANIFEST.write_text(json.dumps(data, indent=2), encoding="utf-8")
return len(pairs)
def write_labeling_pack(before_rgb, after_rgb, seed_mask, *, pair_id,
before_path, after_path, roi=None, zone="",
gsd=None, change_types=None) -> dict:
"""Write a full labeling pack + manifest entry. Returns a summary dict."""
out = LABELING_DIR / pair_id
out.mkdir(parents=True, exist_ok=True)
h, w = before_rgb.shape[:2]
Image.fromarray(before_rgb).save(out / "before.png")
Image.fromarray(after_rgb).save(out / "after.png")
blank = np.zeros((h, w), np.uint8)
Image.fromarray(blank).save(out / "gt_mask_blank.png")
if seed_mask is None:
seed = blank
else:
seed = np.asarray(seed_mask)
if seed.ndim == 3:
seed = seed[:, :, 0]
if seed.shape[:2] != (h, w):
seed = cv2.resize(seed, (w, h), interpolation=cv2.INTER_NEAREST)
seed = (seed > 127).astype(np.uint8) * 255
Image.fromarray(seed).save(out / "seed_mask.png")
seed_px = int((seed > 127).sum())
meta = {
"pair_id": pair_id,
"before": str(before_path),
"after": str(after_path),
"preview_shape": [h, w],
"roi": roi,
"seed_changed_px": seed_px,
"created_unix": time.time(),
}
(out / "meta.json").write_text(json.dumps(meta, indent=2), encoding="utf-8")
(out / "LABELING.md").write_text(_guide_text(pair_id), encoding="utf-8")
n_pairs = _register_manifest(
pair_id, before_path, after_path, w, h,
roi=roi, zone=zone, gsd=gsd, change_types=change_types)
return {
"pairId": pair_id,
"dir": str(out.relative_to(_ROOT)).replace("\\", "/"),
"previewSize": [w, h],
"seedChangedPx": seed_px,
"manifestPairs": n_pairs,
}