Spaces:
Running
Running
File size: 6,196 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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """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,
}
|