Spaces:
Sleeping
Sleeping
| """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() | |