import cv2 import numpy as np import pyzed.sl as sl from typing import Optional, Tuple, Dict from .pose_math import compute_reprojection_error def create_detector( dictionary_id: int = cv2.aruco.DICT_4X4_50, params: Optional[cv2.aruco.DetectorParameters] = None, ) -> cv2.aruco.ArucoDetector: """ Creates an ArUco detector with the specified dictionary and parameters. Args: dictionary_id: The ArUco dictionary ID. params: Optional detector parameters. If None, default parameters are used. Returns: cv2.aruco.ArucoDetector: The configured ArUco detector. """ dictionary = cv2.aruco.getPredefinedDictionary(dictionary_id) if params is None: params = cv2.aruco.DetectorParameters() return cv2.aruco.ArucoDetector(dictionary, params) def detect_markers( image: np.ndarray, detector: cv2.aruco.ArucoDetector ) -> Tuple[np.ndarray, Optional[np.ndarray]]: """ Detects ArUco markers in an image. Args: image: Input image (BGR or grayscale). detector: The ArUco detector to use. Returns: Tuple[np.ndarray, Optional[np.ndarray]]: - corners: Detected corners as float32 shape (N, 4, 2). - ids: Detected marker IDs as int shape (N,) or None if no markers detected. """ if image is None: raise ValueError("Input image is None") if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image corners, ids, _ = detector.detectMarkers(gray) if ids is not None: # corners is a list of (1, 4, 2) arrays, reshape to (N, 4, 2) corners_array = np.array(corners, dtype=np.float32).reshape(-1, 4, 2) ids_array = ids.flatten().astype(np.int32) return corners_array, ids_array else: return np.empty((0, 4, 2), dtype=np.float32), None def build_camera_matrix_from_zed( cam: sl.Camera, resizer: Optional[sl.Resolution] = None ) -> np.ndarray: """ Builds a 3x3 camera intrinsic matrix from a ZED camera's calibration parameters. Args: cam: The ZED camera object. resizer: Optional resolution to get parameters for. Returns: np.ndarray: 3x3 float64 camera matrix K. """ cam_params = cam.get_camera_information( resizer ).camera_configuration.calibration_parameters.left_cam fx = cam_params.fx fy = cam_params.fy cx = cam_params.cx cy = cam_params.cy K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float64) return K def estimate_pose_from_detections( corners: np.ndarray, ids: np.ndarray, marker_geometry: Dict[int, np.ndarray], K: np.ndarray, dist: Optional[np.ndarray] = None, min_markers: int = 4, pnp_flag: int = cv2.SOLVEPNP_SQPNP, ) -> Optional[Tuple[np.ndarray, np.ndarray, float, int]]: """ Estimates the camera pose from detected ArUco markers. Args: corners: Detected corners (N, 4, 2). ids: Detected marker IDs (N,). marker_geometry: Dictionary mapping marker ID to its 3D corners (4, 3). K: 3x3 camera matrix. dist: Optional distortion coefficients. min_markers: Minimum number of markers required for pose estimation. pnp_flag: OpenCV solvePnP flag. Returns: Optional[Tuple[np.ndarray, np.ndarray, float, int]]: (rvec, tvec, reproj_err, n_markers) or None if estimation fails or not enough markers. """ if ids is None or len(ids) == 0: return None obj_pts = [] img_pts = [] n_markers = 0 for i, marker_id in enumerate(ids): if marker_id in marker_geometry: obj_pts.append(marker_geometry[marker_id]) img_pts.append(corners[i]) n_markers += 1 if n_markers < min_markers: return None # Stack points into (M*4, 3) and (M*4, 2) obj_pts_array = np.vstack(obj_pts).astype(np.float32) img_pts_array = np.vstack(img_pts).astype(np.float32) success, rvec, tvec = cv2.solvePnP( obj_pts_array, img_pts_array, K, dist, flags=pnp_flag ) if not success: return None reproj_err = compute_reprojection_error( obj_pts_array, img_pts_array, rvec, tvec, K, dist ) return rvec, tvec, reproj_err, n_markers