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} +