421 lines
17 KiB
Python
421 lines
17 KiB
Python
#!/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)
|