personal memory agent
0
fork

Configure Feed

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

Merge branch 'hopper-mqojmifi-aruco-extrapolation'

+94 -1
+32
observe/aruco.py
··· 24 24 # Singleton detector instance (created on first use) 25 25 _detector: Optional[cv2.aruco.ArucoDetector] = None 26 26 27 + # Per-tag corner extraction index: which corner of each marker gives the outer bounding point 28 + # ArUco corner order within each marker: [TL(0), TR(1), BR(2), BL(3)] 29 + _CORNER_IDX = {6: 0, 7: 1, 2: 2, 4: 3} 30 + 27 31 28 32 def _get_detector() -> cv2.aruco.ArucoDetector: 29 33 """Get or create the ArUco detector singleton.""" ··· 40 44 params.cornerRefinementMethod = cv2.aruco.CORNER_REFINE_SUBPIX 41 45 _detector = cv2.aruco.ArucoDetector(dictionary, params) 42 46 return _detector 47 + 48 + 49 + def _extrapolate_corner(id_to_corners: dict, missing_id: int) -> list: 50 + known = { 51 + tag_id: id_to_corners[tag_id].reshape(4, 2)[_CORNER_IDX[tag_id]] 52 + for tag_id in CORNER_TAG_IDS 53 + if tag_id in id_to_corners 54 + } 55 + # Parallelogram rule: TL + BR = TR + BL (diagonals share midpoint) 56 + if missing_id == 6: # TL = TR + BL - BR 57 + pt = known[7] + known[4] - known[2] 58 + elif missing_id == 7: # TR = TL + BR - BL 59 + pt = known[6] + known[2] - known[4] 60 + elif missing_id == 2: # BR = TR + BL - TL 61 + pt = known[7] + known[4] - known[6] 62 + else: # BL (4) = TL + BR - TR 63 + pt = known[6] + known[2] - known[7] 64 + return pt.tolist() 43 65 44 66 45 67 def detect_markers(image: Image.Image) -> Optional[dict]: ··· 95 117 br = id_to_corners[2].reshape(4, 2)[2] # BR tag, BR corner 96 118 bl = id_to_corners[4].reshape(4, 2)[3] # BL tag, BL corner 97 119 result["polygon"] = [tl.tolist(), tr.tolist(), br.tolist(), bl.tolist()] 120 + elif len(CORNER_TAG_IDS & id_to_corners.keys()) == 3: 121 + found = CORNER_TAG_IDS & id_to_corners.keys() 122 + missing_id = next(iter(CORNER_TAG_IDS - found)) 123 + corners = { 124 + tag_id: id_to_corners[tag_id].reshape(4, 2)[_CORNER_IDX[tag_id]].tolist() 125 + for tag_id in found 126 + } 127 + corners[missing_id] = _extrapolate_corner(id_to_corners, missing_id) 128 + result["polygon"] = [corners[6], corners[7], corners[2], corners[4]] 129 + result["extrapolated"] = missing_id 98 130 99 131 return result 100 132
+10 -1
observe/describe.py
··· 236 236 if mask_area / frame_area > self.MASK_SKIP_THRESHOLD: 237 237 # Skip frame entirely - Convey UI dominates 238 238 pil_img.close() 239 + _extrap = ( 240 + " (extrapolated)" 241 + if aruco_result.get("extrapolated") is not None 242 + else "" 243 + ) 239 244 logger.debug( 240 245 f"Skipping frame at {timestamp:.2f}s " 241 - f"(Convey UI covers {mask_area / frame_area:.0%})" 246 + f"(Convey UI covers {mask_area / frame_area:.0%}){_extrap}" 242 247 ) 243 248 continue 244 249 # Mask the Convey region with black ··· 259 264 "markers": aruco_result["markers"], 260 265 "masked": aruco_masked, 261 266 } 267 + if aruco_result.get("extrapolated") is not None: 268 + frame_data["aruco"]["extrapolated"] = aruco_result[ 269 + "extrapolated" 270 + ] 262 271 263 272 # First frame: always qualify (RMS vs nothing = 100% different) 264 273 if last_qualified_small is None:
+52
tests/test_aruco.py
··· 181 181 182 182 # Polygon should be None (only 2 of 4 corners) 183 183 assert result["polygon"] is None 184 + 185 + 186 + def 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 + 216 + def 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