personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_aruco.py
7.8 kB 235 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for observe.aruco ArUco marker detection and masking.""" 5 6import cv2 7import numpy as np 8from PIL import Image 9 10from solstone.observe.aruco import ( 11 CORNER_TAG_IDS, 12 detect_markers, 13 mask_convey_region, 14 polygon_area, 15) 16 17 18def test_corner_tag_ids(): 19 """Test that corner tag IDs match expected values.""" 20 assert CORNER_TAG_IDS == {2, 4, 6, 7} 21 22 23def test_polygon_area_square(): 24 """Test polygon area calculation for a square.""" 25 # 100x100 square 26 polygon = [(0, 0), (100, 0), (100, 100), (0, 100)] 27 assert polygon_area(polygon) == 10000.0 28 29 30def test_polygon_area_triangle(): 31 """Test polygon area calculation for a triangle.""" 32 # Right triangle with legs 10 and 20 33 polygon = [(0, 0), (10, 0), (0, 20)] 34 assert polygon_area(polygon) == 100.0 # (10 * 20) / 2 35 36 37def test_polygon_area_empty(): 38 """Test polygon area with insufficient points.""" 39 assert polygon_area([]) == 0.0 40 assert polygon_area([(0, 0)]) == 0.0 41 assert polygon_area([(0, 0), (1, 1)]) == 0.0 42 43 44def test_detect_markers_no_markers(): 45 """Test detect_markers returns None when no markers are present.""" 46 img = Image.new("RGB", (640, 480), color="white") 47 result = detect_markers(img) 48 assert result is None 49 50 51def test_detect_markers_grayscale(): 52 """Test detect_markers works with grayscale input.""" 53 img = Image.new("L", (640, 480), color=128) 54 result = detect_markers(img) 55 assert result is None # No markers, but shouldn't crash 56 57 58def test_mask_convey_region(): 59 """Test masking fills polygon with black.""" 60 img = Image.new("RGB", (100, 100), color="white") 61 62 # Define a square polygon in the center 63 polygon = [(25, 25), (75, 25), (75, 75), (25, 75)] 64 mask_convey_region(img, polygon) 65 66 # Check corners are still white 67 assert img.getpixel((0, 0)) == (255, 255, 255) 68 assert img.getpixel((99, 99)) == (255, 255, 255) 69 70 # Check center is black 71 assert img.getpixel((50, 50)) == (0, 0, 0) 72 73 74def test_mask_convey_region_triangle(): 75 """Test masking works with non-rectangular polygon.""" 76 img = Image.new("RGB", (100, 100), color="white") 77 78 # Triangle 79 polygon = [(50, 10), (90, 90), (10, 90)] 80 mask_convey_region(img, polygon) 81 82 # Center should be black (inside triangle) 83 assert img.getpixel((50, 60)) == (0, 0, 0) 84 85 # Top corners should still be white (outside triangle) 86 assert img.getpixel((5, 5)) == (255, 255, 255) 87 assert img.getpixel((95, 5)) == (255, 255, 255) 88 89 90def test_detect_markers_with_all_corners(): 91 """Test detect_markers returns full result with all 4 corner markers.""" 92 # Create a test image 93 img_array = np.ones((480, 640, 3), dtype=np.uint8) * 255 94 95 # Generate and place the 4 corner markers 96 dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) 97 marker_size = 50 98 pad = 20 99 100 # Generate and place markers 101 for tag_id in [6, 7, 4, 2]: 102 marker = cv2.aruco.generateImageMarker(dictionary, tag_id, marker_size) 103 marker_rgb = cv2.cvtColor(marker, cv2.COLOR_GRAY2RGB) 104 105 if tag_id == 6: # TL 106 img_array[pad : pad + marker_size, pad : pad + marker_size] = marker_rgb 107 elif tag_id == 7: # TR 108 img_array[pad : pad + marker_size, 640 - pad - marker_size : 640 - pad] = ( 109 marker_rgb 110 ) 111 elif tag_id == 4: # BL 112 img_array[480 - pad - marker_size : 480 - pad, pad : pad + marker_size] = ( 113 marker_rgb 114 ) 115 elif tag_id == 2: # BR 116 img_array[ 117 480 - pad - marker_size : 480 - pad, 118 640 - pad - marker_size : 640 - pad, 119 ] = marker_rgb 120 121 pil_img = Image.fromarray(img_array) 122 123 result = detect_markers(pil_img) 124 125 # Should return dict with markers and polygon 126 assert result is not None 127 assert "markers" in result 128 assert "polygon" in result 129 130 # Should have 4 markers 131 assert len(result["markers"]) == 4 132 133 # Each marker should have id and corners 134 marker_ids = {m["id"] for m in result["markers"]} 135 assert marker_ids == {2, 4, 6, 7} 136 137 for marker in result["markers"]: 138 assert "id" in marker 139 assert "corners" in marker 140 assert len(marker["corners"]) == 4 141 for corner in marker["corners"]: 142 assert len(corner) == 2 143 assert isinstance(corner[0], (int, float)) 144 assert isinstance(corner[1], (int, float)) 145 146 # Polygon should be present (all 4 corners detected) 147 assert result["polygon"] is not None 148 assert len(result["polygon"]) == 4 149 150 151def test_detect_markers_partial(): 152 """Test detect_markers returns markers but no polygon with partial detection.""" 153 # Create a test image 154 img_array = np.ones((480, 640, 3), dtype=np.uint8) * 255 155 156 # Generate and place only 2 corner markers 157 dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) 158 marker_size = 50 159 pad = 20 160 161 # Only place TL (6) and TR (7) markers 162 for tag_id, pos in [(6, (pad, pad)), (7, (pad, 640 - pad - marker_size))]: 163 marker = cv2.aruco.generateImageMarker(dictionary, tag_id, marker_size) 164 marker_rgb = cv2.cvtColor(marker, cv2.COLOR_GRAY2RGB) 165 y, x = pos 166 img_array[y : y + marker_size, x : x + marker_size] = marker_rgb 167 168 pil_img = Image.fromarray(img_array) 169 170 result = detect_markers(pil_img) 171 172 # Should return dict with markers but no polygon 173 assert result is not None 174 assert "markers" in result 175 assert "polygon" in result 176 177 # Should have 2 markers 178 assert len(result["markers"]) == 2 179 marker_ids = {m["id"] for m in result["markers"]} 180 assert marker_ids == {6, 7} 181 182 # Polygon should be None (only 2 of 4 corners) 183 assert result["polygon"] is None 184 185 186def test_detect_markers_extrapolated_tl(): 187 """Test detect_markers extrapolates missing TL corner from 3 present markers.""" 188 img_array = np.ones((480, 640, 3), dtype=np.uint8) * 255 189 dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) 190 marker_size = 50 191 pad = 20 192 193 # Place TR (7), BR (2), BL (4) — omit TL (6) 194 for tag_id, (y, x) in [ 195 (7, (pad, 640 - pad - marker_size)), 196 (2, (480 - pad - marker_size, 640 - pad - marker_size)), 197 (4, (480 - pad - marker_size, pad)), 198 ]: 199 marker = cv2.aruco.generateImageMarker(dictionary, tag_id, marker_size) 200 marker_rgb = cv2.cvtColor(marker, cv2.COLOR_GRAY2RGB) 201 img_array[y : y + marker_size, x : x + marker_size] = marker_rgb 202 203 result = detect_markers(Image.fromarray(img_array)) 204 205 assert result is not None 206 assert result["polygon"] is not None 207 assert len(result["polygon"]) == 4 208 assert result.get("extrapolated") == 6 209 210 # Extrapolated TL should be within 2px of expected position 211 tl = result["polygon"][0] 212 assert abs(tl[0] - pad) <= 2 213 assert abs(tl[1] - pad) <= 2 214 215 216def test_detect_markers_two_missing_no_extrapolation(): 217 """Test detect_markers returns no polygon when only 2 corner tags present.""" 218 img_array = np.ones((480, 640, 3), dtype=np.uint8) * 255 219 dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) 220 marker_size = 50 221 pad = 20 222 223 # Place only TL (6) and BR (2) 224 for tag_id, (y, x) in [ 225 (6, (pad, pad)), 226 (2, (480 - pad - marker_size, 640 - pad - marker_size)), 227 ]: 228 marker = cv2.aruco.generateImageMarker(dictionary, tag_id, marker_size) 229 marker_rgb = cv2.cvtColor(marker, cv2.COLOR_GRAY2RGB) 230 img_array[y : y + marker_size, x : x + marker_size] = marker_rgb 231 232 result = detect_markers(Image.fromarray(img_array)) 233 234 assert result is not None 235 assert result["polygon"] is None