#!/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()