add ArUco but no activated

This commit is contained in:
gcw_4spBpAfv
2026-03-24 10:18:48 +08:00
parent d1ae364dbd
commit 704b20cde1
9 changed files with 1394 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
id: t11
name: t11
version: 1.2.9
version: 1.2.10
author: t11
icon: ''
desc: t11

420
aruco_detector.py Normal file
View File

@@ -0,0 +1,420 @@
#!/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)

View File

@@ -117,7 +117,7 @@ SAVE_IMAGE_ENABLED = True # 是否保存图像True=保存False=不保存
PHOTO_DIR = "/root/phot" # 照片存储目录
MAX_IMAGES = 1000
SHOW_CAMERA_PHOTO_WHILE_SHOOTING = True # 是否在拍摄时显示摄像头图像True=显示False=不显示建议在连着USB测试过程中打开
SHOW_CAMERA_PHOTO_WHILE_SHOOTING = False # 是否在拍摄时显示摄像头图像True=显示False=不显示建议在连着USB测试过程中打开
# ==================== OTA配置 ====================
MAX_BACKUPS = 5
@@ -149,6 +149,42 @@ PIN_MAPPINGS_WITH_WIFI = {
# 根据WiFi模块开关选择引脚映射
PIN_MAPPINGS = PIN_MAPPINGS_WITH_WIFI if HAS_WIFI_MODULE else PIN_MAPPINGS_NO_WIFI
# ==================== ArUco标定配置 ====================
USE_ARUCO = False # 是否使用ArUco标定True=使用ArUcoFalse=使用传统黄色靶心检测)
# ArUco标记配置
if USE_ARUCO:
import cv2
ARUCO_DICT_TYPE = cv2.aruco.DICT_4X4_50 # ArUco字典类型
ARUCO_MARKER_SIZE_MM = 40 # ArUco标记边长毫米
ARUCO_MARKER_IDS = [0, 1, 2, 3] # 四个角的ArUco标记ID
# 靶纸物理尺寸(毫米)
TARGET_PAPER_SIZE_MM = 400 # 靶纸边长 400mm x 400mm
# ArUco标记在靶纸上的中心坐标毫米以靶纸中心为原点
# 靶纸坐标系:中心(0,0)X向右Y向下图像坐标系
# 四个角位置:(20,20), (20,380), (380,380), (380,20)
# 转换为以中心为原点的坐标:
# 左上角(0): (-180, -180) -> 实际(20,20)相对于中心(200,200) = (-180,-180)
# 右上角(1): (180, -180) -> 实际(380,20)相对于中心 = (180,-180)
# 右下角(2): (180, 180) -> 实际(380,380)相对于中心 = (180,180)
# 左下角(3): (-180, 180) -> 实际(20,380)相对于中心 = (-180,180)
ARUCO_MARKER_POSITIONS_MM = {
0: (-180, -180), # 左上角
1: (180, -180), # 右上角
2: (180, 180), # 右下角
3: (-180, 180), # 左下角
}
# 靶心(黄心)在靶纸上的位置(毫米,相对于靶纸中心)
# 标准靶纸靶心就在正中心
TARGET_CENTER_OFFSET_MM = (0, 0)
# ArUco检测参数
ARUCO_MIN_MARKER_PERIMETER_RATE = 0.03 # 最小标记周长比例(相对于图像)
ARUCO_CORNER_REFINEMENT_METHOD = cv2.aruco.CORNER_REFINE_SUBPIX # 角点精修方法
# ==================== 电源配置 ====================
AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间0表示不自动关机

View File

@@ -8,7 +8,7 @@ import _thread
import json
import os
import binascii
from maix import time, camera
from maix import time
import threading
import config
from logger_manager import logger_manager
@@ -861,7 +861,8 @@ class LaserManager:
center_temp = None
radius_temp = None
if config.LASER_REQUIRE_IN_ELLIPSE:
result_img_temp, center_temp, radius_temp, method_temp, best_radius1_temp, ellipse_params_temp = vision.detect_circle_v3(frame, None)
# 使用统一的检测接口支持ArUco和传统方法
result_img_temp, center_temp, radius_temp, method_temp, best_radius1_temp, ellipse_params_temp = vision.detect_target(frame, None)
# 只有检测到靶心时才继续处理激光点
if center_temp is None or radius_temp is None:
@@ -1114,7 +1115,7 @@ class LaserManager:
return self._laser_point is not None
def compute_laser_position(self, circle_center, laser_point, radius, method):
def compute_laser_position(self, circle_center, laser_point, radius, method, ellipse_params=None):
"""计算激光相对于靶心的偏移量(单位:厘米)
Args:
@@ -1122,6 +1123,7 @@ class LaserManager:
laser_point: 激光点坐标 (x, y)
radius: 靶心半径(像素)
method: 检测方法("模糊" 或其他)
ellipse_params: 椭圆参数,用于透视校正(可选)
Returns:
(dx, dy): 激光相对于靶心的偏移量(厘米),如果输入无效则返回 (None, None)
@@ -1131,6 +1133,14 @@ class LaserManager:
cx, cy = circle_center
lx, ly = laser_point
# 如果有椭圆参数,使用透视校正计算
if ellipse_params is not None and method != "aruco":
return self._compute_with_perspective_correction(
circle_center, laser_point, radius, ellipse_params
)
# 传统计算方法
# r = 22.16 * 5
r = radius * 5
self.logger.debug(f"compute_laser_position: circle_center: {circle_center} laser_point: {laser_point} radius: {radius} method: {method} r: {r}")
@@ -1139,6 +1149,87 @@ class LaserManager:
self.logger.info(f"lx{lx} ly: {ly} cx: {cx} cy: {cy} result_x: {target_x} result_y: {-target_y} real_r_x: {lx-cx} real_r_y: {-1*(ly-cy)}")
return (target_x, -target_y)
def _compute_with_perspective_correction(self, circle_center, laser_point, radius, ellipse_params):
"""
使用透视校正计算激光偏移
当相机不正对靶子时,圆会变成椭圆。使用椭圆参数进行透视校正,
将图像坐标转换到物理坐标系,再计算偏移。
Args:
circle_center: 靶心中心坐标 (x, y)
laser_point: 激光点坐标 (x, y)
radius: 靶心半径(像素)
ellipse_params: ((center_x, center_y), (width, height), angle)
Returns:
(dx, dy): 校正后的偏移量(厘米)
"""
import math
try:
(ex, ey), (width, height), angle = ellipse_params
cx, cy = circle_center
lx, ly = laser_point
# 步骤1: 平移到椭圆中心
dx1 = lx - cx
dy1 = ly - cy
# 步骤2: 旋转坐标系,使椭圆轴与坐标轴对齐
angle_rad = math.radians(-angle)
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
x_rot = dx1 * cos_a - dy1 * sin_a
y_rot = dx1 * sin_a + dy1 * cos_a
# 步骤3: 归一化到单位圆
# 椭圆半轴
a = width / 2.0
b = height / 2.0
# 归一化坐标
if a > 0 and b > 0:
x_norm = x_rot / a
y_norm = y_rot / b
else:
x_norm = x_rot
y_norm = y_rot
# 步骤4: 计算物理距离
# 使用平均半径作为参考
avg_radius = (a + b) / 2.0
pixels_per_cm = avg_radius / 20.0 # 假设靶心半径20cm对应avg_radius像素
if pixels_per_cm > 0:
# 归一化距离(单位:靶心半径的倍数)
norm_distance = math.sqrt(x_norm**2 + y_norm**2)
# 转换为厘米假设靶心半径20cm
distance_cm = norm_distance * 20.0
# 计算方向
angle_offset = math.atan2(y_norm, x_norm)
dx_cm = distance_cm * math.cos(angle_offset)
dy_cm = -distance_cm * math.sin(angle_offset) # Y轴翻转
self.logger.debug(f"[PERSPECTIVE] 原始偏移: ({dx1:.1f}, {dy1:.1f})px, "
f"校正后: ({dx_cm:.2f}, {dy_cm:.2f})cm, "
f"椭圆: ({width:.1f}, {height:.1f}), 角度: {angle:.1f}°")
return (dx_cm, dy_cm)
else:
# 回退到直接计算
r = radius * 5
return ((lx-cx)/r*100, -(ly-cy)/r*100)
except Exception as e:
self.logger.error(f"[PERSPECTIVE] 透视校正计算失败: {e}")
# 回退到直接计算
r = radius * 5
return ((lx-cx)/r*100, -(ly-cy)/r*100)
# # 根据检测方法动态调整靶心物理半径(简化模型)
# circle_r = (radius / 4.0) * 20.0 if method == "模糊" else (68 / 16.0) * 20.0
# dx = lx - cx

View File

@@ -1073,7 +1073,6 @@ class NetworkManager:
def tcp_main(self):
"""TCP 主通信循环:登录、心跳、处理指令、发送数据"""
import _thread
from maix import camera
self.logger.info("[NET] TCP主线程启动")

17
test/test_cammera.py Normal file
View File

@@ -0,0 +1,17 @@
# test_camera.py
from maix import camera, display, time
try:
print("Initializing camera...")
cam = camera.Camera(640, 480)
print("Camera initialized successfully!")
disp = display.Display()
while True:
frame = cam.read()
disp.show(frame)
time.sleep_ms(50)
except Exception as e:
print(f"Error: {e}")

620
test/test_decect_circle.py Normal file
View File

@@ -0,0 +1,620 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
离线测试脚本:直接复用 detect_circle 逻辑进行测试
运行环境MaixPy (Sipeed MAIX)
"""
import sys
import os
# import time
from maix import image,time
import cv2
import numpy as np
# ==================== 全局配置 (与 test_main.py 保持一致) ====================
REAL_RADIUS_CM = 20 # 靶心实际半径(厘米)
# ==================== 复制的核心算法 ====================
# 注意:这里直接复制了 detect_circle 的逻辑,避免 import main 导致的冲突
def detect_circle_v3(frame, laser_point=None):
"""检测图像中的靶心(优先清晰轮廓,其次黄色区域)- 返回椭圆参数版本
增加红色圆圈检测,验证黄色圆圈是否为真正的靶心
如果提供 laser_point会选择最接近激光点的目标
Args:
frame: 图像帧
laser_point: 激光点坐标 (x, y),用于多目标场景下的目标选择
Returns:
(result_img, best_center, best_radius, method, best_radius1, ellipse_params)
"""
img_cv = image.image2cv(frame, False, False)
best_center = best_radius = best_radius1 = method = None
ellipse_params = None
# HSV 黄色掩码检测(模糊靶心)
hsv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2HSV)
h, s, v = cv2.split(hsv)
# 调整饱和度策略:稍微增强,不要过度
s = np.clip(s * 1.1, 0, 255).astype(np.uint8)
hsv = cv2.merge((h, s, v))
# 放宽 HSV 阈值范围(针对模糊图像的关键调整)
lower_yellow = np.array([7, 80, 0]) # 饱和度下限降低,捕捉淡黄色
upper_yellow = np.array([32, 255, 255]) # 亮度上限拉满
mask_yellow = cv2.inRange(hsv, lower_yellow, upper_yellow)
# 调整形态学操作
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_CLOSE, kernel)
contours_yellow, _ = cv2.findContours(mask_yellow, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 存储所有有效的黄色-红色组合
valid_targets = []
if contours_yellow:
for cnt_yellow in contours_yellow:
area = cv2.contourArea(cnt_yellow)
perimeter = cv2.arcLength(cnt_yellow, True)
# 计算圆度
if perimeter > 0:
circularity = (4 * np.pi * area) / (perimeter * perimeter)
else:
circularity = 0
logger = get_logger()
if area > 50 and circularity > 0.7:
if logger:
logger.info(f"[target] -> 面积:{area}, 圆度:{circularity:.2f}")
# 尝试拟合椭圆
yellow_center = None
yellow_radius = None
yellow_ellipse = None
if len(cnt_yellow) >= 5:
(x, y), (width, height), angle = cv2.fitEllipse(cnt_yellow)
yellow_ellipse = ((x, y), (width, height), angle)
axes_minor = min(width, height)
radius = axes_minor / 2
yellow_center = (int(x), int(y))
yellow_radius = int(radius)
else:
(x, y), radius = cv2.minEnclosingCircle(cnt_yellow)
yellow_center = (int(x), int(y))
yellow_radius = int(radius)
yellow_ellipse = None
# 如果检测到黄色圆圈,再检测红色圆圈进行验证
if yellow_center and yellow_radius:
# HSV 红色掩码检测红色在HSV中跨越0度需要两个范围
# 红色范围1: 0-10度接近0度的红色
lower_red1 = np.array([0, 80, 0])
upper_red1 = np.array([10, 255, 255])
mask_red1 = cv2.inRange(hsv, lower_red1, upper_red1)
# 红色范围2: 170-180度接近180度的红色
lower_red2 = np.array([170, 80, 0])
upper_red2 = np.array([180, 255, 255])
mask_red2 = cv2.inRange(hsv, lower_red2, upper_red2)
# 合并两个红色掩码
mask_red = cv2.bitwise_or(mask_red1, mask_red2)
# 形态学操作
kernel_red = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask_red = cv2.morphologyEx(mask_red, cv2.MORPH_CLOSE, kernel_red)
contours_red, _ = cv2.findContours(mask_red, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
found_valid_red = False
if contours_red:
# 找到所有符合条件的红色圆圈
for cnt_red in contours_red:
area_red = cv2.contourArea(cnt_red)
perimeter_red = cv2.arcLength(cnt_red, True)
if perimeter_red > 0:
circularity_red = (4 * np.pi * area_red) / (perimeter_red * perimeter_red)
else:
circularity_red = 0
# 红色圆圈也应该有一定的圆度
if area_red > 50 and circularity_red > 0.6:
# 计算红色圆圈的中心和半径
if len(cnt_red) >= 5:
(x_red, y_red), (w_red, h_red), angle_red = cv2.fitEllipse(cnt_red)
radius_red = min(w_red, h_red) / 2
red_center = (int(x_red), int(y_red))
red_radius = int(radius_red)
else:
(x_red, y_red), radius_red = cv2.minEnclosingCircle(cnt_red)
red_center = (int(x_red), int(y_red))
red_radius = int(radius_red)
# 计算黄色和红色圆心的距离
if red_center:
dx = yellow_center[0] - red_center[0]
dy = yellow_center[1] - red_center[1]
distance = np.sqrt(dx*dx + dy*dy)
# 圆心距离阈值应该小于黄色半径的某个倍数比如1.5倍)
max_distance = yellow_radius * 1.5
# 红色圆圈应该比黄色圆圈大(外圈)
if distance < max_distance and red_radius > yellow_radius * 0.8:
found_valid_red = True
logger = get_logger()
if logger:
logger.info(f"[target] -> 找到匹配的红圈: 黄心({yellow_center}), 红心({red_center}), 距离:{distance:.1f}, 黄半径:{yellow_radius}, 红半径:{red_radius}")
# 记录这个有效目标
valid_targets.append({
'center': yellow_center,
'radius': yellow_radius,
'ellipse': yellow_ellipse,
'area': area
})
break
if not found_valid_red:
logger = get_logger()
if logger:
logger.debug("Debug -> 未找到匹配的红色圆圈,可能是误识别")
# 从所有有效目标中选择最佳目标
if valid_targets:
if laser_point:
# 如果有激光点,选择最接近激光点的目标
best_target = None
min_distance = float('inf')
for target in valid_targets:
dx = target['center'][0] - laser_point[0]
dy = target['center'][1] - laser_point[1]
distance = np.sqrt(dx*dx + dy*dy)
if distance < min_distance:
min_distance = distance
best_target = target
if best_target:
best_center = best_target['center']
best_radius = best_target['radius']
ellipse_params = best_target['ellipse']
method = "v3_ellipse_red_validated_laser_selected"
best_radius1 = best_radius * 5
else:
# 如果没有激光点,选择面积最大的目标
best_target = max(valid_targets, key=lambda t: t['area'])
best_center = best_target['center']
best_radius = best_target['radius']
ellipse_params = best_target['ellipse']
method = "v3_ellipse_red_validated"
best_radius1 = best_radius * 5
result_img = image.cv2image(img_cv, False, False)
return result_img, best_center, best_radius, method, best_radius1, ellipse_params
def detect_circle(frame):
"""检测图像中的靶心(优先清晰轮廓,其次黄色区域)"""
img_cv = image.image2cv(frame, False, False)
# gray = cv2.cvtColor(img_cv, cv2.COLOR_RGB2GRAY)
# blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# edged = cv2.Canny(blurred, 50, 150)
# kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# ceroded = cv2.erode(cv2.dilate(edged, kernel), kernel)
# contours, _ = cv2.findContours(ceroded, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# best_center = best_radius = best_radius1 = method = None
# hsv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2HSV)
# h, s, v = cv2.split(hsv)
# s = np.clip(s * 2, 0, 255).astype(np.uint8)
# hsv = cv2.merge((h, s, v))
# lower_yellow = np.array([7, 80, 0])
# upper_yellow = np.array([32, 255, 182])
# mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
# kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# mask = cv2.morphologyEx(mask, cv2.MORPH_DILATE, kernel)
# contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# if contours:
# largest = max(contours, key=cv2.contourArea)
# if cv2.contourArea(largest) > 50:
# (x, y), radius = cv2.minEnclosingCircle(largest)
# best_center = (int(x), int(y))
# best_radius = int(radius)
# best_radius1 = radius * 5
# method = "v2"
# auto
# R:31 M:v2 D:2.410110127692767
# hsv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2HSV)
# h, s, v = cv2.split(hsv)
# # 1. 增强饱和度(模糊照片需要更强的增强)
# s = np.clip(s * 2.5, 0, 255).astype(np.uint8) # 从2.0改为2.5
# # 2. 增强亮度(模糊照片可能偏暗)
# v = np.clip(v * 1.2, 0, 255).astype(np.uint8) # 新增:提升亮度
# hsv = cv2.merge((h, s, v))
# # 3. 放宽HSV颜色范围特别是模糊照片
# # 降低饱和度下限,提高亮度上限
# lower_yellow = np.array([5, 50, 30]) # H:5-35, S:50-255, V:30-255
# upper_yellow = np.array([35, 255, 255])
# mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
# # 4. 增强形态学操作(连接被分割的区域)
# kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
# kernel_large = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9)) # 更大的核
# # 先开运算去除噪声
# mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_small)
# # 多次膨胀连接区域(模糊照片需要更多膨胀)
# mask = cv2.dilate(mask, kernel_large, iterations=2) # 增加迭代次数
# mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_large) # 闭运算填充空洞
# contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# if contours:
# largest = max(contours, key=cv2.contourArea)
# area = cv2.contourArea(largest)
# if area > 50:
# # 5. 使用面积计算等效半径(更准确)
# equivalent_radius = np.sqrt(area / np.pi)
# # 6. 同时使用minEnclosingCircle作为备选取较大值
# (x, y), enclosing_radius = cv2.minEnclosingCircle(largest)
# # 取两者中的较大值,确保不遗漏
# radius = max(equivalent_radius, enclosing_radius)
# best_center = (int(x), int(y))
# best_radius = int(radius)
# best_radius1 = radius * 5
# method = "v2"
# codegee
# R:24 M:v2 D:3.061493895819174
# R:22 M:v2 D:3.3644971681267077 np.clip(s * 1.1, 0, 255)
hsv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2HSV)
h, s, v = cv2.split(hsv)
# 2. 调整饱和度策略:
# 不要暴力翻倍,可以尝试稍微增强,或者使用 CLAHE 增强亮度/对比度
# 这里我们稍微增加一点饱和度,并确保不溢出
s = np.clip(s * 1.1, 0, 255).astype(np.uint8)
# 对亮度通道 v 也可以做一点 CLAHE 处理来增强对比度(可选)
# clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
# v = clahe.apply(v)
hsv = cv2.merge((h, s, v))
# 3. 放宽 HSV 阈值范围(针对模糊图像的关键调整)
# 降低 S 的下限 (80 -> 35),提高 V 的上限 (182 -> 255)
lower_yellow = np.array([7, 80, 0]) # 饱和度下限降低,捕捉淡黄色
upper_yellow = np.array([32, 255, 255]) # 亮度上限拉满
mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
# 4. 调整形态学操作
# 去掉 MORPH_OPEN因为它会减小面积。
# 使用 MORPH_CLOSE (先膨胀后腐蚀) 来填充内部小黑洞,连接近邻区域
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# 再进行一次膨胀,确保边缘被包含进来
# mask = cv2.dilate(mask, kernel, iterations=1)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
largest = max(contours, key=cv2.contourArea)
# 这里可以适当降低面积阈值,或者保持不变
if cv2.contourArea(largest) > 50:
# (x, y), radius = cv2.minEnclosingCircle(largest)
# best_center = (int(x), int(y))
# best_radius = int(radius)
# --- 核心修改开始 ---
# 1. 尝试拟合椭圆 (需要轮廓点至少为5个)
if len(largest) >= 5:
# 返回值: ((中心x, 中心y), (长轴, 短轴), 旋转角度)
(x, y), (axes_major, axes_minor), angle = cv2.fitEllipse(largest)
# 2. 计算半径
# 选项A取长短轴的平均值 (比较稳健)
# radius = (axes_major + axes_minor) / 4
# 选项B直接取短轴的一半 (抗模糊最强,推荐)
radius = axes_minor / 2
best_center = (int(x), int(y))
best_radius = int(radius)
method = "v2_ellipse"
else:
# 如果点太少无法拟合椭圆,降级回 minEnclosingCircle
(x, y), radius = cv2.minEnclosingCircle(largest)
best_center = (int(x), int(y))
best_radius = int(radius)
method = "v2"
# --- 核心修改结束 ---
# 你的后续逻辑
best_radius1 = radius * 5
# operas 4.5
# R:25 M:v2 D:2.9554872521538527
# hsv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2HSV)
# h, s, v = cv2.split(hsv)
# # 1. 适度增强饱和度(不要过度,否则噪声也会增强)
# s = np.clip(s * 1.5, 0, 255).astype(np.uint8)
# hsv = cv2.merge((h, s, v))
# # 2. 放宽 HSV 阈值范围(关键改动)
# # - 饱和度下限从 80 降到 40捕捉淡黄色
# # - 亮度上限从 182 提高到 255允许更亮的黄色
# lower_yellow = np.array([7, 40, 30])
# upper_yellow = np.array([35, 255, 255])
# mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
# # 3. 调整形态学操作:用 CLOSE 替代 OPEN
# # CLOSE先膨胀后腐蚀填充内部空洞连接相邻区域
# # OPEN先腐蚀后膨胀会缩小区域不适合模糊图像
# kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) # 稍大的核
# mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
# mask = cv2.dilate(mask, kernel, iterations=1) # 额外膨胀,确保边缘被包含
# contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# if contours:
# largest = max(contours, key=cv2.contourArea)
# if cv2.contourArea(largest) > 50:
# (x, y), radius = cv2.minEnclosingCircle(largest)
# best_center = (int(x), int(y))
# best_radius = int(radius)
# best_radius1 = radius * 5
# method = "v2"
# # --- 新增:将 Mask 叠加到原图上用于调试 ---
# # 创建一个彩色掩码红色通道为255其他为0
# mask_overlay = np.zeros_like(img_cv)
# mask_overlay[:, :, 2] = mask # 将掩码放在红色通道 (BGR中的R)
#
# cv2.addWeighted(img_cv, 0.6, mask_overlay, 0.4, 0, img_cv)
result_img = image.cv2image(img_cv, False, False)
return result_img, best_center, best_radius, method, best_radius1
def detect_circle_v2(frame):
"""检测图像中的靶心(优先清晰轮廓,其次黄色区域)- 返回椭圆参数版本"""
global REAL_RADIUS_CM
img_cv = image.image2cv(frame, False, False)
best_center = best_radius = best_radius1 = method = None
ellipse_params = None # 存储椭圆参数 ((x, y), (axes_major, axes_minor), angle)
# HSV 黄色掩码检测(模糊靶心)
hsv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2HSV)
h, s, v = cv2.split(hsv)
# 调整饱和度策略:稍微增强,不要过度
s = np.clip(s * 1.1, 0, 255).astype(np.uint8)
hsv = cv2.merge((h, s, v))
# 放宽 HSV 阈值范围(针对模糊图像的关键调整)
lower_yellow = np.array([7, 80, 0]) # 饱和度下限降低,捕捉淡黄色
upper_yellow = np.array([32, 255, 255]) # 亮度上限拉满
mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
# 调整形态学操作
# 使用 MORPH_CLOSE (先膨胀后腐蚀) 来填充内部小黑洞,连接近邻区域
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
largest = max(contours, key=cv2.contourArea)
if cv2.contourArea(largest) > 50:
# 尝试拟合椭圆 (需要轮廓点至少为5个)
if len(largest) >= 5:
# 返回值: ((中心x, 中心y), (width, height), 旋转角度)
# 注意width 和 height 是外接矩形的尺寸,不是长轴和短轴
(x, y), (width, height), angle = cv2.fitEllipse(largest)
# 保存椭圆参数(保持原始顺序,用于绘制)
ellipse_params = ((x, y), (width, height), angle)
# 计算半径:使用较小的尺寸作为短轴
axes_minor = min(width, height)
radius = axes_minor / 2
best_center = (int(x), int(y))
best_radius = int(radius)
method = "v2_ellipse"
else:
# 如果点太少无法拟合椭圆,降级回 minEnclosingCircle
(x, y), radius = cv2.minEnclosingCircle(largest)
best_center = (int(x), int(y))
best_radius = int(radius)
method = "v2"
ellipse_params = None # 圆形,没有椭圆参数
best_radius1 = radius * 5
result_img = image.cv2image(img_cv, False, False)
return result_img, best_center, best_radius, method, best_radius1, ellipse_params
# ==================== 测试逻辑 ====================
def run_offline_test(image_path):
"""读取图片,检测圆,绘制结果,保存图片"""
# 1. 检查文件是否存在
if not os.path.exists(image_path):
print(f"[ERROR] 找不到图片文件: {image_path}")
return
# 2. 使用 maix.image 读取图片 (适配 MaixPy v4)
try:
# 使用 image.load 读取文件,返回 Image 对象
img = image.load(image_path)
print(f"[INFO] 成功读取图片: {image_path} (尺寸: {img.width()}x{img.height()})")
except Exception as e:
print(f"[ERROR] 读取图片失败: {e}")
print("提示:请确认 MaixPy 版本是否为 v4且图片路径正确。")
return
# 3. 调用 detect_circle_v2 函数
print("[INFO] 正在调用 detect_circle_v2 进行检测...")
start_time = time.ticks_ms()
result_img, center, radius, method, radius1, ellipse_params = detect_circle_v3(img)
cost_time = time.ticks_ms() - start_time
print(f"[INFO] 检测完成,耗时: {cost_time}ms")
print(f" 结果 -> 圆心: {center}, 半径: {radius}, 方法: {method}")
if ellipse_params:
(ell_center, (width, height), angle) = ellipse_params
print(f" 椭圆 -> 中心: ({ell_center[0]:.1f}, {ell_center[1]:.1f}), 长轴: {max(width, height):.1f}, 短轴: {min(width, height):.1f}, 角度: {angle:.1f}°")
# 4. 绘制辅助线(可选,用于调试)
if center and radius:
# 为了绘制椭圆,需要转换回 cv2 图像
img_cv = image.image2cv(result_img, False, False)
cx, cy = center
# 如果有椭圆参数,绘制椭圆
if ellipse_params:
(ell_center, (width, height), angle) = ellipse_params
cx_ell, cy_ell = int(ell_center[0]), int(ell_center[1])
# 确定长轴和短轴
if width >= height:
# width 是长轴height 是短轴
axes_major = width
axes_minor = height
major_angle = angle # 长轴角度就是 angle
minor_angle = angle + 90 # 短轴角度 = 长轴角度 + 90度
else:
# height 是长轴width 是短轴
axes_major = height
axes_minor = width
major_angle = angle + 90 # 长轴角度 = width角度 + 90度
minor_angle = angle # 短轴角度就是 angle
# 使用 OpenCV 绘制椭圆绿色线宽2
cv2.ellipse(img_cv,
(cx_ell, cy_ell), # 中心点
(int(width/2), int(height/2)), # 半宽、半高
angle, # 旋转角度OpenCV需要原始angle
0, 360, # 起始和结束角度
(0, 255, 0), # 绿色 (RGB格式)
2) # 线宽
# 绘制椭圆中心点(红色)
cv2.circle(img_cv, (cx_ell, cy_ell), 3, (255, 0, 0), -1)
import math
# 绘制短轴(蓝色线条)
minor_length = axes_minor / 2
minor_angle_rad = math.radians(minor_angle)
dx_minor = minor_length * math.cos(minor_angle_rad)
dy_minor = minor_length * math.sin(minor_angle_rad)
pt1_minor = (int(cx_ell - dx_minor), int(cy_ell - dy_minor))
pt2_minor = (int(cx_ell + dx_minor), int(cy_ell + dy_minor))
cv2.line(img_cv, pt1_minor, pt2_minor, (0, 0, 255), 2) # 蓝色 (RGB格式)
else:
# 如果没有椭圆参数,绘制圆形(红色)
cv2.circle(img_cv, (cx, cy), radius, (0, 0, 255), 2)
cv2.circle(img_cv, (cx, cy), 2, (0, 0, 255), -1)
# 转换回 maix image
result_img = image.cv2image(img_cv, False, False)
# 定义颜色对象用于文字
try:
color_black = image.Color.from_rgb(0,0,0)
except AttributeError:
color_black = image.Color(0,0,0)
# D. 添加文字信息
FOCAL_LENGTH_PIX = 1900
d = (REAL_RADIUS_CM * FOCAL_LENGTH_PIX) / radius1 / 100.0
info_str = f"R:{radius} M:{method} D:{d:.2f}"
print(info_str)
# 计算文字位置,防止超出图片边界
r_outer = int(radius * 11.0) if radius else 100
text_y = cy - r_outer - 20 if cy > r_outer + 20 else cy + r_outer + 20
# 调用 draw_string
result_img.draw_string(0, 0, info_str, color=color_black, scale=1.0)
# 5. 保存结果图片
output_path = image_path.replace(".bmp", "_result.bmp")
output_path = image_path.replace(".jpg", "_result.jpg")
try:
result_img.save(output_path, quality=100)
print(f"[SUCCESS] 结果已保存至: {output_path}")
except Exception as e:
print(f"[ERROR] 保存图片失败: {e}")
if __name__ == "__main__":
# ================= 配置区域 =================
# 1. 设置要测试的图片路径
# 建议将图片放在与脚本同级目录,或者使用绝对路径
TARGET_IMAGE = "/root/phot/None_314_258_0_0041.bmp"
# TARGET_DIR = "/root/phot_test2" # 修改为你想要读取的目录路径
# 支持的图片格式
IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp']
# ================= 执行区域 =================
if 'TARGET_DIR' in locals():
# 读取目录下所有图片文件,过滤掉 _result.jpg 后缀的文件
image_files = []
if os.path.exists(TARGET_DIR) and os.path.isdir(TARGET_DIR):
for filename in os.listdir(TARGET_DIR):
# 检查文件扩展名
if any(filename.lower().endswith(ext) for ext in IMAGE_EXTENSIONS):
# 过滤掉 _result.jpg 后缀的文件
if not filename.endswith('_result.jpg'):
filepath = os.path.join(TARGET_DIR, filename)
if os.path.isfile(filepath):
image_files.append(filepath)
# 按文件名排序(可选)
image_files.sort()
print(f"[INFO] 在目录 {TARGET_DIR} 中找到 {len(image_files)} 张图片")
# 处理每张图片
for img_path in image_files:
print(f"\n{'='*10} 开始处理: {img_path} {'='*10}")
run_offline_test(img_path)
else:
print(f"[ERROR] 目录不存在或不是有效目录: {TARGET_DIR}")
else:
run_offline_test(TARGET_IMAGE)

