Spaces:
Sleeping
Sleeping
File size: 4,628 Bytes
d1ac326 | 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 | """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()
|