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,
    }