File size: 3,840 Bytes
56b261b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""Regression tests for depth_validation using synthetic depth.

No real LiDAR is required: we render a fronto-parallel wall plane with a
recessed window and a protruding pipe and check the module recovers the plane,
the metric scale, and both features. Run with ``python3 test_depth_validation.py``.
"""
import numpy as np

import depth_validation as dv

FX = FY = 500.0
CX = CY = 160.0
WALL_Z = 3.0


def synthetic_scene(height=320, width=320, noise=0.002, seed=1):
    rng = np.random.default_rng(seed)
    depth = np.full((height, width), WALL_Z, dtype=np.float32)
    window = np.zeros((height, width), bool)
    window[90:150, 60:120] = True     # recessed: farther from camera
    depth[window] = WALL_Z + 0.20
    pipe = np.zeros((height, width), bool)
    pipe[200:280, 250:262] = True     # protruding: closer to camera
    depth[pipe] = WALL_Z - 0.15
    depth += rng.normal(0, noise, depth.shape).astype(np.float32)
    return depth, window, pipe


def test_unproject_center_ray():
    depth = np.full((11, 11), 3.0, dtype=np.float32)
    points = dv.unproject(depth, FX, FY, 5.0, 5.0)
    assert np.allclose(points[5, 5], [0.0, 0.0, 3.0], atol=1e-6), points[5, 5]
    # One pixel to the right of the principal point is fx-normalised in X.
    assert abs(points[5, 6, 0] - 3.0 / FX) < 1e-6


def test_fit_plane_recovers_wall():
    depth, _, _ = synthetic_scene()
    points = dv.unproject(depth, FX, FY, CX, CY)
    valid = np.isfinite(points).all(-1)
    normal, offset, inliers = dv.fit_plane(points[valid], distance_threshold=0.01)
    assert abs(abs(normal[2]) - 1.0) < 1e-3, normal
    assert abs(offset + WALL_Z) < 0.02, offset
    assert inliers.mean() > 0.9


def test_residual_sign_separates_protrusion_from_recess():
    depth, window, pipe = synthetic_scene()
    points = dv.unproject(depth, FX, FY, CX, CY)
    valid = np.isfinite(points).all(-1)
    normal, offset, _ = dv.fit_plane(points[valid], distance_threshold=0.01)
    residual = dv.plane_residuals(points, normal, offset)
    assert residual[window].mean() > 0.15, "window recess should be positive (farther)"
    assert residual[pipe].mean() < -0.10, "pipe protrusion should be negative (closer)"


def test_validate_flags_missed_features():
    depth, window, pipe = synthetic_scene()
    wall = np.ones_like(depth, bool)
    # Keep-out covers the window but not the pipe: the pipe must be flagged.
    keepout = window.copy()
    report, layers = dv.validate(depth, (FX, FY, CX, CY), wall_mask=wall, window_mask=window,
                                 keepout_mask=keepout)
    assert report["plane"]["inlier_fraction"] > 0.9
    assert report["recess_vs_window_iou"] > 0.8
    assert report["missed_protrusions_pixels"] > 500
    assert any("protrusion" in w for w in report["warnings"])
    assert layers["recesses"][130, 90] and layers["protrusions"][240, 256]


def test_metric_scale_is_physical():
    depth, _, _ = synthetic_scene(noise=0.0)
    points = dv.unproject(depth, FX, FY, CX, CY)
    scale = dv.metric_scale_mm_per_pixel(points, FX, FY)
    # 3 m at fx=500 -> 6 mm per pixel.
    assert abs(scale["x"] - 6.0) < 0.05, scale
    assert abs(scale["y"] - 6.0) < 0.05, scale


def test_non_planar_wall_warns():
    rng = np.random.default_rng(3)
    depth = (WALL_Z + rng.normal(0, 0.3, (320, 320))).astype(np.float32)  # rough, non-planar
    wall = np.ones_like(depth, bool)
    report, _ = dv.validate(depth, (FX, FY, CX, CY), wall_mask=wall)
    assert report["wall_fraction_on_plane"] < 0.9
    assert any("plan" in w for w in report["warnings"])


def main():
    tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
    for test in tests:
        test()
        print(f"ok  {test.__name__}")
    print(f"{len(tests)} tests passed")


if __name__ == "__main__":
    main()