diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..35410ca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..7a739ba
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+network.py
\ No newline at end of file
diff --git a/.idea/archery.iml b/.idea/archery.iml
new file mode 100644
index 0000000..ec63674
--- /dev/null
+++ b/.idea/archery.iml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..f9cb2e1
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__pycache__/version.cpython-312.pyc b/__pycache__/version.cpython-312.pyc
new file mode 100644
index 0000000..2968e09
Binary files /dev/null and b/__pycache__/version.cpython-312.pyc differ
diff --git a/adc.py b/adc.py
index 0584e63..47a3c44 100644
--- a/adc.py
+++ b/adc.py
@@ -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
diff --git a/app.yaml b/app.yaml
index 21b2c21..d2b771e 100644
--- a/app.yaml
+++ b/app.yaml
@@ -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
diff --git a/config.py b/config.py
index f65e0f4..29c1654 100644
--- a/config.py
+++ b/config.py
@@ -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 detector;detect 使用 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 相同
diff --git a/model_270820.cvimodel b/model_270820.cvimodel
deleted file mode 100644
index 0ab2069..0000000
Binary files a/model_270820.cvimodel and /dev/null differ
diff --git a/model_270820.mud b/model_270820.mud
deleted file mode 100644
index cf95de4..0000000
--- a/model_270820.mud
+++ /dev/null
@@ -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
-
diff --git a/shoot_manager.py b/shoot_manager.py
index 81e1739..f47c4f9 100644
--- a/shoot_manager.py
+++ b/shoot_manager.py
@@ -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
diff --git a/test/test_yolov8 b/test/test_yolov8
new file mode 100644
index 0000000..7b09d10
--- /dev/null
+++ b/test/test_yolov8
@@ -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")
\ No newline at end of file
diff --git a/triangle_target.py b/triangle_target.py
index 07506c6..2ac60fb 100644
--- a/triangle_target.py
+++ b/triangle_target.py
@@ -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
diff --git a/version.py b/version.py
index 2ecda51..2701dde 100644
--- a/version.py
+++ b/version.py
@@ -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
diff --git a/vision.py b/vision.py
index d4fba86..afd4fb3 100644
--- a/vision.py
+++ b/vision.py
@@ -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}
+