#!/usr/bin/env python3 """Synthetic "camera" — a deterministic textured 3D-ish scene with a moving camera so the pipeline can be tested without a webcam, end to end, offline. Modes: pixi run python scene.py still out.png [--t 2.5] render ONE frame at time t pixi run python scene.py frames out_dir [--t0 0] [--t1 6] [--fps 10] batch stills pixi run scene show the live feed (no pipeline) This is the scene the whole company develops against. Improving fidelity is allowed — but the camera PATH is contract: don't change it without a meeting and an updated judge.py. """ import argparse import sys from pathlib import Path import numpy as np import cv2 W, H = 640, 400 FPS = 30.0 FOCAL = 260.0 def camera_path(t): """Deterministic camera pose over time (a slow forward dolly + pan).""" x = 2.4 * t * np.sin(0.35 * t) z = 1.0 + 1.5 * t yaw = 0.12 * np.sin(0.5 * t) # radians return np.array([x, 0.0, z]), yaw def landmarks(): """Static 3D points on three walls + floor, deterministic.""" rng = np.random.default_rng(7) pts = [] # left wall x=-4 pts.append(np.column_stack([np.full(60, -4.0), rng.uniform(-2, 2.2, 60), rng.uniform(2, 9, 60)])) # right wall x=+4 pts.append(np.column_stack([np.full(60, 4.0), rng.uniform(-2, 2.2, 60), rng.uniform(2, 9, 60)])) # floor y=-2 (weak relevance: keep fewer dots) pts.append(np.column_stack([rng.uniform(-4, 4, 25), np.full(25, -2.0), rng.uniform(2, 9, 25)])) return np.concatenate(pts) LANDMARKS = landmarks() def render(t): (pos, yaw) = camera_path(t) c, s = np.cos(yaw), np.sin(yaw) rel = LANDMARKS - pos xs, ys, zs = rel[:, 0], rel[:, 1], rel[:, 2] xc = c * xs + s * zs # camera yaws about the world-Y axis zc = -s * xs + c * zs # zc = depth along the camera's view axis valid = zc > 0.3 px = FOCAL * xc[valid] / zc[valid] + W / 2 py = -FOCAL * ys[valid] / zc[valid] + H / 2 frame = np.full((H, W, 3), 40, np.uint8) depth = zc[valid] for (x, y, d) in zip(px, py, depth): color = tuple(int(v) for v in (255 - 18 * d, 210 - 12 * d, 160 - 6 * d)) xi, yi = int(round(float(x))), int(round(float(y))) if 0 <= yi < H and 0 <= xi < W: b = 2 + int(1.6 / d) cv2.circle(frame, (xi, yi), b, color, -1) return frame def main(): ap = argparse.ArgumentParser() ap.add_argument("mode", choices=["still", "frames", "stream"]) ap.add_argument("out", nargs="?", default="out.png") ap.add_argument("--t", type=float, default=2.5) ap.add_argument("--t0", type=float, default=0.0) ap.add_argument("--t1", type=float, default=6.0) ap.add_argument("--fps", type=float, default=10.0) a = ap.parse_args() if a.mode == "still": cv2.imwrite(a.out, render(a.t)) elif a.mode == "frames": out = Path(a.out) out.mkdir(parents=True, exist_ok=True) n = int((a.t1 - a.t0) * a.fps) for i in range(n): cv2.imwrite(str(out / f"f{i:04d}.png"), render(a.t0 + i / a.fps)) print(f"wrote {n} frames to {out}") else: t = 0.0 while True: cv2.imshow("scene (raw feed)", render(t)) if cv2.waitKey(int(1000 / FPS)) == 27: break t += 1 / FPS cv2.destroyAllWindows() if __name__ == "__main__": sys.exit(main())