refine the code to different part
This commit is contained in:
360
vision.py
Normal file
360
vision.py
Normal file
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
视觉检测模块
|
||||
提供靶心检测、距离估算、图像保存等功能
|
||||
"""
|
||||
import cv2
|
||||
import numpy as np
|
||||
import os
|
||||
import math
|
||||
from maix import image
|
||||
import globals
|
||||
import config
|
||||
from logger_manager import logger_manager
|
||||
|
||||
|
||||
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 = logger_manager.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 = logger_manager.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 = logger_manager.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 estimate_distance(pixel_radius):
|
||||
"""根据像素半径估算实际距离(单位:米)"""
|
||||
if not pixel_radius:
|
||||
return 0.0
|
||||
return (config.REAL_RADIUS_CM * config.FOCAL_LENGTH_PIX) / pixel_radius / 100.0
|
||||
|
||||
|
||||
def compute_laser_position(circle_center, laser_point, radius, method):
|
||||
"""计算激光相对于靶心的偏移量(单位:厘米)"""
|
||||
if not all([circle_center, radius, method]):
|
||||
return None, None
|
||||
cx, cy = circle_center
|
||||
lx, ly = laser_point
|
||||
# 根据检测方法动态调整靶心物理半径(简化模型)
|
||||
circle_r = (radius / 4.0) * 20.0 if method == "模糊" else (68 / 16.0) * 20.0
|
||||
dx = lx - cx
|
||||
dy = ly - cy
|
||||
return dx / (circle_r / 100.0), -dy / (circle_r / 100.0)
|
||||
|
||||
|
||||
def save_shot_image(result_img, center, radius, method, ellipse_params,
|
||||
laser_point, distance_m, photo_dir=None):
|
||||
"""
|
||||
保存射击图像(带标注)
|
||||
即使没有检测到靶心也会保存图像,文件名会标注 "no_target"
|
||||
确保保存的图像总是包含激光十字线
|
||||
|
||||
Args:
|
||||
result_img: 处理后的图像对象(可能已经包含激光十字线或检测标注)
|
||||
center: 靶心中心坐标 (x, y),可能为 None(未检测到靶心)
|
||||
radius: 靶心半径,可能为 None(未检测到靶心)
|
||||
method: 检测方法,可能为 None(未检测到靶心)
|
||||
ellipse_params: 椭圆参数 ((center, (width, height), angle)) 或 None
|
||||
laser_point: 激光点坐标 (x, y)
|
||||
distance_m: 距离(米),可能为 None(未检测到靶心)
|
||||
photo_dir: 照片存储目录,如果为None则使用 config.PHOTO_DIR
|
||||
|
||||
Returns:
|
||||
str: 保存的文件路径,如果保存失败或未启用则返回 None
|
||||
"""
|
||||
# 检查是否启用图像保存
|
||||
if not config.SAVE_IMAGE_ENABLED:
|
||||
return None
|
||||
|
||||
if photo_dir is None:
|
||||
photo_dir = config.PHOTO_DIR
|
||||
|
||||
try:
|
||||
# 确保照片目录存在
|
||||
try:
|
||||
if photo_dir not in os.listdir("/root"):
|
||||
os.mkdir(photo_dir)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 生成文件名
|
||||
# 统计所有图片文件(包括 .bmp 和 .jpg)
|
||||
try:
|
||||
all_images = [f for f in os.listdir(photo_dir) if f.endswith(('.bmp', '.jpg', '.jpeg'))]
|
||||
img_count = len(all_images)
|
||||
except:
|
||||
img_count = 0
|
||||
|
||||
x, y = laser_point
|
||||
|
||||
# 如果未检测到靶心,在文件名中标注
|
||||
if center is None or radius is None:
|
||||
method_str = "no_target"
|
||||
distance_str = "000"
|
||||
else:
|
||||
method_str = method or "unknown"
|
||||
distance_str = str(round((distance_m or 0.0) * 100))
|
||||
|
||||
filename = f"{photo_dir}/{method_str}_{int(x)}_{int(y)}_{distance_str}_{img_count:04d}.bmp"
|
||||
|
||||
logger = logger_manager.logger
|
||||
if logger:
|
||||
if center and radius:
|
||||
logger.info(f"结果 -> 圆心: {center}, 半径: {radius}, 方法: {method}")
|
||||
if ellipse_params:
|
||||
(ell_center, (width, height), angle) = ellipse_params
|
||||
logger.info(f"椭圆 -> 中心: ({ell_center[0]:.1f}, {ell_center[1]:.1f}), 长轴: {max(width, height):.1f}, 短轴: {min(width, height):.1f}, 角度: {angle:.1f}°")
|
||||
else:
|
||||
logger.info(f"结果 -> 未检测到靶心,保存原始图像(激光点: ({x}, {y}))")
|
||||
|
||||
# 转换图像为 OpenCV 格式以便绘制
|
||||
img_cv = image.image2cv(result_img, False, False)
|
||||
|
||||
# 确保激光十字线被绘制(使用OpenCV在图像上绘制,确保可见性)
|
||||
laser_color = (config.LASER_COLOR[0], config.LASER_COLOR[1], config.LASER_COLOR[2])
|
||||
thickness = max(config.LASER_THICKNESS, 2) # 至少2像素宽,确保可见
|
||||
length = max(config.LASER_LENGTH, 10) # 至少10像素长
|
||||
|
||||
# 绘制激光十字线(水平线)
|
||||
cv2.line(img_cv,
|
||||
(int(x - length), int(y)),
|
||||
(int(x + length), int(y)),
|
||||
laser_color, thickness)
|
||||
# 绘制激光十字线(垂直线)
|
||||
cv2.line(img_cv,
|
||||
(int(x), int(y - length)),
|
||||
(int(x), int(y + length)),
|
||||
laser_color, thickness)
|
||||
# 绘制激光点
|
||||
cv2.circle(img_cv, (int(x), int(y)), max(thickness, 3), laser_color, -1)
|
||||
|
||||
# 如果检测到靶心,绘制靶心标注
|
||||
if center and radius:
|
||||
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])
|
||||
|
||||
# 绘制椭圆
|
||||
cv2.ellipse(img_cv,
|
||||
(cx_ell, cy_ell),
|
||||
(int(width/2), int(height/2)),
|
||||
angle,
|
||||
0, 360,
|
||||
(0, 255, 0),
|
||||
2)
|
||||
cv2.circle(img_cv, (cx_ell, cy_ell), 3, (255, 0, 0), -1)
|
||||
|
||||
# 绘制短轴
|
||||
minor_length = min(width, height) / 2
|
||||
minor_angle = angle + 90 if width >= height else angle
|
||||
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)
|
||||
else:
|
||||
# 绘制圆形靶心
|
||||
cv2.circle(img_cv, (cx, cy), radius, (0, 0, 255), 2)
|
||||
cv2.circle(img_cv, (cx, cy), 2, (0, 0, 255), -1)
|
||||
|
||||
# 如果检测到靶心,绘制从激光点到靶心的连线(可选,用于可视化偏移)
|
||||
cv2.line(img_cv, (int(x), int(y)), (cx, cy), (255, 255, 0), 1)
|
||||
|
||||
# 转换回 MaixPy 图像格式并保存
|
||||
result_img = image.cv2image(img_cv, False, False)
|
||||
result_img.save(filename)
|
||||
|
||||
if logger:
|
||||
if center and radius:
|
||||
logger.debug(f"图像已保存(含靶心标注): {filename}")
|
||||
else:
|
||||
logger.debug(f"图像已保存(无靶心,含激光十字线): {filename}")
|
||||
|
||||
return filename
|
||||
except Exception as e:
|
||||
logger = logger_manager.logger
|
||||
if logger:
|
||||
logger.error(f"保存图像失败: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user