geospatial1 / backend /gen_sample_pairs.py
Umar666's picture
Deploy curated change-detection demo (FastAPI + React/MapLibre, Docker :7860); bundles pulled from the Model repo at startup
d1ac326 verified
Raw
History Blame Contribute Delete
4.63 kB
"""Generate synthetic before/after curated pairs so the Space is demonstrable offline.
These are **placeholders**, not LEVIR-CD: textured pseudo-aerial tiles where the "after" image
adds/removes a few bright rectangles ("buildings"). They exercise the full curated pipeline (slider,
overlay, stats) end to end. Replace with real LEVIR-CD test tiles for the shipped Space.
python app/backend/gen_sample_pairs.py --out app/backend/data/curated
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
from PIL import Image
SIZE = 256
def _texture(rng: np.random.Generator, base: tuple[int, int, int]) -> np.ndarray:
"""Low-frequency muted tonal ground texture around a base RGB colour (pseudo-aerial)."""
# a single smooth grayscale field (upsampled low-res noise) drives tonal variation on all
# channels so the result reads like ground/vegetation, not colour noise.
small = rng.normal(0, 1, (12, 12)).astype(np.float32)
norm = (small - small.min()) / (float(small.max() - small.min()) + 1e-6)
field_img = Image.fromarray((norm * 255).astype("uint8")).resize((SIZE, SIZE), Image.BICUBIC)
field = (np.asarray(field_img, dtype=np.float32) - 128.0) * 0.42 # signed, ~[-54, 54]
img = np.asarray(base, dtype=np.float32)[None, None, :] + field[:, :, None]
img += rng.normal(0, 4, (SIZE, SIZE, 3)) # fine grain
return np.clip(img, 0, 255)
def _add_building(img: np.ndarray, rng: np.random.Generator) -> None:
"""Paint one bright rectangular 'building' with a slight roof-colour jitter (in place)."""
h = int(rng.integers(14, 40))
w = int(rng.integers(14, 40))
y = int(rng.integers(0, SIZE - h))
x = int(rng.integers(0, SIZE - w))
roof = np.asarray([210, 205, 195], dtype=np.float32) + rng.normal(0, 12, 3)
img[y : y + h, x : x + w] = np.clip(roof, 60, 255)
img[y : y + 2, x : x + w] = np.clip(roof - 45, 0, 255) # a little shadow/eave
def make_pair(
seed: int, n_common: int, n_added: int, n_removed: int
) -> tuple[Image.Image, Image.Image]:
rng = np.random.default_rng(seed)
base = (95 + int(rng.integers(-10, 10)), 110 + int(rng.integers(-10, 10)), 80)
ground = _texture(rng, base)
before = ground.copy()
after = ground.copy()
for _ in range(n_common): # unchanged buildings — present in both dates
r2 = np.random.default_rng(int(rng.integers(0, 1 << 31)))
h = int(r2.integers(14, 40))
w = int(r2.integers(14, 40))
y = int(r2.integers(0, SIZE - h))
x = int(r2.integers(0, SIZE - w))
roof = np.clip(np.asarray([205, 200, 190], np.float32) + r2.normal(0, 10, 3), 60, 255)
before[y : y + h, x : x + w] = roof
after[y : y + h, x : x + w] = roof
for _ in range(n_removed): # in before only (demolished)
_add_building(before, rng)
for _ in range(n_added): # in after only (constructed) — the real "change"
_add_building(after, rng)
return (
Image.fromarray(before.astype("uint8"), "RGB"),
Image.fromarray(after.astype("uint8"), "RGB"),
)
SPECS = [
(
"suburban_growth",
"Suburban growth",
"Several new houses appear on former open ground.",
5,
4,
0,
),
(
"infill_and_demo",
"Infill + demolition",
"Two buildings removed, three added between dates.",
6,
3,
2,
),
(
"dense_block",
"Dense urban block",
"Mostly stable; one new structure on the block edge.",
9,
1,
0,
),
(
"no_change",
"Stable scene (control)",
"No construction — tests the false-positive rate.",
7,
0,
0,
),
]
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="app/backend/data/curated")
args = ap.parse_args()
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
pairs = []
for i, (pid, title, desc, common, added, removed) in enumerate(SPECS):
before, after = make_pair(seed=100 + i, n_common=common, n_added=added, n_removed=removed)
(out / pid).mkdir(exist_ok=True)
before.save(out / pid / "before.png")
after.save(out / pid / "after.png")
pairs.append(
{"id": pid, "title": title, "description": desc, "source": "synthetic placeholder"}
)
(out / "manifest.json").write_text(json.dumps({"pairs": pairs}, indent=2))
print(f"wrote {len(pairs)} synthetic pairs to {out}")
if __name__ == "__main__":
main()