#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ArUco标记检测模块 提供基于ArUco标记的靶心标定和激光点定位功能 """ import cv2 import numpy as np import math import config from logger_manager import logger_manager class ArUcoDetector: """ArUco标记检测器""" def __init__(self): self.logger = logger_manager.logger # 创建ArUco字典和检测器参数 self.aruco_dict = cv2.aruco.getPredefinedDictionary(config.ARUCO_DICT_TYPE) self.detector_params = cv2.aruco.DetectorParameters() # 设置检测参数 self.detector_params.minMarkerPerimeterRate = config.ARUCO_MIN_MARKER_PERIMETER_RATE self.detector_params.cornerRefinementMethod = config.ARUCO_CORNER_REFINEMENT_METHOD # 创建检测器 self.detector = cv2.aruco.ArucoDetector(self.aruco_dict, self.detector_params) # 预定义靶纸上的标记位置(物理坐标,毫米) self.marker_positions_mm = config.ARUCO_MARKER_POSITIONS_MM self.marker_ids = config.ARUCO_MARKER_IDS self.marker_size_mm = config.ARUCO_MARKER_SIZE_MM self.target_paper_size_mm = config.TARGET_PAPER_SIZE_MM # 靶心偏移(相对于靶纸中心) self.target_center_offset_mm = config.TARGET_CENTER_OFFSET_MM if self.logger: self.logger.info(f"[ARUCO] ArUco检测器初始化完成,字典类型: {config.ARUCO_DICT_TYPE}") def detect_markers(self, frame): """ 检测图像中的ArUco标记 Args: frame: MaixPy图像帧对象 Returns: (corners, ids, rejected) - 检测到的标记角点、ID列表、被拒绝的候选 如果检测失败返回 (None, None, None) """ try: # 转换为OpenCV格式 from maix import image img_cv = image.image2cv(frame, False, False) # 转换为灰度图(ArUco检测需要) if len(img_cv.shape) == 3: gray = cv2.cvtColor(img_cv, cv2.COLOR_RGB2GRAY) else: gray = img_cv # 检测标记 corners, ids, rejected = self.detector.detectMarkers(gray) if self.logger and ids is not None: self.logger.debug(f"[ARUCO] 检测到 {len(ids)} 个标记: {ids.flatten().tolist()}") return corners, ids, rejected except Exception as e: if self.logger: self.logger.error(f"[ARUCO] 标记检测失败: {e}") return None, None, None def get_target_center_from_markers(self, corners, ids): """ 从检测到的ArUco标记计算靶心位置 Args: corners: 标记角点列表 ids: 标记ID列表 Returns: (center_x, center_y, radius, ellipse_params) 或 (None, None, None, None) center_x, center_y: 靶心像素坐标 radius: 估计的靶心半径(像素) ellipse_params: 椭圆参数用于透视校正 """ if ids is None or len(ids) < 3: if self.logger: self.logger.debug(f"[ARUCO] 检测到的标记数量不足: {len(ids) if ids is not None else 0} < 3") return None, None, None, None try: # 将ID转换为列表便于查找 detected_ids = ids.flatten().tolist() # 收集检测到的标记中心点和对应的物理坐标 image_points = [] # 图像坐标 (像素) object_points = [] # 物理坐标 (毫米) marker_centers = {} # 存储每个标记的中心 for i, marker_id in enumerate(detected_ids): if marker_id not in self.marker_ids: continue # 计算标记中心(四个角的平均值) corner = corners[i][0] # shape: (4, 2) center_x = np.mean(corner[:, 0]) center_y = np.mean(corner[:, 1]) marker_centers[marker_id] = (center_x, center_y) # 添加到点列表 image_points.append([center_x, center_y]) object_points.append(self.marker_positions_mm[marker_id]) if len(image_points) < 3: if self.logger: self.logger.debug(f"[ARUCO] 有效标记数量不足: {len(image_points)} < 3") return None, None, None, None # 转换为numpy数组 image_points = np.array(image_points, dtype=np.float32) object_points = np.array(object_points, dtype=np.float32) # 计算单应性矩阵(Homography) # 这建立了物理坐标到图像坐标的映射 H, status = cv2.findHomography(object_points, image_points, cv2.RANSAC, 5.0) if H is None: if self.logger: self.logger.warning("[ARUCO] 无法计算单应性矩阵") return None, None, None, None # 计算靶心在图像中的位置 # 靶心物理坐标 = 靶纸中心 + 偏移 target_center_mm = np.array([[self.target_center_offset_mm[0], self.target_center_offset_mm[1]]], dtype=np.float32) target_center_mm = target_center_mm.reshape(-1, 1, 2) # 使用单应性矩阵投影到图像坐标 target_center_img = cv2.perspectiveTransform(target_center_mm, H) center_x = target_center_img[0][0][0] center_y = target_center_img[0][0][1] # 计算靶心半径(像素) # 使用已知物理距离和像素距离的比例 # 选择两个标记计算比例尺 if len(marker_centers) >= 2: # 使用对角线上的标记计算比例尺 if 0 in marker_centers and 2 in marker_centers: p1_img = np.array(marker_centers[0]) p2_img = np.array(marker_centers[2]) p1_mm = np.array(self.marker_positions_mm[0]) p2_mm = np.array(self.marker_positions_mm[2]) elif 1 in marker_centers and 3 in marker_centers: p1_img = np.array(marker_centers[1]) p2_img = np.array(marker_centers[3]) p1_mm = np.array(self.marker_positions_mm[1]) p2_mm = np.array(self.marker_positions_mm[3]) else: # 使用任意两个标记 keys = list(marker_centers.keys()) p1_img = np.array(marker_centers[keys[0]]) p2_img = np.array(marker_centers[keys[1]]) p1_mm = np.array(self.marker_positions_mm[keys[0]]) p2_mm = np.array(self.marker_positions_mm[keys[1]]) pixel_distance = np.linalg.norm(p1_img - p2_img) mm_distance = np.linalg.norm(p1_mm - p2_mm) if mm_distance > 0: pixels_per_mm = pixel_distance / mm_distance # 标准靶心半径:10环半径约1.22cm = 12.2mm # 但这里我们返回一个估计值,实际环数计算在laser_manager中 radius_mm = 122.0 # 整个靶纸的半径约200mm,但靶心区域较小 radius = int(radius_mm * pixels_per_mm) else: radius = 100 # 默认值 else: radius = 100 # 默认值 # 计算椭圆参数(用于透视校正) # 从单应性矩阵可以推导出透视变形 ellipse_params = self._compute_ellipse_params(H, center_x, center_y) if self.logger: self.logger.info(f"[ARUCO] 靶心计算成功: 中心=({center_x:.1f}, {center_y:.1f}), " f"半径={radius}px, 检测到{len(marker_centers)}个标记") return (int(center_x), int(center_y)), radius, "aruco", ellipse_params except Exception as e: if self.logger: self.logger.error(f"[ARUCO] 计算靶心失败: {e}") import traceback self.logger.error(traceback.format_exc()) return None, None, None, None def _compute_ellipse_params(self, H, center_x, center_y): """ 从单应性矩阵计算椭圆参数,用于透视校正 Args: H: 单应性矩阵 (3x3) center_x, center_y: 靶心图像坐标 Returns: ellipse_params: ((center_x, center_y), (width, height), angle) """ try: # 在物理坐标系中画一个圆,投影到图像中看变成什么形状 # 物理圆:半径10mm r_mm = 10.0 angles = np.linspace(0, 2*np.pi, 16) circle_mm = np.array([[self.target_center_offset_mm[0] + r_mm * np.cos(a), self.target_center_offset_mm[1] + r_mm * np.sin(a)] for a in angles], dtype=np.float32) circle_mm = circle_mm.reshape(-1, 1, 2) # 投影到图像 circle_img = cv2.perspectiveTransform(circle_mm, H) circle_img = circle_img.reshape(-1, 2) # 拟合椭圆 if len(circle_img) >= 5: ellipse = cv2.fitEllipse(circle_img.astype(np.float32)) return ellipse else: # 从单应性矩阵近似估计 # 提取缩放和旋转 # H = K * [R|t] 的近似 # 这里简化处理:假设没有严重变形 scale_x = np.linalg.norm(H[0, :2]) scale_y = np.linalg.norm(H[1, :2]) avg_scale = (scale_x + scale_y) / 2 width = r_mm * 2 * scale_x height = r_mm * 2 * scale_y angle = np.degrees(np.arctan2(H[1, 0], H[0, 0])) return ((center_x, center_y), (width, height), angle) except Exception as e: if self.logger: self.logger.debug(f"[ARUCO] 计算椭圆参数失败: {e}") return None def transform_laser_point(self, laser_point, corners, ids): """ 将激光点从图像坐标转换到物理坐标(毫米),再计算相对于靶心的偏移 Args: laser_point: (x, y) 激光点在图像中的坐标 corners: 检测到的标记角点 ids: 检测到的标记ID Returns: (dx_mm, dy_mm) 激光点相对于靶心的偏移(毫米),或 (None, None) """ if laser_point is None or ids is None or len(ids) < 3: return None, None try: # 重新计算单应性矩阵(可以优化为缓存) detected_ids = ids.flatten().tolist() image_points = [] object_points = [] for i, marker_id in enumerate(detected_ids): if marker_id not in self.marker_ids: continue corner = corners[i][0] center_x = np.mean(corner[:, 0]) center_y = np.mean(corner[:, 1]) image_points.append([center_x, center_y]) object_points.append(self.marker_positions_mm[marker_id]) if len(image_points) < 3: return None, None image_points = np.array(image_points, dtype=np.float32) object_points = np.array(object_points, dtype=np.float32) H, _ = cv2.findHomography(object_points, image_points, cv2.RANSAC, 5.0) if H is None: return None, None # 求逆矩阵,将图像坐标转换到物理坐标 H_inv = np.linalg.inv(H) laser_img = np.array([[laser_point[0], laser_point[1]]], dtype=np.float32) laser_img = laser_img.reshape(-1, 1, 2) laser_mm = cv2.perspectiveTransform(laser_img, H_inv) laser_x_mm = laser_mm[0][0][0] laser_y_mm = laser_mm[0][0][1] # 计算相对于靶心的偏移 # 注意:Y轴方向可能需要翻转(图像Y向下,物理Y通常向上) dx_mm = laser_x_mm - self.target_center_offset_mm[0] dy_mm = -(laser_y_mm - self.target_center_offset_mm[1]) # 翻转Y轴 if self.logger: self.logger.debug(f"[ARUCO] 激光点转换: 图像({laser_point[0]:.1f}, {laser_point[1]:.1f}) -> " f"物理({laser_x_mm:.1f}, {laser_y_mm:.1f}) -> " f"偏移({dx_mm:.1f}, {dy_mm:.1f})mm") return dx_mm, dy_mm except Exception as e: if self.logger: self.logger.error(f"[ARUCO] 激光点转换失败: {e}") return None, None def draw_debug_info(self, frame, corners, ids, target_center=None, laser_point=None): """ 在图像上绘制调试信息 Args: frame: MaixPy图像帧 corners: 标记角点 ids: 标记ID target_center: 计算的靶心位置 laser_point: 激光点位置 Returns: 绘制后的图像 """ try: from maix import image img_cv = image.image2cv(frame, False, False).copy() # 绘制检测到的标记 if ids is not None: cv2.aruco.drawDetectedMarkers(img_cv, corners, ids) # 绘制标记ID和中心 for i, marker_id in enumerate(ids.flatten()): corner = corners[i][0] center_x = int(np.mean(corner[:, 0])) center_y = int(np.mean(corner[:, 1])) # 绘制中心点 cv2.circle(img_cv, (center_x, center_y), 5, (0, 255, 0), -1) # 绘制ID cv2.putText(img_cv, f"ID:{marker_id}", (center_x + 10, center_y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 绘制靶心 if target_center: cv2.circle(img_cv, target_center, 8, (255, 0, 0), -1) cv2.circle(img_cv, target_center, 50, (255, 0, 0), 2) cv2.putText(img_cv, "TARGET", (target_center[0] + 15, target_center[1] - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2) # 绘制激光点 if laser_point: cv2.circle(img_cv, (int(laser_point[0]), int(laser_point[1])), 6, (0, 0, 255), -1) cv2.putText(img_cv, "LASER", (int(laser_point[0]) + 10, int(laser_point[1]) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) # 转换回MaixPy图像 return image.cv2image(img_cv, False, False) except Exception as e: if self.logger: self.logger.error(f"[ARUCO] 绘制调试信息失败: {e}") return frame # 创建全局单例实例 aruco_detector = ArUcoDetector() def detect_target_with_aruco(frame, laser_point=None): """ 使用ArUco标记检测靶心的便捷函数 Args: frame: MaixPy图像帧 laser_point: 激光点坐标(可选) Returns: (result_img, center, radius, method, best_radius1, ellipse_params) 与detect_circle_v3保持相同的返回格式 """ detector = aruco_detector # 检测ArUco标记 corners, ids, rejected = detector.detect_markers(frame) # 计算靶心 center, radius, method, ellipse_params = detector.get_target_center_from_markers(corners, ids) # 绘制调试信息 result_img = detector.draw_debug_info(frame, corners, ids, center, laser_point) # 返回与detect_circle_v3相同的格式 # best_radius1用于距离估算,这里用radius代替 return result_img, center, radius, method, radius, ellipse_params def compute_laser_offset_aruco(laser_point, corners, ids): """ 使用ArUco计算激光点相对于靶心的偏移(毫米) Args: laser_point: (x, y) 激光点图像坐标 corners: ArUco标记角点 ids: ArUco标记ID Returns: (dx_mm, dy_mm) 偏移量(毫米),或 (None, None) """ return aruco_detector.transform_laser_point(laser_point, corners, ids)