Spaces:
Sleeping
Sleeping
File size: 7,191 Bytes
6a75a66 0d1fbc6 6a75a66 0d1fbc6 6a75a66 0d1fbc6 6a75a66 0d1fbc6 6a75a66 0d1fbc6 6a75a66 | 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 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | """R1-4 β wall-aligned rotation v2 certification (CI-safe: analytic scene,
no torch).
Reuses the pinhole ground-plane camera from verify_r1_scale_sim and builds a
ROTATED room: a floor rectangle at a known world angle phi with wall bands
along its far edges. The analytic metre-plane homography is constructed in
closed form (and self-checked against the projection), so the only thing
under test is the REAL estimate_wall_rotation from app.py.
Checks:
1-4. recovers phi = 0 / 18 / -30 / 70 deg within 2 deg (70 folds to -20:
the tile grid is 90-deg symmetric; perpendicular walls reinforce)
5. abstains on scattered wall blobs (no straight contact line)
6. abstains when two walls disagree (0 vs 45 deg β conflicting cues)
"""
import cv2
import numpy as np
from verify_r1_scale_sim import CAM_H, F, H as IMG_H, PITCH, W as IMG_W
# --- real implementation from app.py ------------------------------------------
# R1-4 v3: the span covers the shared voting helper plus both estimators
# (wall-contact and floor-lattice), ending at the next section banner.
src = open("app.py").read()
ns = {"np": np, "cv2": cv2}
start = src.index("def _fold_segments_to_rotation")
end = src.index("\n# ---", start)
exec(compile(src[start:end], "app.py", "exec"), ns)
estimate_wall_rotation = ns["estimate_wall_rotation"]
estimate_floor_lattice_rotation = ns["estimate_floor_lattice_rotation"]
def plane_homography():
"""Closed-form image -> (x_w, fwd) metre-plane homography for the scene
camera (height CAM_H, pitch PITCH, f = F, principal point at centre)."""
c, s = np.cos(PITCH), np.sin(PITCH)
M = np.array(
[
[F, (IMG_W / 2.0) * c, (IMG_W / 2.0) * s * CAM_H],
[0.0, -F * s + (IMG_H / 2.0) * c, F * c * CAM_H + (IMG_H / 2.0) * s * CAM_H],
[0.0, c, s * CAM_H],
]
)
return np.linalg.inv(M)
def world_grid(Hp):
"""Per-pixel (x_w, fwd) via the plane homography; fwd<=0 marked invalid."""
yy, xx = np.mgrid[0:IMG_H, 0:IMG_W].astype(np.float64)
p = np.stack([xx, yy, np.ones_like(xx)], axis=-1) @ Hp.T
z = p[..., 2]
valid = np.abs(z) > 1e-9
x_w = np.where(valid, p[..., 0] / np.where(valid, z, 1), 0)
fwd = np.where(valid, p[..., 1] / np.where(valid, z, 1), -1)
return x_w, fwd, valid & (fwd > 0.2)
def rotated_room(phi_deg, Hp, walls=("back", "right")):
"""Floor rect at angle phi with 0.35 m wall bands on the given edges."""
x_w, fwd, ok = world_grid(Hp)
phi = np.radians(phi_deg)
f_c = CAM_H / np.tan(PITCH) * 0.9 + 2.0
fx = fwd - f_c
xr = np.cos(phi) * x_w + np.sin(phi) * fx
fr = -np.sin(phi) * x_w + np.cos(phi) * fx
HX, HF = 4.0, 2.0
floor = ok & (np.abs(xr) <= HX) & (np.abs(fr) <= HF)
wall = np.zeros_like(floor)
if "back" in walls:
wall |= ok & (np.abs(xr) <= HX) & (fr > HF) & (fr <= HF + 0.35)
if "right" in walls:
wall |= ok & (np.abs(fr) <= HF) & (xr > HX) & (xr <= HX + 0.35)
return wall.astype(np.uint8), floor.astype(np.uint8)
def lattice_room(phi_deg, Hp, tile=0.5):
"""R1-4 v3 β synthetic photo of an EXISTING tiled floor: dark grout lines
every `tile` metres in a world frame rotated by phi (perspective-correct
through the scene camera), on a light tile field."""
x_w, fwd, ok = world_grid(Hp)
phi = np.radians(phi_deg)
f_c = CAM_H / np.tan(PITCH) * 0.9 + 2.0
fx = fwd - f_c
xr = np.cos(phi) * x_w + np.sin(phi) * fx
fr = -np.sin(phi) * x_w + np.cos(phi) * fx
HX, HF = 4.0, 2.0
floor = ok & (np.abs(xr) <= HX) & (np.abs(fr) <= HF)
img = np.full((IMG_H, IMG_W, 3), 200, np.uint8)
gx = np.abs(xr / tile - np.round(xr / tile)) * tile < 0.015
gf = np.abs(fr / tile - np.round(fr / tile)) * tile < 0.015
img[floor & (gx | gf)] = 90
img[~floor] = 230
return img, floor.astype(np.uint8)
def fold(a):
a = a % 90.0
return a - 90.0 if a >= 45.0 else a
def main():
ok = True
Hp = plane_homography()
# self-check: the closed-form homography inverts the scene projection
f0 = CAM_H / np.tan(PITCH) * 0.9
z_c = np.cos(PITCH) * f0 + np.sin(PITCH) * CAM_H
y_c = -np.sin(PITCH) * f0 + np.cos(PITCH) * CAM_H
u, v = 1.0 / z_c * F + IMG_W / 2.0, y_c / z_c * F + IMG_H / 2.0
p = Hp @ np.array([u, v, 1.0])
assert abs(p[0] / p[2] - 1.0) < 1e-6 and abs(p[1] / p[2] - f0) < 1e-6, \
"scene homography inconsistent with projection"
for phi in (0.0, 18.0, -30.0, 70.0):
wall, floor = rotated_room(phi, Hp)
got = estimate_wall_rotation(wall, floor, Hp)
want = fold(phi)
good = got is not None and abs(fold(got - want)) <= 2.0
print(f" [{'PASS' if good else 'FAIL'}] phi={phi:+.0f} deg -> "
f"{'None' if got is None else f'{got:+.2f}'} (want {want:+.1f})")
ok &= good
# 5 β scattered blobs over a wide band: no dominant direction to read
rng = np.random.default_rng(7)
_, floor = rotated_room(0.0, Hp)
blobs = np.zeros((IMG_H, IMG_W), np.uint8)
edge_rows = np.where(floor.any(axis=1))[0]
top = int(edge_rows[0])
for _ in range(40):
cx = int(rng.integers(40, IMG_W - 40))
cy = int(np.clip(top + rng.integers(-60, 60), 4, IMG_H - 4))
blobs[cy - 3 : cy + 3, max(0, cx - 5) : cx + 5] = 1
got = estimate_wall_rotation(blobs, floor, Hp)
print(f" [{'PASS' if got is None else 'FAIL'}] scattered blobs -> {got}")
ok &= got is None
# 6 β conflicting walls: 0-deg back band + 45-deg diagonal band
wall0, floor0 = rotated_room(0.0, Hp, walls=("back",))
x_w, fwd, valid = world_grid(Hp)
f_c = CAM_H / np.tan(PITCH) * 0.9 + 2.0
diag = valid & (np.abs((fwd - f_c) - x_w) <= 0.25) & (np.abs(x_w) <= 3.0)
conflicted = np.maximum(wall0, diag.astype(np.uint8))
got = estimate_wall_rotation(conflicted, floor0, Hp)
print(f" [{'PASS' if got is None else 'FAIL'}] conflicting 0/45 walls -> {got}")
ok &= got is None
# 7-8 β R1-4 v3: the floor-lattice estimator recovers the EXISTING
# floor's grout direction from the photo (the cue that outranks walls)
for phi in (18.0, -30.0):
img, floor = lattice_room(phi, Hp)
got = estimate_floor_lattice_rotation(img, floor, Hp)
want = fold(phi)
good = got is not None and abs(fold(got - want)) <= 2.0
print(f" [{'PASS' if good else 'FAIL'}] lattice phi={phi:+.0f} deg -> "
f"{'None' if got is None else f'{got:+.2f}'} (want {want:+.1f})")
ok &= good
# 9 β carpet / lineless floor: speckle texture, no lattice β must abstain
# so the pipeline falls through to the wall-contact estimate
rng = np.random.default_rng(11)
_, floor = rotated_room(0.0, Hp)
carpet = np.clip(
180 + rng.normal(0, 6, (IMG_H, IMG_W, 3)), 0, 255
).astype(np.uint8)
got = estimate_floor_lattice_rotation(carpet, floor, Hp)
print(f" [{'PASS' if got is None else 'FAIL'}] carpet (speckle) -> {got}")
ok &= got is None
print("\n" + ("ALL R1-4 SIM CHECKS PASSED" if ok else "R1-4 SIM CHECKS FAILED"))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
|