Image Segmentation
PyTorch
English
painting-vision-robotics-kit
semantic-segmentation
robotics
edge-ai
construction-ai
autonomous-painting
wall-painting-robot
paint-coverage-estimation
building-facade
drywall
skirting-detection
window-detection
lidar
depth-validation
segformer
deeplabv3
Eval Results (legacy)
Download test_robot_planner.py from constructelligence/painting-vision-robotics-kit: direct link, hf CLI and curl.
- Browser
- Download file 7.94 kB
-
https://huggingface.co/constructelligence/painting-vision-robotics-kit/resolve/main/test_robot_planner.py
- Command line
-
hf download hf://constructelligence/painting-vision-robotics-kit/test_robot_planner.py
-
curl -L -o test_robot_planner.py https://huggingface.co/constructelligence/painting-vision-robotics-kit/resolve/main/test_robot_planner.py
7.94 kB
| #!/usr/bin/env python3 | |
| """Fast geometry regression tests for robot_planner (no torch/cv2 needed). | |
| Run with ``python3 test_robot_planner.py``. These pin the pure-numpy/scipy | |
| behaviour: contour tracing, polygon simplification, homography round-trips, and | |
| stroke planning/coverage, including the failure path when the roller cannot fit. | |
| """ | |
| import numpy as np | |
| import robot_planner as rp | |
| def rectangle(height, width, top, left, bottom, right): | |
| mask = np.zeros((height, width), dtype=bool) | |
| mask[top:bottom, left:right] = True | |
| return mask | |
| def test_trace_follows_concavity(): | |
| # L-shape: a convex hull would have 4 corners and bridge the notch. | |
| mask = rectangle(60, 60, 5, 5, 55, 20) | |
| mask |= rectangle(60, 60, 40, 5, 55, 55) | |
| traced = [(x, y) for y, x in rp.trace_boundary(mask)] | |
| simplified = rp.simplify_closed(traced, tolerance=2.0) | |
| assert 5 <= len(simplified) <= 8, f"expected ~6 corners for an L-shape, got {len(simplified)}" | |
| assert len(traced) > 40 | |
| def test_rectangle_simplifies_to_four_corners(): | |
| traced = [(x, y) for y, x in rp.trace_boundary(rectangle(40, 70, 5, 6, 33, 61))] | |
| simplified = rp.simplify_closed(traced, tolerance=1.5) | |
| assert len(simplified) == 4, simplified | |
| def test_homography_round_trip(): | |
| corners = np.array([[10, 12], [410, 8], [420, 308], [5, 300]], float) | |
| matrix = rp.homography(corners, [(0, 0), (400, 0), (400, 300), (0, 300)]) | |
| mapped = rp.apply_homography(matrix, corners) | |
| assert np.allclose(mapped, [[0, 0], [400, 0], [400, 300], [0, 300]], atol=1e-6) | |
| back = rp.apply_homography(np.linalg.inv(matrix), mapped) | |
| assert np.allclose(back, corners, atol=1e-6) | |
| def test_plan_covers_open_wall(): | |
| paintable = rectangle(400, 600, 20, 20, 380, 580) | |
| keepout = np.zeros_like(paintable) | |
| plan, debug = rp.build_plan(paintable, keepout, stroke_width=80, overlap=0.2, margin=20, | |
| orientation="horizontal", corners=None, mm_per_unit=2.5, | |
| boundary_tolerance=6.0) | |
| assert plan["warnings"] == [], plan["warnings"] | |
| assert plan["wall"]["corners"] == 4 | |
| assert plan["plan"]["segments"] > 0 | |
| assert plan["coverage"]["planned_fraction"] >= 0.8, plan["coverage"] | |
| assert plan["plan"]["path_length_mm"] > plan["plan"]["path_length_units"] | |
| assert debug["allowed"].any() | |
| def test_plan_reports_infeasible_roller(): | |
| paintable = rectangle(200, 200, 0, 0, 200, 200) | |
| keepout = np.zeros_like(paintable) | |
| plan, _ = rp.build_plan(paintable, keepout, stroke_width=900, overlap=0.2, margin=100, | |
| orientation="horizontal", corners=None, mm_per_unit=None, | |
| boundary_tolerance=4.0) | |
| assert plan["plan"]["segments"] == 0 | |
| assert plan["warnings"] and "centre-line" in plan["warnings"][0] | |
| def test_keepout_blocks_strokes(): | |
| paintable = rectangle(300, 300, 10, 10, 290, 290) | |
| keepout = rectangle(300, 300, 0, 140, 300, 160) # vertical stripe: splits every pass | |
| plan, _ = rp.build_plan(paintable, keepout, stroke_width=40, overlap=0.2, margin=10, | |
| orientation="horizontal", corners=None, mm_per_unit=None, | |
| boundary_tolerance=4.0) | |
| assert plan["plan"]["segments"] > plan["plan"]["passes"], "keep-out stripe should split passes" | |
| assert 0.0 < plan["coverage"]["planned_fraction"] < 1.0 | |
| def test_trim_tool_reaches_edges_and_fixtures(): | |
| paintable = rectangle(400, 600, 20, 20, 380, 580) | |
| keepout = rectangle(400, 600, 0, 290, 400, 310) # fixture stripe the roller keeps clear of | |
| roller_only, _ = rp.build_plan(paintable, keepout, stroke_width=90, overlap=0.2, margin=25, | |
| orientation="horizontal", corners=None, mm_per_unit=None, | |
| boundary_tolerance=4.0) | |
| combined, _ = rp.build_plan(paintable, keepout, stroke_width=90, overlap=0.2, margin=25, | |
| orientation="horizontal", corners=None, mm_per_unit=None, | |
| boundary_tolerance=4.0, trim_width=20, trim_overlap=0.3) | |
| assert roller_only["tools"] == ["roller"] | |
| assert combined["tools"] == ["roller", "trim"] | |
| assert combined["plan"]["trim"]["segments"] > 0 | |
| assert combined["coverage"]["planned_fraction"] > roller_only["coverage"]["planned_fraction"] | |
| assert combined["coverage"]["remaining_fraction"] < roller_only["coverage"]["remaining_fraction"] | |
| assert combined["coverage"]["remaining_fraction"] < 0.05 | |
| def test_waypoints_carry_paint_flags(): | |
| paintable = rectangle(200, 200, 10, 10, 190, 190) | |
| plan, _ = rp.build_plan(paintable, np.zeros_like(paintable), stroke_width=40, overlap=0.2, margin=10, | |
| orientation="horizontal", corners=None, mm_per_unit=None, boundary_tolerance=4.0) | |
| waypoints = plan["plan"]["waypoints"] | |
| segments = plan["plan"]["segments"] | |
| assert waypoints[0]["paint"] is True | |
| assert sum(1 for w in waypoints if not w["paint"]) == max(0, segments - 1) | |
| assert all(set(("x", "y", "paint", "tool")) <= set(w) for w in waypoints) | |
| def test_progress_state_round_trip_and_replan(): | |
| import tempfile | |
| from pathlib import Path | |
| painted = rectangle(120, 160, 10, 10, 90, 150) | |
| with tempfile.TemporaryDirectory() as folder: | |
| path = Path(folder) / "state.npz" | |
| assert rp.load_state(path) is None | |
| rp.save_state(path, painted, (160, 120)) | |
| loaded, size = rp.load_state(path) | |
| assert size == (160, 120) and np.array_equal(loaded, painted) | |
| # A fully painted target is recognised and yields no new strokes. | |
| plan, _ = rp.build_plan(painted, np.zeros_like(painted), stroke_width=30, overlap=0.2, margin=5, | |
| orientation="horizontal", corners=None, mm_per_unit=None, | |
| boundary_tolerance=3.0, already_painted=painted) | |
| assert plan["plan"]["segments"] == 0 | |
| assert plan["coverage"]["already_painted_fraction"] == 1.0 | |
| assert any("already fully painted" in w for w in plan["warnings"]) | |
| def test_progress_state_rejects_size_mismatch(): | |
| paintable = rectangle(120, 160, 10, 10, 90, 150) | |
| try: | |
| rp.build_plan(paintable, np.zeros_like(paintable), stroke_width=30, overlap=0.2, margin=5, | |
| orientation="horizontal", corners=None, mm_per_unit=None, boundary_tolerance=3.0, | |
| already_painted=rectangle(100, 100, 0, 0, 100, 100)) | |
| except ValueError as exc: | |
| assert "does not match plane" in str(exc) | |
| else: | |
| raise AssertionError("expected a size-mismatch ValueError") | |
| def test_measured_plane_size_sets_true_aspect(): | |
| # A foreshortened wall in the image: without the measured size the plane takes the | |
| # image's longest edges; with it, the plane has the wall's true proportions. | |
| paintable = rectangle(400, 600, 0, 0, 400, 600) | |
| corners = [(100, 80), (500, 120), (500, 300), (100, 340)] | |
| _, guessed = rp.build_plan(paintable, np.zeros_like(paintable), stroke_width=20, overlap=0.2, margin=5, | |
| orientation="horizontal", corners=corners, mm_per_unit=None, boundary_tolerance=4.0) | |
| plan, measured = rp.build_plan(paintable, np.zeros_like(paintable), stroke_width=20, overlap=0.2, margin=5, | |
| orientation="horizontal", corners=corners, mm_per_unit=10.0, | |
| boundary_tolerance=4.0, plane_size=(366, 229)) | |
| assert tuple(guessed["size"]) == (402, 260) and tuple(measured["size"]) == (366, 229) | |
| assert plan["plane_size"]["width"] == 366 and plan["plane_size"]["mm_per_unit"] == 10.0 | |
| 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() | |