room-visualizer / verify_r1_plane_sim.py
GitHub Actions
Deploy from GitHub commit ca72656c17476e5aa37a4735af6e47ff9f94fa1a
b20c82e
Raw
History Blame Contribute Delete
5.33 kB
"""R1-2 — depth-based plane-fit fallback certification (CI-safe: analytic
depth, no torch).
Reuses the exact pinhole ground-plane scene from verify_r1_scale_sim (camera
1.5 m up, pitch 25 deg, f = image width) and runs the REAL
plane_homography_from_depth from app.py.
Checks:
1. engages: returns a homography + plane on a clean metric ground plane
2. metric by construction: the certified R1-3 estimator measures
metersPerUnit ~ 1 on the produced homography (within 2%)
3. shear-free and true-to-size: a known 1 m ground square maps to a
1 x 1 plane square with right angles (sides within 2%, angle within 2 deg)
4. orientation: plane-y grows toward the camera (near field), plane-x
image-right — the bundle convention the frontend assumes
5. rejection: non-planar depth (a dome) returns None
6. rejection: relative (normalised) depth returns None
"""
import cv2
import numpy as np
from verify_r1_scale_sim import CAM_H, F, H as IMG_H, PITCH, W as IMG_W, scene
# --- real implementations from app.py ----------------------------------------
src = open("app.py").read()
ns = {"np": np, "cv2": cv2, "depth_model_is_metric": lambda name=None: True}
for fn in ["estimate_meters_per_unit", "plane_homography_from_depth"]:
start = src.index(f"def {fn}")
end = src.index("\ndef ", start + 10)
exec(compile(src[start:end], "app.py", "exec"), ns)
plane_homography_from_depth = ns["plane_homography_from_depth"]
estimate_meters_per_unit = ns["estimate_meters_per_unit"]
def to_plane(H, px, py):
den = H[2, 0] * px + H[2, 1] * py + H[2, 2]
return (
(H[0, 0] * px + H[0, 1] * py + H[0, 2]) / den,
(H[1, 0] * px + H[1, 1] * py + H[1, 2]) / den,
)
def project_ground(x_w, fwd_w):
"""Image pixel of a world ground point — inverse of the scene mapping."""
# world -> camera: y_c, z_c from CAM_H/pitch; then u,v via pinhole
y_w = -CAM_H
z_c = np.cos(PITCH) * fwd_w - np.sin(PITCH) * y_w
y_c = -np.sin(PITCH) * fwd_w - np.cos(PITCH) * y_w
u = x_w / z_c * F + IMG_W / 2.0
v = y_c / z_c * F + IMG_H / 2.0
return u, v
def main():
ok = True
mask, z, x_w, fwd_w = scene()
fitted = plane_homography_from_depth(z, mask, IMG_W, IMG_H)
if fitted is None:
print(" [FAIL] fallback did not engage on a clean metric ground plane")
print("\nR1-2 SIM CHECKS FAILED")
return 1
hom, plane = fitted
H = np.asarray(hom, np.float64).reshape(3, 3)
print(f" [PASS] engages: plane {plane['width']:.2f} x {plane['height']:.2f} m, "
f"source={plane.get('geometrySource')}")
mpu = estimate_meters_per_unit(z, mask, hom, IMG_W, IMG_H)
good = mpu is not None and abs(mpu - 1.0) <= 0.02
print(f" [{'PASS' if good else 'FAIL'}] metric: metersPerUnit = {mpu}")
ok &= good
# known 1m ground square in the near field, centred
cx_w = 0.0
f0 = CAM_H / np.tan(PITCH) * 0.9 # comfortably inside the visible floor
corners_w = [(cx_w - 0.5, f0), (cx_w + 0.5, f0), (cx_w + 0.5, f0 + 1.0), (cx_w - 0.5, f0 + 1.0)]
corners_p = []
for xw, fw in corners_w:
u, v = project_ground(xw, fw)
corners_p.append(to_plane(H, u, v))
corners_p = np.asarray(corners_p)
s1 = np.linalg.norm(corners_p[1] - corners_p[0])
s2 = np.linalg.norm(corners_p[2] - corners_p[1])
d1 = corners_p[1] - corners_p[0]
d2 = corners_p[2] - corners_p[1]
angle = np.degrees(np.arccos(abs(d1 @ d2) / (s1 * s2 + 1e-12)))
square_ok = abs(s1 - 1) <= 0.02 and abs(s2 - 1) <= 0.02 and angle >= 88.0
print(f" [{'PASS' if square_ok else 'FAIL'}] 1m square -> sides {s1:.3f} x {s2:.3f} m, "
f"corner angle {angle:.1f} deg")
ok &= square_ok
# orientation: nearer ground (smaller fwd) must have LARGER plane-y;
# world +x (image right) must have larger plane-x
u_near, v_near = project_ground(0.0, f0)
u_far, v_far = project_ground(0.0, f0 + 2.0)
_, b_near = to_plane(H, u_near, v_near)
_, b_far = to_plane(H, u_far, v_far)
u_r, v_r = project_ground(1.0, f0)
a_l, _ = to_plane(H, u_near, v_near)
a_r, _ = to_plane(H, u_r, v_r)
orient_ok = b_near > b_far and a_r > a_l
print(f" [{'PASS' if orient_ok else 'FAIL'}] orientation: near-y {b_near:.2f} > far-y {b_far:.2f}, "
f"right-x {a_r:.2f} > left-x {a_l:.2f}")
ok &= orient_ok
# rejection: dome instead of plane
yy, xx = np.mgrid[0:IMG_H, 0:IMG_W].astype(np.float64)
dome = (2.5 - 1.2 * np.exp(-(((xx - IMG_W / 2) / 300) ** 2 + ((yy - IMG_H / 2) / 220) ** 2))).astype(np.float32)
r_dome = plane_homography_from_depth(dome, mask, IMG_W, IMG_H)
print(f" [{'PASS' if r_dome is None else 'FAIL'}] rejection: dome depth -> {None if r_dome is None else 'accepted'}")
ok &= r_dome is None
# rejection: relative depth
ns["depth_model_is_metric"] = lambda name=None: False
r_rel = plane_homography_from_depth(z, mask, IMG_W, IMG_H)
ns["depth_model_is_metric"] = lambda name=None: True
print(f" [{'PASS' if r_rel is None else 'FAIL'}] rejection: relative depth -> {None if r_rel is None else 'accepted'}")
ok &= r_rel is None
print("\n" + ("ALL R1-2 SIM CHECKS PASSED" if ok else "R1-2 SIM CHECKS FAILED"))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())