File size: 4,216 Bytes
b20c82e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""R1-3 — metric scale certification on an exact synthetic scene (CI-safe:
no torch, analytic depth).

Scene: pinhole camera (f = image width), height 1.5 m, pitch 25 deg, looking
at an infinite ground plane. Depth is computed analytically, the homography is
built exactly from four ground points with a known plane-unit scale, so the
true metersPerUnit is known in closed form.

Checks (real implementation extracted from app.py):
  1. recovery: estimate_meters_per_unit returns the true scale within 2%
  2. rejection: a sheared homography (the synthetic-VP failure mode) returns
     None instead of a confidently-wrong scale
  3. relative depth (normalised [0,1]) returns None (metric-only feature)
"""

import cv2
import numpy as np

# --- real implementation from app.py ----------------------------------------
src = open("app.py").read()
ns = {"np": np, "cv2": cv2, "depth_model_is_metric": lambda name=None: True}
start = src.index("def estimate_meters_per_unit")
end = src.index("\n# ---", start)
exec(compile(src[start:end], "app.py", "exec"), ns)
estimate_meters_per_unit = ns["estimate_meters_per_unit"]

W, H = 800, 600
F = float(W)
CAM_H = 1.5
PITCH = np.deg2rad(25.0)
UNITS_PER_M = 200.0          # plane-unit scale baked into the homography
TRUE_MPU = 1.0 / UNITS_PER_M


def scene():
    cx, cy = W / 2.0, H / 2.0
    u, v = np.meshgrid(np.arange(W, dtype=np.float64), np.arange(H, dtype=np.float64))
    # ground plane: 1/Z = (sin(t) + cos(t) * (v - cy)/f) / h  (v grows downward)
    inv_z = (np.sin(PITCH) + np.cos(PITCH) * (v - cy) / F) / CAM_H
    mask = inv_z > 1.0 / 30.0          # floor visible, within 30 m
    z = np.where(mask, 1.0 / np.maximum(inv_z, 1e-9), 0.0)

    # camera-frame 3D, then world ground coordinates. Camera pitched DOWN by
    # PITCH, world y up, image v down: world_y = -cos*y_c - sin*z (must be
    # exactly -CAM_H on the ground — asserted), forward = cos*z - sin*y_c.
    x_c = z * (u - cx) / F
    y_c = z * (v - cy) / F
    world_y = -np.cos(PITCH) * y_c - np.sin(PITCH) * z
    assert np.allclose(world_y[mask], -CAM_H, atol=1e-9), "sim geometry inconsistent"
    x_w = x_c
    fwd_w = np.cos(PITCH) * z - np.sin(PITCH) * y_c
    return mask.astype(np.uint8), z.astype(np.float32), x_w, fwd_w


def exact_homography(mask, x_w, fwd_w):
    ys, xs = np.nonzero(mask)
    # four well-spread ground points
    picks = []
    for fy, fx in [(0.95, 0.2), (0.95, 0.8), (0.55, 0.3), (0.55, 0.7)]:
        yy = int(np.percentile(ys, fy * 100))
        row = xs[ys == yy]
        xx = int(np.percentile(row, fx * 100))
        picks.append((xx, yy))
    src_pts = np.float32(picks)
    dst_pts = np.float32(
        [[x_w[y, x] * UNITS_PER_M, fwd_w[y, x] * UNITS_PER_M] for x, y in picks]
    )
    return cv2.getPerspectiveTransform(src_pts, dst_pts)


def main():
    ok = True
    mask, z, x_w, fwd_w = scene()
    Hm = exact_homography(mask, x_w, fwd_w)

    mpu = estimate_meters_per_unit(z, mask, Hm.flatten().tolist(), W, H)
    if mpu is None:
        print("  [FAIL] recovery: returned None on exact scene")
        ok = False
    else:
        err = abs(mpu - TRUE_MPU) / TRUE_MPU
        good = err <= 0.02
        print(f"  [{'PASS' if good else 'FAIL'}] recovery: mpu={mpu:.6f} "
              f"(true {TRUE_MPU:.6f}, err {err * 100:.2f}%)")
        ok &= good

    # synthetic-VP failure mode: progressive horizontal shear of plane coords
    S = np.array([[1.0, 0.35, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
    H_bad = S @ Hm
    mpu_bad = estimate_meters_per_unit(z, mask, H_bad.flatten().tolist(), W, H)
    print(f"  [{'PASS' if mpu_bad is None else 'FAIL'}] rejection: sheared homography -> {mpu_bad}")
    ok &= mpu_bad is None

    rel = (z - z[mask > 0].min()) / (z[mask > 0].max() - z[mask > 0].min())
    ns["depth_model_is_metric"] = lambda name=None: False
    mpu_rel = estimate_meters_per_unit(rel.astype(np.float32), mask, Hm.flatten().tolist(), W, H)
    print(f"  [{'PASS' if mpu_rel is None else 'FAIL'}] relative depth -> {mpu_rel}")
    ok &= mpu_rel is None

    print("\n" + ("ALL R1-3 SIM CHECKS PASSED" if ok else "R1-3 SIM CHECKS FAILED"))
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())