"""Generate `viewer/sample/` — a synthetic scene so the viewer runs with no model. It exists for two reasons: the viewer has to be demonstrable (and testable in CI) before a checkpoint exists, and a *known* surface is the only way to prove the height read-out, the slope tool and the live metrics report the right numbers. The block heights below are exact, so probing a rooftop must read exactly that. python viewer/make_sample.py """ from __future__ import annotations import json import sys from pathlib import Path import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parents[1])) W = H = 384 GSD = 0.5 OUT = Path(__file__).resolve().parent / "sample" BLOCKS = [ # (row0, col0, h, w, height_m) (40, 40, 70, 55, 24.0), (40, 120, 70, 90, 11.0), (40, 240, 70, 60, 38.0), (150, 40, 60, 70, 8.0), (150, 140, 60, 60, 17.0), (150, 230, 60, 90, 6.0), (260, 60, 70, 110, 30.0), (260, 200, 70, 70, 13.5), ] def build(): rng = np.random.default_rng(7) gt = np.zeros((H, W), np.float32) rgb = np.zeros((H, W, 3), np.uint8) rgb[:] = (86, 84, 78) # asphalt # a park, so the scene has something that is neither road nor roof gt[300:370, 280:360] = 5.0 + rng.random((70, 80)).astype(np.float32) * 3.0 rgb[300:370, 280:360] = (54, 92, 48) for r0, c0, h, w, z in BLOCKS: gt[r0:r0 + h, c0:c0 + w] = z shade = int(np.clip(150 + z * 1.6, 120, 235)) rgb[r0:r0 + h, c0:c0 + w] = (shade, shade - 8, shade - 20) noise = rng.normal(0, 3, (H, W)).astype(np.float32) rgb = np.clip(rgb.astype(np.float32) + noise[..., None], 0, 255).astype(np.uint8) # A plausible *prediction*: tall structures compressed, edges blurred, a # little hallucinated relief on flat ground. That is what the real failure # modes look like, and the live metrics panel should show them. pred = gt.copy() pred[gt > 15.0] *= 0.84 pred = _blur(pred, 2.0) pred += rng.normal(0, 0.18, pred.shape).astype(np.float32) pred = np.clip(pred, 0.0, None).astype(np.float32) return rgb, pred, gt def _blur(a, sigma): try: from scipy.ndimage import gaussian_filter return gaussian_filter(a, sigma=sigma, mode="nearest") except Exception: # noqa: BLE001 return a def main(): from PIL import Image rgb, pred, gt = build() OUT.mkdir(parents=True, exist_ok=True) Image.fromarray(rgb).save(OUT / "rgb.png") np.save(OUT / "ndsm_m.npy", pred) np.save(OUT / "gt_ndsm_m.npy", gt) lo, hi = float(pred.min()), float(max(pred.max(), 1.0)) Image.fromarray((((pred - lo) / (hi - lo)) * 65535).astype(np.uint16)) \ .save(OUT / "ndsm16.png") (OUT / "meta.json").write_text(json.dumps({ "stem": "synthetic_city", "product": "rDSM", "units": "metres_above_ground", "height_min_m": lo, "height_max_m": hi, "height_mean_m": float(pred.mean()), "height_median_m": float(np.median(pred)), "frac_below_1m": float((pred < 1.0).mean()), "size_px": [H, W], "scene": {"path": "synthetic", "width": W, "height": H, "gsd_m": GSD, "gsd_source": "assumed", "georeferenced": False, "crs": None}, "note": "synthetic scene generated by viewer/make_sample.py — " "block heights are exact, so the probe read-out can be checked", "truth_blocks_m": [b[-1] for b in BLOCKS], }, indent=2)) print(f"wrote {OUT} ({W}x{H} @ {GSD} m, heights 0..{gt.max():.1f} m)") if __name__ == "__main__": main()