Files
archery/aruco_detector.py
2026-03-24 10:18:48 +08:00

421 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)