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 id: t11
name: t11 name: t11
version: 1.2.9 version: 1.2.10
author: t11 author: t11
icon: '' icon: ''
desc: t11 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" # 照片存储目录 PHOTO_DIR = "/root/phot" # 照片存储目录
MAX_IMAGES = 1000 MAX_IMAGES = 1000
SHOW_CAMERA_PHOTO_WHILE_SHOOTING = True # 是否在拍摄时显示摄像头图像True=显示False=不显示建议在连着USB测试过程中打开 SHOW_CAMERA_PHOTO_WHILE_SHOOTING = False # 是否在拍摄时显示摄像头图像True=显示False=不显示建议在连着USB测试过程中打开
# ==================== OTA配置 ==================== # ==================== OTA配置 ====================
MAX_BACKUPS = 5 MAX_BACKUPS = 5
@@ -149,6 +149,42 @@ PIN_MAPPINGS_WITH_WIFI = {
# 根据WiFi模块开关选择引脚映射 # 根据WiFi模块开关选择引脚映射
PIN_MAPPINGS = PIN_MAPPINGS_WITH_WIFI if HAS_WIFI_MODULE else PIN_MAPPINGS_NO_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表示不自动关机 AUTO_POWER_OFF_IN_SECONDS = 10 * 60 # 自动关机时间0表示不自动关机

View File

@@ -8,7 +8,7 @@ import _thread
import json import json
import os import os
import binascii import binascii
from maix import time, camera from maix import time
import threading import threading
import config import config
from logger_manager import logger_manager from logger_manager import logger_manager
@@ -861,7 +861,8 @@ class LaserManager:
center_temp = None center_temp = None
radius_temp = None radius_temp = None
if config.LASER_REQUIRE_IN_ELLIPSE: 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: if center_temp is None or radius_temp is None:
@@ -1114,7 +1115,7 @@ class LaserManager:
return self._laser_point is not None 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: Args:
@@ -1122,6 +1123,7 @@ class LaserManager:
laser_point: 激光点坐标 (x, y) laser_point: 激光点坐标 (x, y)
radius: 靶心半径(像素) radius: 靶心半径(像素)
method: 检测方法("模糊" 或其他) method: 检测方法("模糊" 或其他)
ellipse_params: 椭圆参数,用于透视校正(可选)
Returns: Returns:
(dx, dy): 激光相对于靶心的偏移量(厘米),如果输入无效则返回 (None, None) (dx, dy): 激光相对于靶心的偏移量(厘米),如果输入无效则返回 (None, None)
@@ -1131,6 +1133,14 @@ class LaserManager:
cx, cy = circle_center cx, cy = circle_center
lx, ly = laser_point 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 = 22.16 * 5
r = radius * 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}") self.logger.debug(f"compute_laser_position: circle_center: {circle_center} laser_point: {laser_point} radius: {radius} method: {method} r: {r}")
@@ -1138,6 +1148,87 @@ class LaserManager:
target_y = (ly-cy)/r*100 target_y = (ly-cy)/r*100
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)}") 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) 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 # circle_r = (radius / 4.0) * 20.0 if method == "模糊" else (68 / 16.0) * 20.0

View File

@@ -1073,7 +1073,6 @@ class NetworkManager:
def tcp_main(self): def tcp_main(self):
"""TCP 主通信循环:登录、心跳、处理指令、发送数据""" """TCP 主通信循环:登录、心跳、处理指令、发送数据"""
import _thread import _thread
from maix import camera
self.logger.info("[NET] TCP主线程启动") 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 import config
from logger_manager import logger_manager from logger_manager import logger_manager
# 导入ArUco检测器如果启用
if config.USE_ARUCO:
from aruco_detector import detect_target_with_aruco, aruco_detector
# 存图队列 + worker # 存图队列 + worker
_save_queue = queue.Queue(maxsize=16) _save_queue = queue.Queue(maxsize=16)
_save_worker_started = False _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}") logger.error(f"[VISION] save_shot_image 转换图像失败: {e}")
return None 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)