172
test/test_laser.py Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
激光模块测试脚本
用于诊断激光开关问题
使用方法:
python test_laser.py
功能:
1. 初始化串口
2. 循环测试激光开/关
3. 打印详细调试信息
"""
from maix import uart, pinmap, time
# ==================== 配置 ====================
UART_PORT = "/dev/ttyS1" # 激光模块连接的串口UART1
BAUDRATE = 9600 # 波特率
# 引脚映射(确保与硬件连接一致)
print("=" * 50)
print("🔧 步骤1: 配置引脚映射")
print("=" * 50)
try:
pinmap.set_pin_function("A18", "UART1_RX")
print("✅ A18 -> UART1_RX")
except Exception as e:
print(f"❌ A18 配置失败: {e}")
try:
pinmap.set_pin_function("A19", "UART1_TX")
print("✅ A19 -> UART1_TX")
except Exception as e:
print(f"❌ A19 配置失败: {e}")
# ==================== 激光控制指令 ====================
MODULE_ADDR = 0x00
# 原始命令
LASER_ON_CMD = bytes([0xAA, MODULE_ADDR, 0x01, 0xBE, 0x00, 0x01, 0x00, 0x01, 0xC1])
LASER_OFF_CMD = bytes([0xAA, MODULE_ADDR, 0x01, 0xBE, 0x00, 0x01, 0x00, 0x00, 0xC0])
# 备用命令格式(如果原始命令不工作,可以尝试这些)
# 格式1: 简化命令
LASER_ON_CMD_ALT1 = bytes([0xAA, 0x01, 0x01])
LASER_OFF_CMD_ALT1 = bytes([0xAA, 0x01, 0x00])
# 格式2: 不同的协议头
LASER_ON_CMD_ALT2 = bytes([0x55, 0xAA, 0x01])
LASER_OFF_CMD_ALT2 = bytes([0x55, 0xAA, 0x00])
print("\n" + "=" * 50)
print("🔧 步骤2: 初始化串口")
print("=" * 50)
print(f"设备: {UART_PORT}")
print(f"波特率: {BAUDRATE}")
try:
laser_uart = uart.UART(UART_PORT, BAUDRATE)
print(f"✅ 串口初始化成功: {laser_uart}")
except Exception as e:
print(f"❌ 串口初始化失败: {e}")
exit(1)
# ==================== 测试函数 ====================
def send_and_check(cmd, name):
"""发送命令并检查回包"""
print(f"\n📤 发送: {name}")
print(f" 命令字节: {cmd.hex()}")
print(f" 命令长度: {len(cmd)} 字节")
# 清空接收缓冲区
try:
old_data = laser_uart.read(-1)
if old_data:
print(f" 清空缓冲区: {len(old_data)} 字节")
except:
pass
# 发送命令
try:
written = laser_uart.write(cmd)
print(f" 写入字节数: {written}")
except Exception as e:
print(f" ❌ 写入失败: {e}")
return None
# 等待响应
time.sleep_ms(100)
# 读取回包
try:
resp = laser_uart.read(50)
if resp:
print(f" 📥 收到回包: {resp.hex()} ({len(resp)} 字节)")
return resp
else:
print(f" ⚠️ 无回包")
return None
except Exception as e:
print(f" ❌ 读取失败: {e}")
return None
def test_laser_cycle(on_cmd, off_cmd, cmd_name="标准命令"):
"""测试一个开关周期"""
print(f"\n{'='*50}")
print(f"🧪 测试 {cmd_name}")
print(f"{'='*50}")
print("\n>>> 测试开启激光")
send_and_check(on_cmd, f"{cmd_name} - 开启")
print(" ⏱️ 等待 2 秒观察激光是否亮起...")
time.sleep(2)
print("\n>>> 测试关闭激光")
send_and_check(off_cmd, f"{cmd_name} - 关闭")
print(" ⏱️ 等待 2 秒观察激光是否熄灭...")
time.sleep(2)
# ==================== 主测试 ====================
print("\n" + "=" * 50)
print("🚀 开始激光测试")
print("=" * 50)
print("\n请观察激光模块的状态变化...")
print("测试将依次尝试不同的命令格式\n")
try:
# 测试1: 标准命令
test_laser_cycle(LASER_ON_CMD, LASER_OFF_CMD, "标准命令")
input("\n按回车继续测试备用命令1...")
# 测试2: 备用命令格式1
test_laser_cycle(LASER_ON_CMD_ALT1, LASER_OFF_CMD_ALT1, "备用命令1 (简化)")
input("\n按回车继续测试备用命令2...")
# 测试3: 备用命令格式2
test_laser_cycle(LASER_ON_CMD_ALT2, LASER_OFF_CMD_ALT2, "备用命令2 (0x55AA头)")
print("\n" + "=" * 50)
print("🏁 测试完成")
print("=" * 50)
print("\n诊断建议:")
print("1. 如果激光始终不亮/始终亮:")
print(" - 检查激光模块的电源连接")
print(" - 检查串口TX/RX是否接反")
print(" - 尝试不同的波特率 (4800/19200)")
print("")
print("2. 如果有回包但激光无反应:")
print(" - 命令格式可能正确但激光硬件问题")
print("")
print("3. 如果某个备用命令有效:")
print(" - 需要更新 config.py 中的命令格式")
except KeyboardInterrupt:
print("\n\n🛑 测试被中断")
# 确保激光关闭
laser_uart.write(LASER_OFF_CMD)
print("✅ 已发送关闭指令")
except Exception as e:
print(f"\n❌ 测试出错: {e}")
import traceback
traceback.print_exc()

View File

@@ -14,6 +14,10 @@ from maix import image
import config
from logger_manager import logger_manager
# 导入ArUco检测器如果启用
if config.USE_ARUCO:
from aruco_detector import detect_target_with_aruco, aruco_detector
# 存图队列 + worker
_save_queue = queue.Queue(maxsize=16)
_save_worker_started = False
@@ -749,3 +753,32 @@ def save_shot_image(result_img, center, radius, method, ellipse_params,
logger.error(f"[VISION] save_shot_image 转换图像失败: {e}")
return None
def detect_target(frame, laser_point=None):
"""
统一的靶心检测接口,根据配置自动选择检测方法
Args:
frame: MaixPy图像帧
laser_point: 激光点坐标(可选)
Returns:
(result_img, center, radius, method, best_radius1, ellipse_params)
与detect_circle_v3保持相同的返回格式
"""
logger = logger_manager.logger
if config.USE_ARUCO:
# 使用ArUco检测
if logger:
logger.debug("[VISION] 使用ArUco标记检测靶心")
# 延迟导入以避免循环依赖
from aruco_detector import detect_target_with_aruco
return detect_target_with_aruco(frame, laser_point)
else:
# 使用传统黄色靶心检测
if logger:
logger.debug("[VISION] 使用传统黄色靶心检测")
return detect_circle_v3(frame, laser_point)