新分支 加入了标靶判断

This commit is contained in:
yrx
2026-05-22 09:45:49 +08:00
parent c754dff4ad
commit 46508e4b31
17 changed files with 356 additions and 34 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
network.py

7
.idea/archery.iml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 virtualenv at H:\iot\racingiot_v1\.venv" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (PyCharmMiscProject)" project-jdk-type="Python SDK" />
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

Binary file not shown.

12
adc.py
View File

@@ -4,12 +4,12 @@ from maix import time
a = adc.ADC(0, adc.RES_BIT_12)
while True:
# raw_data = a.read()
# print(f"ADC raw data:{raw_data}")
# if raw_data > 2450:
# print(f"ADC raw data:{raw_data}")
# elif raw_data < 2000:
# print(f"ADC raw data:{raw_data}")
raw_data = a.read()
print(f"ADC raw data:{raw_data}")
if raw_data > 2450:
print(f"ADC raw data:{raw_data}")
elif raw_data < 2000:
print(f"ADC raw data:{raw_data}")
time.sleep_ms(1)
vol = int(a.read_vol() * 10) / 10

View File

@@ -1,6 +1,6 @@
id: t11
name: t11
version: 2.14.1
version: 1.2.15.1
author: t11
icon: ''
desc: t11
@@ -23,6 +23,7 @@ files:
- ota_manager.py
- power.py
- server.pem
- set_autostart.py
- shoot_manager.py
- shot_id_generator.py
- target_roi_yolo.py

View File

@@ -169,6 +169,7 @@ TRIANGLE_SHAPE_COS_TOLERANCE = 0.25 # 直角余弦绝对值上限(原 0.20
# 建议设为实测最坏耗时的 1.2 倍;超时后圆心检测仍会并行跑完,跑完后若三角形已结束则优先用三角形。
TRIANGLE_TIMEOUT_MS = 1000
# True=打印各阶段耗时(ms),用于定位瓶颈;稳定后可 False 减少日志
ARCHERY_TIMING_ENABLE = True # 总开关False 关闭所有算法耗时统计shoot_manager + triangle_target + vision
TRIANGLE_TIMING_LOG = True
# True=Stage2 每个子框内传统三角失败时打一条统计Otsu/Adaptive 下轮廓数与各拒绝原因计数)
TRIANGLE_LOG_STAGE2_PATCH_REJECT = True
@@ -256,9 +257,13 @@ TRIANGLE_CROP_ROI_MIN_SIDE_PX = 64
# 射箭保存图 / 预览上绘制 YOLO 靶环 ROI 矩形 (x0,y0,x1,y1),核对是否裁准;不需要时改 False
TRIANGLE_YOLO_DRAW_ROI_ON_SHOT = True
# 物方采样调试:以靶心为中心,取半径 15cm 的圆周样本点,用于黑/白颜色对比
TRIANGLE_SAMPLE_ENABLE = True
TRIANGLE_SAMPLE_TIMING_ENABLE = True # 仅统计物方采样耗时(其他 timing 可关)
TRIANGLE_SAMPLE_RADIUS_CM = 15.0
TRIANGLE_SAMPLE_ANGLES_DEG = (0, 90, 180, 270)
TRIANGLE_SAMPLE_PATCH_HALF_PX = 2
# 物方采样判断黑白阈值R/G/B 均小于此值视为黑40cm 黑靶在靶面位置全黑20cm 白靶则 R/G/B 偏高
TRIANGLE_SAMPLE_BLACK_THRESH = 30.0
# 开机阶段预加载 YOLO detectordetect 使用 dual_buff=False避免返回上一帧结果。
TRIANGLE_YOLO_PRELOAD_ON_BOOT = True
@@ -310,6 +315,7 @@ LASER_LENGTH = 2
# ==================== 图像保存配置 ====================
SAVE_IMAGE_ENABLED = True # 是否保存图像True=保存False=不保存)
VISION_TIMING_ENABLE = True # 视觉圆检测耗时统计detect_circle_v3 内部各步骤耗时)
PHOTO_DIR = "/root/phot" # 照片存储目录
MAX_IMAGES = 1000
# Stage2 调试目录(默认 PHOTO_DIR/stage2_roi内 JPEG 最多保留张数None 表示与 MAX_IMAGES 相同

