v1.2.2
This commit is contained in:
230
vision.py
230
vision.py
@@ -8,10 +8,17 @@ import cv2
|
||||
import numpy as np
|
||||
import os
|
||||
import math
|
||||
import threading
|
||||
import queue
|
||||
from maix import image
|
||||
import config
|
||||
from logger_manager import logger_manager
|
||||
|
||||
# 存图队列 + worker
|
||||
_save_queue = queue.Queue(maxsize=16)
|
||||
_save_worker_started = False
|
||||
_save_worker_lock = threading.Lock()
|
||||
|
||||
def check_laser_point_sharpness(frame, laser_point=None, roi_size=30, threshold=100.0, ellipse_params=None):
|
||||
"""
|
||||
检测激光点本身的清晰度(不是整个靶子)
|
||||
@@ -529,71 +536,45 @@ def estimate_pixel(physical_distance_cm, target_distance_m):
|
||||
# 公式:像素偏移 = (物理距离_米) * 焦距_像素 / 目标距离_米
|
||||
return (physical_distance_cm / 100.0) * config.FOCAL_LENGTH_PIX / target_distance_m
|
||||
|
||||
def save_shot_image(result_img, center, radius, method, ellipse_params,
|
||||
laser_point, distance_m, shot_id=None, photo_dir=None):
|
||||
|
||||
def _save_shot_image_impl(img_cv, center, radius, method, ellipse_params,
|
||||
laser_point, distance_m, shot_id=None, 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(未检测到靶心)
|
||||
shot_id: 射箭ID,如果提供则用作文件名,否则使用旧的文件名格式
|
||||
photo_dir: 照片存储目录,如果为None则使用 config.PHOTO_DIR
|
||||
|
||||
Returns:
|
||||
str: 保存的文件路径,如果保存失败或未启用则返回 None
|
||||
内部实现:在 img_cv (numpy HWC RGB) 上绘制标注并保存。
|
||||
由 save_shot_image(同步)和存图 worker(异步)调用。
|
||||
"""
|
||||
# 检查是否启用图像保存
|
||||
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:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
x, y = laser_point
|
||||
|
||||
# 生成文件名:优先使用 shot_id,否则使用旧格式
|
||||
if shot_id:
|
||||
# 使用射箭ID作为文件名
|
||||
# 如果未检测到靶心,在文件名中标注
|
||||
if center is None or radius is None:
|
||||
filename = f"{photo_dir}/shot_{shot_id}_no_target.bmp"
|
||||
else:
|
||||
method_str = method or "unknown"
|
||||
filename = f"{photo_dir}/shot_{shot_id}_{method_str}.bmp"
|
||||
else:
|
||||
# 旧的文件名格式(向后兼容)
|
||||
try:
|
||||
all_images = [f for f in os.listdir(photo_dir) if f.endswith(('.bmp', '.jpg', '.jpeg'))]
|
||||
img_count = len(all_images)
|
||||
except:
|
||||
except Exception:
|
||||
img_count = 0
|
||||
|
||||
# 如果未检测到靶心,在文件名中标注
|
||||
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 shot_id:
|
||||
@@ -601,89 +582,82 @@ def save_shot_image(result_img, center, radius, method, ellipse_params,
|
||||
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}°")
|
||||
(ec, (ew, eh), ea) = ellipse_params
|
||||
logger.info(f"椭圆 -> 中心: ({ec[0]:.1f}, {ec[1]:.1f}), 长轴: {max(ew, eh):.1f}, 短轴: {min(ew, eh):.1f}, 角度: {ea:.1f}°")
|
||||
else:
|
||||
logger.info(f"结果 -> 未检测到靶心,保存原始图像(激光点: ({x}, {y}))")
|
||||
|
||||
# 转换图像为 OpenCV 格式以便绘制
|
||||
img_cv = image.image2cv(result_img, False, False)
|
||||
|
||||
# 绘制激光十字线(保存图像时统一绘制,避免影响检测)
|
||||
laser_color = (config.LASER_COLOR[0], config.LASER_COLOR[1], config.LASER_COLOR[2])
|
||||
cross_thickness = int(max(getattr(config, "LASER_THICKNESS", 1), 1))
|
||||
cross_length = int(max(getattr(config, "LASER_LENGTH", 10), 10))
|
||||
|
||||
# 水平线
|
||||
cv2.line(
|
||||
img_cv,
|
||||
(int(x - cross_length), int(y)),
|
||||
(int(x + cross_length), int(y)),
|
||||
laser_color,
|
||||
cross_thickness,
|
||||
)
|
||||
# 垂直线
|
||||
cv2.line(
|
||||
img_cv,
|
||||
(int(x), int(y - cross_length)),
|
||||
(int(x), int(y + cross_length)),
|
||||
laser_color,
|
||||
cross_thickness,
|
||||
)
|
||||
# 小点(与原 main.py 行为一致)
|
||||
cv2.line(img_cv, (int(x - cross_length), int(y)), (int(x + cross_length), int(y)), laser_color, cross_thickness)
|
||||
cv2.line(img_cv, (int(x), int(y - cross_length)), (int(x), int(y + cross_length)), laser_color, cross_thickness)
|
||||
cv2.circle(img_cv, (int(x), int(y)), 1, laser_color, cross_thickness)
|
||||
|
||||
# 额外的激光点标注(空心圆圈,便于肉眼查看)
|
||||
ring_thickness = 1
|
||||
cv2.circle(img_cv, (int(x), int(y)), 10, laser_color, ring_thickness)
|
||||
cv2.circle(img_cv, (int(x), int(y)), 5, laser_color, ring_thickness)
|
||||
cv2.circle(img_cv, (int(x), int(y)), 2, 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.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)
|
||||
pt1 = (int(cx_ell - dx_minor), int(cy_ell - dy_minor))
|
||||
pt2 = (int(cx_ell + dx_minor), int(cy_ell + dy_minor))
|
||||
cv2.line(img_cv, pt1, pt2, (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)
|
||||
|
||||
|
||||
out = image.cv2image(img_cv, False, False)
|
||||
out.save(filename)
|
||||
if logger:
|
||||
if center and radius:
|
||||
logger.debug(f"图像已保存(含靶心标注): {filename}")
|
||||
else:
|
||||
logger.debug(f"图像已保存(无靶心,含激光十字线): {filename}")
|
||||
|
||||
|
||||
# 清理旧图片:如果目录下图片超过100张,删除最老的
|
||||
try:
|
||||
image_files = []
|
||||
for f in os.listdir(photo_dir):
|
||||
if f.endswith(('.bmp', '.jpg', '.jpeg')):
|
||||
filepath = os.path.join(photo_dir, f)
|
||||
try:
|
||||
mtime = os.path.getmtime(filepath)
|
||||
image_files.append((mtime, filepath, f))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from config import MAX_IMAGES
|
||||
if len(image_files) > MAX_IMAGES:
|
||||
image_files.sort(key=lambda x: x[0])
|
||||
to_delete = len(image_files) - MAX_IMAGES
|
||||
deleted_count = 0
|
||||
for _, filepath, fname in image_files[:to_delete]:
|
||||
try:
|
||||
os.remove(filepath)
|
||||
deleted_count += 1
|
||||
if logger:
|
||||
logger.debug(f"[VISION] 删除旧图片: {fname}")
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.warning(f"[VISION] 删除旧图片失败 {fname}: {e}")
|
||||
if logger and deleted_count > 0:
|
||||
logger.info(f"[VISION] 已清理 {deleted_count} 张旧图片,当前剩余 {MAX_IMAGES} 张")
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.warning(f"[VISION] 清理旧图片时出错(可忽略): {e}")
|
||||
|
||||
return filename
|
||||
except Exception as e:
|
||||
logger = logger_manager.logger
|
||||
@@ -693,3 +667,85 @@ def save_shot_image(result_img, center, radius, method, ellipse_params,
|
||||
logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
|
||||
def _save_worker_loop():
|
||||
"""存图 worker:从队列取任务并调用 _save_shot_image_impl。"""
|
||||
while True:
|
||||
try:
|
||||
item = _save_queue.get()
|
||||
if item is None:
|
||||
break
|
||||
_save_shot_image_impl(*item)
|
||||
except Exception as e:
|
||||
logger = logger_manager.logger
|
||||
if logger:
|
||||
logger.error(f"[VISION] 存图 worker 异常: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
finally:
|
||||
try:
|
||||
_save_queue.task_done()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def start_save_shot_worker():
|
||||
"""启动存图 worker 线程(应在程序初始化时调用一次)。"""
|
||||
global _save_worker_started
|
||||
with _save_worker_lock:
|
||||
if _save_worker_started:
|
||||
return
|
||||
_save_worker_started = True
|
||||
t = threading.Thread(target=_save_worker_loop, daemon=True)
|
||||
t.start()
|
||||
logger = logger_manager.logger
|
||||
if logger:
|
||||
logger.info("[VISION] 存图 worker 线程已启动")
|
||||
|
||||
|
||||
def enqueue_save_shot(result_img, center, radius, method, ellipse_params,
|
||||
laser_point, distance_m, shot_id=None, photo_dir=None):
|
||||
"""
|
||||
将存图任务放入队列,由 worker 异步保存。主线程传入 result_img 的复制,不阻塞。
|
||||
"""
|
||||
if not config.SAVE_IMAGE_ENABLED:
|
||||
return
|
||||
if photo_dir is None:
|
||||
photo_dir = config.PHOTO_DIR
|
||||
try:
|
||||
img_cv = image.image2cv(result_img, False, False)
|
||||
img_copy = np.copy(img_cv)
|
||||
except Exception as e:
|
||||
logger = logger_manager.logger
|
||||
if logger:
|
||||
logger.error(f"[VISION] enqueue_save_shot 复制图像失败: {e}")
|
||||
return
|
||||
task = (img_copy, center, radius, method, ellipse_params, laser_point, distance_m, shot_id, photo_dir)
|
||||
try:
|
||||
_save_queue.put_nowait(task)
|
||||
except queue.Full:
|
||||
logger = logger_manager.logger
|
||||
if logger:
|
||||
logger.warning("[VISION] 存图队列已满,跳过本次保存")
|
||||
|
||||
|
||||
def save_shot_image(result_img, center, radius, method, ellipse_params,
|
||||
laser_point, distance_m, shot_id=None, photo_dir=None):
|
||||
"""
|
||||
保存射击图像(带标注)。同步调用,会阻塞。
|
||||
主流程建议使用 enqueue_save_shot;此处保留供校准、测试等场景使用。
|
||||
"""
|
||||
if not config.SAVE_IMAGE_ENABLED:
|
||||
return None
|
||||
if photo_dir is None:
|
||||
photo_dir = config.PHOTO_DIR
|
||||
try:
|
||||
img_cv = image.image2cv(result_img, False, False)
|
||||
return _save_shot_image_impl(img_cv, center, radius, method, ellipse_params,
|
||||
laser_point, distance_m, shot_id, photo_dir)
|
||||
except Exception as e:
|
||||
logger = logger_manager.logger
|
||||
if logger:
|
||||
logger.error(f"[VISION] save_shot_image 转换图像失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user