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)
|