Binary file not shown.

View File

@@ -1,13 +0,0 @@
[basic]
type = cvimodel
model = model_270820.cvimodel
[extra]
model_type = yolov5
input_type = rgb
mean = 0, 0, 0
scale = 0.00392156862745098, 0.00392156862745098, 0.00392156862745098
anchors = 10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326
labels = triangle

View File

@@ -58,6 +58,7 @@ def analyze_shot(frame, laser_point=None):
# ── Step 1: 确定激光点 ────────────────────────────────────────────────────
laser_point_method = None
distance_m_first = None
best_radius1_temp = None
if config.HARDCODE_LASER_POINT:
laser_point = laser_manager.laser_point
@@ -102,9 +103,22 @@ def analyze_shot(frame, laser_point=None):
r_img, center, radius, method, best_radius1, ellipse_params = cdata
dx, dy = None, None
d_m = distance_m_first
tri_h = None
if center and radius:
dx, dy = laser_manager.compute_laser_position(center, (x, y), radius, method)
d_m = estimate_distance(best_radius1) if best_radius1 else distance_m_first
try:
import numpy as _np
px_per_cm = float(radius) / 10.0
if px_per_cm > 1e-6:
cxp, cyp = float(center[0]), float(center[1])
tri_h = _np.array([
[1.0 / px_per_cm, 0.0, -cxp / px_per_cm],
[0.0, 1.0 / px_per_cm, -cyp / px_per_cm],
[0.0, 0.0, 1.0],
], dtype=float)
except Exception:
tri_h = None
out = {
"success": True,
"result_img": r_img,
@@ -114,6 +128,7 @@ def analyze_shot(frame, laser_point=None):
"laser_point": laser_point, "laser_point_method": laser_point_method,
"offset_method": "yellow_ellipse" if ellipse_params else "yellow_circle",
"distance_method": "yellow_radius",
"tri_homography": tri_h,
}
if yolo_roi_xyxy is not None:
out["yolo_roi_xyxy"] = yolo_roi_xyxy
@@ -129,8 +144,10 @@ def analyze_shot(frame, laser_point=None):
roi_xyxy = None
yolo_ring_ms = 0.0
yolo_black_ms = 0.0
_timing_on = bool(getattr(config, "ARCHERY_TIMING_ENABLE", True))
_sample_on = bool(getattr(config, "TRIANGLE_SAMPLE_ENABLE", False))
if getattr(config, "TRIANGLE_YOLO_ROI_ENABLE", False):
_t_yolo_ring = time_std.perf_counter()
_t_yolo_ring = time_std.perf_counter() if _timing_on else None
try:
from target_roi_yolo import try_get_triangle_roi_from_yolo
roi_xyxy = try_get_triangle_roi_from_yolo(
@@ -140,7 +157,8 @@ def analyze_shot(frame, laser_point=None):
if logger:
logger.warning(f"[YOLO-ROI] {e}")
finally:
yolo_ring_ms = (time_std.perf_counter() - _t_yolo_ring) * 1000.0
if _timing_on and _t_yolo_ring is not None:
yolo_ring_ms = (time_std.perf_counter() - _t_yolo_ring) * 1000.0
_loc_mode = str(
getattr(config, "TRIANGLE_BLACK_TRIANGLE_LOCATE_MODE", "yolo")
@@ -155,7 +173,7 @@ def analyze_shot(frame, laser_point=None):
and roi_xyxy is not None
)
if _run_stage2_black_yolo:
_t_yolo_black = time_std.perf_counter()
_t_yolo_black = time_std.perf_counter() if _timing_on else None
try:
from target_roi_yolo import try_black_triangle_boxes_work
@@ -166,7 +184,8 @@ def analyze_shot(frame, laser_point=None):
if logger:
logger.warning(f"[YOLO-BLACK] {e}")
finally:
yolo_black_ms = (time_std.perf_counter() - _t_yolo_black) * 1000.0
if _timing_on and _t_yolo_black is not None:
yolo_black_ms = (time_std.perf_counter() - _t_yolo_black) * 1000.0
elif (
logger
and _loc_mode == "traditional"
@@ -184,7 +203,7 @@ def analyze_shot(frame, laser_point=None):
try:
logger.info(f"[TRI] begin {datetime.now()}")
logger.info(f"[TRI] K: {K}, dist: {dist_coef}, pos: {pos}, {datetime.now()}")
_t_wall_try = time_std.perf_counter()
_t_wall_try = time_std.perf_counter() if _timing_on else None
tri = try_triangle_scoring(
img_cv, (x, y), pos, K, dist_coef,
size_range=getattr(config, "TRIANGLE_SIZE_RANGE", (8, 500)),
@@ -193,8 +212,8 @@ def analyze_shot(frame, laser_point=None):
yolo_ring_ms=yolo_ring_ms,
yolo_black_ms=yolo_black_ms,
)
_wall_try_ms = (time_std.perf_counter() - _t_wall_try) * 1000.0
if logger and bool(getattr(config, "TRIANGLE_LOG_E2E_TIMING", True)):
_wall_try_ms = (time_std.perf_counter() - _t_wall_try) * 1000.0 if _timing_on else 0.0
if logger and bool(getattr(config, "TRIANGLE_LOG_E2E_TIMING", True)) and _timing_on:
_e2e = float(yolo_ring_ms) + float(yolo_black_ms) + float(_wall_try_ms)
logger.info(
f"[TRI] timing_e2e_triangle_ms={_e2e:.1f} "
@@ -280,6 +299,16 @@ def analyze_shot(frame, laser_point=None):
"tri_markers_completed": tri.get("markers_completed", []),
"tri_homography": tri.get("homography"),
}
try:
import numpy as _np
_H = tri.get("homography")
if _H is not None and _np.all(_np.isfinite(_H)):
_H_inv = _np.linalg.inv(_H)
_pt = _np.array([[[0.0, 0.0]]], dtype=_np.float32)
_center_pt = cv2.perspectiveTransform(_pt, _H_inv)[0][0]
out["tri_center_px"] = [float(_center_pt[0]), float(_center_pt[1])]
except Exception:
pass
if yolo_roi_xyxy is not None:
out["yolo_roi_xyxy"] = yolo_roi_xyxy
return out
@@ -318,6 +347,7 @@ def process_shot(adc_val):
:return: None
"""
logger = logger_manager.logger
_timing_on = bool(getattr(config, "ARCHERY_TIMING_ENABLE", True))
try:
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
@@ -356,6 +386,86 @@ def process_shot(adc_val):
)
x, y = laser_point
# 物方采样调试config.TRIANGLE_SAMPLE_ENABLE靶心为原点取两个对称点判断黑白来区分 40/20 标靶
# 逻辑:若两个采样点 RGB 均 < 阈值 → 全黑 → 40cm 标靶;否则 → 20cm 标靶
sample_target_type = None
_t_sample = time_std.perf_counter() if _timing_on else None
_t_sample_ms = 0.0
sample_points = []
sample_patch_half = 2
if bool(getattr(config, "TRIANGLE_SAMPLE_ENABLE", False)):
sample_obj_radius_cm = float(getattr(config, "TRIANGLE_SAMPLE_RADIUS_CM", 15.0))
sample_obj_angles_deg = (0, 180) # 只取两个对称点:+X 和 -X
sample_patch_half = int(getattr(config, "TRIANGLE_SAMPLE_PATCH_HALF_PX", 2))
sample_black_thresh = float(getattr(config, "TRIANGLE_SAMPLE_BLACK_THRESH", 30.0))
try:
import math as _math
import numpy as _np
import cv2 as _cv2
if tri_homography is not None:
_H_inv = _np.linalg.inv(tri_homography)
for _ang in sample_obj_angles_deg:
_rad = _math.radians(float(_ang))
_pt_obj = _np.array([
[[sample_obj_radius_cm * _math.cos(_rad), sample_obj_radius_cm * _math.sin(_rad)]]
], dtype=_np.float32)
_pt_img = _cv2.perspectiveTransform(_pt_obj, _H_inv)[0][0]
_px, _py = float(_pt_img[0]), float(_pt_img[1])
sample_points.append({
"angle_deg": float(_ang),
"obj_cm": (float(sample_obj_radius_cm * _math.cos(_rad)), float(sample_obj_radius_cm * _math.sin(_rad))),
"img_px": (int(round(_px)), int(round(_py))),
})
elif center and radius:
_px_per_cm = float(radius) / 10.0
for _ang in sample_obj_angles_deg:
_rad = _math.radians(float(_ang))
_px = float(center[0]) + sample_obj_radius_cm * _math.cos(_rad) * _px_per_cm
_py = float(center[1]) + sample_obj_radius_cm * _math.sin(_rad) * _px_per_cm
sample_points.append({
"angle_deg": float(_ang),
"obj_cm": (float(sample_obj_radius_cm * _math.cos(_rad)), float(sample_obj_radius_cm * _math.sin(_rad))),
"img_px": (int(round(_px)), int(round(_py))),
})
# 取样后立即读像素并判断黑白:三角成功用 H_inv三角失败但圆心成功用 center/radius 近似物方半径
_all_black = False
_sample_infos = []
if sample_points:
_img_cv_for_sample = image.image2cv(result_img, False, False)
_all_black = True
for _sp in sample_points:
_sx, _sy = _sp["img_px"]
_hh = max(1, sample_patch_half)
_patch = []
for _yy in range(_sy - _hh, _sy + _hh + 1):
if _yy < 0 or _yy >= _img_cv_for_sample.shape[0]:
continue
for _xx in range(_sx - _hh, _sx + _hh + 1):
if _xx < 0 or _xx >= _img_cv_for_sample.shape[1]:
continue
_patch.append(_img_cv_for_sample[_yy, _xx].astype(float))
if _patch:
_mean_rgb = _np.mean(_patch, axis=0)
_is_black = bool(_mean_rgb[0] < sample_black_thresh
and _mean_rgb[1] < sample_black_thresh
and _mean_rgb[2] < sample_black_thresh)
if not _is_black:
_all_black = False
_sample_infos.append(
f"{int(_sp['angle_deg'])}°@{_sx},{_sy} rgb=({int(_mean_rgb[0])},{int(_mean_rgb[1])},{int(_mean_rgb[2])})"
)
sample_target_type = "40cm_black" if _all_black else "20cm"
if _sample_infos:
logger.info("[采样] " + " | ".join(_sample_infos) + f"{sample_target_type}")
except Exception as _e_sample:
sample_points = []
if logger:
logger.warning(f"[采样] 标靶类型判断失败: {_e_sample}")
if _timing_on and _t_sample is not None:
_t_sample_ms = (time_std.perf_counter() - _t_sample) * 1000.0
# 三角形路径成功时 center/radius 为空是正常的;此时用 triangle 方法名用于保存文件名与上报字段 m
if (not method) and tri_markers:
method = "triangle_homography"
@@ -397,6 +507,7 @@ def process_shot(adc_val):
"target_y": float(y),
"offset_method": offset_method,
"distance_method": distance_method,
"target_type": 40 if sample_target_type == "40cm_black" else (20 if sample_target_type == "20cm" else None),
}
if ellipse_params:
@@ -471,6 +582,11 @@ def process_shot(adc_val):
except Exception:
pass
# 物方采样标靶类型判断耗时(合并在上面采样块内,单独统计)
if _timing_on and bool(getattr(config, "TRIANGLE_SAMPLE_ENABLE", False)) and sample_target_type is not None:
logger.info(f"[采样] 标靶类型: {sample_target_type} 耗时: {_t_sample_ms:.2f}ms")
# 叠加信息:落点-圆心距离 / 相机-靶距离等
try:
import math as _math

30
test/test_yolov8 Normal file
View File

@@ -0,0 +1,30 @@
from maix import image, nn, display
# 1. 加载模型
detector = nn.YOLOv8(model="/root/models/yolov8n.mud", dual_buff=True)
# 2. 加载指定图片(根据模型输入尺寸自动缩放宽高)
img = image.load("/root/test.jpg")
if img is None:
raise FileNotFoundError("图片加载失败,请检查路径")
# 3. 调整图片尺寸到模型输入要求可选detect内部会处理但提前缩放可提高速度
# img = img.resize(detector.input_width(), detector.input_height())
# 4. 检测
objs = detector.detect(img, conf_th=0.5, iou_th=0.45)
# 5. 在图片上绘制结果
for obj in objs:
img.draw_rect(obj.x, obj.y, obj.w, obj.h, color=image.COLOR_RED)
msg = f'{detector.labels[obj.class_id]}: {obj.score:.2f}'
img.draw_string(obj.x, obj.y, msg, color=image.COLOR_RED)
# 6. 显示结果(如果设备有屏幕)
disp = display.Display()
disp.show(img)
# 7. 保存结果(可选)
img.save("/root/result.jpg")
print("识别完成,结果已显示并保存为 result.jpg")

View File

@@ -224,7 +224,7 @@ def detect_triangle_markers(
blackhat_kernel_frac = 0.018
try:
import config as _tcfg
_timing_log = bool(getattr(_tcfg, "TRIANGLE_TIMING_LOG", True))
_timing_log = bool(getattr(_tcfg, "ARCHERY_TIMING_ENABLE", True)) and bool(getattr(_tcfg, "TRIANGLE_TIMING_LOG", True))
except Exception:
_timing_log = True
@@ -641,7 +641,40 @@ def detect_triangle_markers(
med_l = float(np.median(legs))
leg_dev = max(abs(l - med_l) / (med_l + 1e-6) for l in legs)
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0
# 方向一致性:四个标靶角的直角顶点朝向应符合大致的四象限布局。
# 对于相邻标靶很近的情况,这个方向信息能显著减少“把同一方向的两个三角混成一组”的误判。
orient_pen = 0.0
orient_vote = []
for c in cands_4:
cen = np.array(c["center_px"], dtype=np.float32)
rpt = np.array(c["right_pt"], dtype=np.float32)
vx = float(cen[0] - rpt[0])
vy = float(cen[1] - rpt[1])
# 方向太接近轴线时,说明顶点朝向不稳定,增加惩罚
if abs(vx) < 1e-6 or abs(vy) < 1e-6:
orient_pen += 1.0
orient_vote.append(None)
continue
if abs(vx) < abs(vy) * 0.15 or abs(vy) < abs(vx) * 0.15:
orient_pen += 0.5
# 以中心相对右角顶点的方向做粗分类TL=向右下TR=向左下BL=向右上BR=向左上
if vx > 0 and vy > 0:
orient_vote.append(0)
elif vx < 0 and vy > 0:
orient_vote.append(1)
elif vx > 0 and vy < 0:
orient_vote.append(2)
else:
orient_vote.append(3)
# 如果 4 个候选的方向落点本身就重复很多,说明可能混入了别的靶标角,直接加罚。
valid_votes = [v for v in orient_vote if v is not None]
if valid_votes:
from collections import Counter
vc = Counter(valid_votes)
orient_pen += max(0, max(vc.values()) - 1) * 0.8
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0 + orient_pen
return score, (tl, bl, br, tr)
assigned = None
@@ -947,7 +980,33 @@ def _assign_marker_ids_from_filtered(filtered, verbose=True):
v_ratio = max(s_left, s_right) / (min(s_left, s_right) + 1e-6)
med_l = float(np.median(legs))
leg_dev = max(abs(l - med_l) / (med_l + 1e-6) for l in legs)
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0
orient_pen = 0.0
orient_vote = []
for c in cands_4:
cen = np.array(c["center_px"], dtype=np.float32)
rpt = np.array(c["right_pt"], dtype=np.float32)
vx = float(cen[0] - rpt[0])
vy = float(cen[1] - rpt[1])
if abs(vx) < 1e-6 or abs(vy) < 1e-6:
orient_pen += 1.0
orient_vote.append(None)
continue
if abs(vx) < abs(vy) * 0.15 or abs(vy) < abs(vx) * 0.15:
orient_pen += 0.5
if vx > 0 and vy > 0:
orient_vote.append(0)
elif vx < 0 and vy > 0:
orient_vote.append(1)
elif vx > 0 and vy < 0:
orient_vote.append(2)
else:
orient_vote.append(3)
valid_votes = [v for v in orient_vote if v is not None]
if valid_votes:
from collections import Counter
vc = Counter(valid_votes)
orient_pen += max(0, max(vc.values()) - 1) * 0.8
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0 + orient_pen
return score, (tl, bl, br, tr)
assigned = None
@@ -1113,7 +1172,7 @@ def try_triangle_scoring(
try:
import config as _cfg_tl
_try_timing_log = bool(getattr(_cfg_tl, "TRIANGLE_TIMING_LOG", True))
_try_timing_log = bool(getattr(_cfg_tl, "ARCHERY_TIMING_ENABLE", True)) and bool(getattr(_cfg_tl, "TRIANGLE_TIMING_LOG", True))
_crop_min_side = int(getattr(_cfg_tl, "TRIANGLE_CROP_ROI_MIN_SIDE_PX", 64))
except Exception:
_try_timing_log = True

View File

@@ -4,7 +4,7 @@
应用版本号
每次 OTA 更新时,只需要更新这个文件中的版本号
"""
VERSION = '2.14.1'
VERSION = '1.2.15'
# 1.2.0 开始使用C++编译成.so替换部分代码
@@ -22,7 +22,7 @@ VERSION = '2.14.1'
# 1.2.110 关掉了黑色三角形算法,只用于测试
# 1.2.13 修改wifi连接
# 1.2.14 修改了icc登录部分
# 1.2.15 增加了标靶判断 20 40

View File

@@ -10,6 +10,7 @@ import os
import math
import threading
import queue
import time
from maix import image
import config
from logger_manager import logger_manager
@@ -531,6 +532,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
if img_cv is None:
img_cv = image.image2cv(frame, False, False)
logger = logger_manager.logger
_timing_on = bool(getattr(config, "VISION_TIMING_ENABLE", True))
_t0 = time.perf_counter() if _timing_on else None
_t1 = _t2 = _t3 = _t4 = _t5 = None
from datetime import datetime
logger.debug(f"[detect_circle_v3] begin {datetime.now()}")
# -- 1. 缩图加速(与三角形路径保持一致)
@@ -554,6 +558,8 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
ellipse_params = None
logger.debug(f"[detect_circle_v3] step 1 fin {datetime.now()}")
if _timing_on:
_t1 = time.perf_counter()
# -- 2. HSV + 黄色掩码
hsv = cv2.cvtColor(img_det, cv2.COLOR_RGB2HSV)
@@ -567,6 +573,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_CLOSE, kernel)
logger.debug(f"[detect_circle_v3] step 2 fin {datetime.now()}")
if _timing_on:
_t2 = time.perf_counter()
_t3 = time.perf_counter()
# -- 3. 红色掩码:在循环外只算一次
mask_red = cv2.bitwise_or(
@@ -593,6 +602,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
red_candidates.append({"center": (int(xr), int(yr)), "radius": int(rr)})
logger.debug(f"[detect_circle_v3] step 3 fin {datetime.now()}")
if _timing_on:
_t3 = time.perf_counter()
_t4 = time.perf_counter()
# -- 4. 黄色轮廓循环(复用上面的红色候选列表)
contours_yellow, _ = cv2.findContours(mask_yellow, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
@@ -642,6 +654,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
logger.debug("Debug -> 未找到匹配的红色圆圈,可能是误识别")
logger.debug(f"[detect_circle_v3] step 4 fin {datetime.now()}")
if _timing_on:
_t4 = time.perf_counter()
_t5 = time.perf_counter()
# -- 5. 选最佳目标,坐标还原到原始分辨率
if valid_targets:
@@ -669,7 +684,20 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
ellipse_params = be
best_radius1 = best_radius * 5
result_img = image.cv2image(img_cv, False, False)
logger.debug(f"[detect_circle_v3] step 5 fin {datetime.now()}")
if _timing_on:
_t5 = time.perf_counter()
_t_all = (_t5 - _t0) * 1000
_ms1 = (_t1 - _t0) * 1000
_ms2 = (_t2 - _t1) * 1000
_ms3 = (_t3 - _t2) * 1000
_ms4 = (_t4 - _t3) * 1000
_ms5 = (_t5 - _t4) * 1000
logger.info(
f"[VISION timing] total={_t_all:.1f}ms "
f"resize={_ms1:.1f} hsv_yellow={_ms2:.1f} "
f"red_mask={_ms3:.1f} yellow_loop={_ms4:.1f} "
f"select_cv2img={_ms5:.1f}"
)
return result_img, best_center, best_radius, method, best_radius1, ellipse_params
def estimate_distance(pixel_radius):
@@ -1010,3 +1038,63 @@ def detect_target(frame, laser_point=None):
logger.debug("[VISION] 使用传统黄色靶心检测")
return detect_circle_v3(frame, laser_point)
def sample_target_rgb_at_physical_radius(frame, target_center, target_radius_px, radius_cm=None, angles_deg=None, patch_half_px=None, black_thresh=None, timing=False):
"""
在物方半径位置采样 RGB判断黑/白靶。
返回: dict {ok, is_black, mean_rgb, samples, black_ratio, elapsed_ms}
"""
logger = logger_manager.logger
if target_center is None or target_radius_px is None:
return {"ok": False, "reason": "no_target", "is_black": None, "elapsed_ms": 0.0}
radius_cm = float(radius_cm if radius_cm is not None else getattr(config, "TRIANGLE_SAMPLE_RADIUS_CM", 15.0))
angles_deg = tuple(angles_deg if angles_deg is not None else getattr(config, "TRIANGLE_SAMPLE_ANGLES_DEG", (0, 90, 180, 270)))
patch_half_px = int(patch_half_px if patch_half_px is not None else getattr(config, "TRIANGLE_SAMPLE_PATCH_HALF_PX", 2))
black_thresh = float(black_thresh if black_thresh is not None else getattr(config, "TRIANGLE_SAMPLE_BLACK_THRESH", 30.0))
timing_on = bool(timing) and bool(getattr(config, "TRIANGLE_SAMPLE_TIMING_ENABLE", True))
t0 = time.perf_counter() if timing_on else None
try:
img_cv = image.image2cv(frame, False, False)
h, w = img_cv.shape[:2]
cx, cy = float(target_center[0]), float(target_center[1])
scale = float(target_radius_px) / max(radius_cm, 1e-6)
samples = []
black_count = 0
for ang in angles_deg:
rad = math.radians(float(ang))
sx = int(round(cx + math.cos(rad) * radius_cm * scale))
sy = int(round(cy + math.sin(rad) * radius_cm * scale))
x0 = max(0, sx - patch_half_px)
y0 = max(0, sy - patch_half_px)
x1 = min(w, sx + patch_half_px + 1)
y1 = min(h, sy + patch_half_px + 1)
if x1 <= x0 or y1 <= y0:
continue
patch = img_cv[y0:y1, x0:x1]
mean_rgb = patch.reshape(-1, 3).mean(axis=0)
is_black = bool(np.all(mean_rgb < black_thresh))
black_count += 1 if is_black else 0
samples.append({"angle": float(ang), "xy": (sx, sy), "mean_rgb": tuple(float(v) for v in mean_rgb), "is_black": is_black})
black_ratio = float(black_count) / float(len(samples) or 1)
out = {
"ok": len(samples) > 0,
"is_black": black_ratio >= 0.5,
"mean_rgb": tuple(float(v) for v in (np.mean([s["mean_rgb"] for s in samples], axis=0) if samples else (0, 0, 0))),
"samples": samples,
"black_ratio": black_ratio,
"elapsed_ms": (time.perf_counter() - t0) * 1000.0 if timing_on else 0.0,
}
if logger:
logger.info(
f"[TRI-SAMPLE] radius_cm={radius_cm:.1f} black_thresh={black_thresh:.1f} "
f"black_ratio={black_ratio:.2f} is_black={out['is_black']} "
f"elapsed_ms={out['elapsed_ms']:.1f} samples={len(samples)}"
)
return out
except Exception as e:
if logger:
logger.error(f"[TRI-SAMPLE] 采样失败: {e}")
return {"ok": False, "reason": str(e), "is_black": None, "elapsed_ms": 0.0}