diff --git a/app.yaml b/app.yaml index 16bdd0a..d95064f 100644 --- a/app.yaml +++ b/app.yaml @@ -1,6 +1,6 @@ id: t11 name: t11 -version: 1.2.10 +version: 1.2.11 author: t11 icon: '' desc: t11 diff --git a/config.py b/config.py index 62d2c41..53e96fa 100644 --- a/config.py +++ b/config.py @@ -130,10 +130,23 @@ CAMERA_CALIB_XML = APP_DIR + "/cameraParameters.xml" TRIANGLE_POSITIONS_JSON = APP_DIR + "/triangle_positions.json" # 检测到的三角形边长在图像中的像素范围,分辨率或靶纸占比变化时可微调 TRIANGLE_SIZE_RANGE = (8, 500) -# 三角形检测兜底增强:CLAHE(更鲁棒但更慢)。默认关闭以优先速度。 +# 三角形检测兜底增强:CLAHE(更鲁棒但更慢)。颜色阈值修复后通常不需要,保持关闭以优先速度。 TRIANGLE_ENABLE_CLAHE_FALLBACK = False +# 三角形检测调试:保存 Otsu 二值化图像(临时调试用,定位后关闭) +TRIANGLE_SAVE_DEBUG_IMAGE = False +# 三角形颜色过滤阈值(三角形内部灰度判定) +# 如果三角形标记印刷较浅/环境较亮,可放宽: +# max_interior_gray: 三角形内部平均灰度上限(越大越宽松,90→130 适应浅色印刷) +# dark_pixel_gray: "暗像素"灰度判定阈值(越大越宽松,80→130) +# min_dark_ratio: 暗像素占比下限(越小越宽松,0.70→0.30) +TRIANGLE_MAX_INTERIOR_GRAY = 130 +TRIANGLE_DARK_PIXEL_GRAY = 130 +TRIANGLE_MIN_DARK_RATIO = 0.30 +# 三角形相对对比度阈值:内部比周围暗多少灰度值才认为有效(0=禁用相对对比度) +TRIANGLE_MIN_CONTRAST_DIFF = 15 # 三角形检测超时(毫秒)。超过该时间直接判失败,回退圆心算法(并行时不再等待)。 -TRIANGLE_TIMEOUT_MS = 1000 +# CLAHE 启用或颜色阈值放宽后检测耗时增加,需相应提高(1000→2500) +TRIANGLE_TIMEOUT_MS = 2500 # 三角形检测性能/鲁棒性参数(偏向速度的默认值) # 说明: diff --git a/design_doc/command_record.md b/design_doc/command_record.md index f4f0527..5826176 100644 --- a/design_doc/command_record.md +++ b/design_doc/command_record.md @@ -81,5 +81,6 @@ printf 'AT+MHTTPDLFILE="http://static.shelingxingqiu.com/shoot/v1/main.py","down sudo apt install mtools 6. 相机标定: +然后在板子上跑 test 目录下的 test_camera_rtsp.py ,让相机启动了一个服务,然后在电脑上接收这个视频流,并且跑opencv 内置的标定程序: set OPENCV_FFMPEG_CAPTURE_OPTIONS="rtsp_transport;tcp" -opencv_interactive-calibration -t=chessboard -w=9 -h=6 -sz=0.025 -v="http://192.168.1.55:8000/stream" \ No newline at end of file +opencv_interactive-calibration -t=chessboard -w=9 -h=6 -sz=0.025 -v="http://192.168.1.81:8000/stream" 2>nul diff --git a/shoot_manager.py b/shoot_manager.py index 89c8fad..9e64212 100644 --- a/shoot_manager.py +++ b/shoot_manager.py @@ -166,7 +166,7 @@ def analyze_shot(frame, laser_point=None): tri = tri_result.get('data', {}) if tri.get('ok'): - logger.info(f"[TRI] end {datetime.now()}") + logger.info(f"[TRI] end {datetime.now()} — 使用三角形结果(dx={tri['dx_cm']:.2f},dy={tri['dy_cm']:.2f}cm)") return { "success": True, "result_img": frame, diff --git a/test/test_camera_rtsp.py b/test/test_camera_rtsp.py new file mode 100644 index 0000000..2af0519 --- /dev/null +++ b/test/test_camera_rtsp.py @@ -0,0 +1,36 @@ +# from maix import time, rtsp, camera, image + +# # 1. 初始化摄像头(注意:RTSP需要NV21格式) +# # 分辨率可以根据需要调整,如 640x480 或 1280x720 +# cam = camera.Camera(640, 480, image.Format.FMT_YVU420SP) + +# # 2. 创建并启动RTSP服务器 +# server = rtsp.Rtsp() +# server.bind_camera(cam) +# server.start() + +# # 3. 打印出访问地址,例如: rtsp://192.168.xxx.xxx:8554/live +# print("RTSP 流地址:", server.get_url()) + +# # 4. 保持服务运行 +# while True: +# time.sleep(1) + + + +from maix import camera, time, app, http, image + +# 初始化相机,注意格式要用 FMT_RGB888(JPEG 编码需要 RGB 输入) +cam = camera.Camera(640, 480, image.Format.FMT_RGB888) + +# 创建 JPEG 流服务器 +stream = http.JpegStreamer() +stream.start() + +print("RTSP 替代方案 - HTTP JPEG 流地址: http://{}:{}".format(stream.host(), stream.port())) +print("请在浏览器或 OpenCV 中访问: http://:8000/stream") + +while not app.need_exit(): + img = cam.read() + jpg = img.to_jpeg() # 将 RGB 图像编码为 JPEG + stream.write(jpg) # 推送到 HTTP 客户端 \ No newline at end of file diff --git a/triangle_target.py b/triangle_target.py index 8778750..72e50b5 100644 --- a/triangle_target.py +++ b/triangle_target.py @@ -131,9 +131,9 @@ def detect_triangle_markers( gray_image, orig_gray=None, size_range=(8, 500), - max_interior_gray=90, - dark_pixel_gray=80, - min_dark_ratio=0.70, + max_interior_gray=None, + dark_pixel_gray=None, + min_dark_ratio=None, verbose=True, ): # 读取可调参数(缺省值与 config.py 保持一致) @@ -142,10 +142,24 @@ def detect_triangle_markers( early_exit = int(getattr(_cfg, "TRIANGLE_EARLY_EXIT_CANDIDATES", 4)) block_sizes = tuple(getattr(_cfg, "TRIANGLE_ADAPTIVE_BLOCK_SIZES", (11, 21, 35))) max_combo_n = int(getattr(_cfg, "TRIANGLE_MAX_FILTERED_FOR_COMBO", 10)) + if max_interior_gray is None: + max_interior_gray = int(getattr(_cfg, "TRIANGLE_MAX_INTERIOR_GRAY", 130)) + if dark_pixel_gray is None: + dark_pixel_gray = int(getattr(_cfg, "TRIANGLE_DARK_PIXEL_GRAY", 130)) + if min_dark_ratio is None: + min_dark_ratio = float(getattr(_cfg, "TRIANGLE_MIN_DARK_RATIO", 0.30)) + min_contrast_diff = int(getattr(_cfg, "TRIANGLE_MIN_CONTRAST_DIFF", 15)) except Exception: early_exit = 4 block_sizes = (11, 21, 35) max_combo_n = 10 + if max_interior_gray is None: + max_interior_gray = 130 + if dark_pixel_gray is None: + dark_pixel_gray = 130 + if min_dark_ratio is None: + min_dark_ratio = 0.30 + min_contrast_diff = 15 min_leg, max_leg = size_range min_area = 0.5 * (min_leg ** 2) * 0.1 @@ -202,23 +216,72 @@ def detect_triangle_markers( return False interior = orig_gray[ys, xs] dark_ratio = float(np.mean(interior <= dark_pixel_gray)) - return (mean_val <= max_interior_gray) and (dark_ratio >= min_dark_ratio) + + # 条件1:绝对阈值(三角形内部足够暗) + abs_ok = (mean_val <= max_interior_gray) and (dark_ratio >= min_dark_ratio) + + # 条件2:相对对比度 — 三角形内部比周围背景明显更暗 + contrast_ok = False + if min_contrast_diff > 0: + try: + dilate_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * erode_k + 3, 2 * erode_k + 3)) + mask_dilated = cv2.dilate(mask, dilate_k, iterations=2) + mask_border = cv2.subtract(mask_dilated, mask) + border_nz = cv2.countNonZero(mask_border) + if border_nz > 20: + mean_surround = cv2.mean(orig_gray, mask=mask_border)[0] + contrast_ok = (mean_surround - mean_val) >= min_contrast_diff + except Exception: + pass + + return abs_ok or contrast_ok def _extract_candidates(binary_img): contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) found = [] + # ---- 诊断计数 ---- + _n_area_skip = 0 + _n_3vert = 0 + _n_shape_ok = 0 + _n_color_ok = 0 + _dbg_fail_shape = [] # 记录前几个失败原因 + _dbg_fail_color = [] # 记录前几个颜色失败详情 for cnt in contours: - if cv2.contourArea(cnt) < min_area: + area = cv2.contourArea(cnt) + if area < min_area: + _n_area_skip += 1 continue peri = cv2.arcLength(cnt, True) - approx = cv2.approxPolyDP(cnt, 0.05 * peri, True) + eps = 0.05 * peri if peri > 60 else 0.03 * peri + approx = cv2.approxPolyDP(cnt, eps, True) if len(approx) != 3: continue + _n_3vert += 1 shape = _check_shape(approx) if shape is None: + if len(_dbg_fail_shape) < 3: + pts3 = approx.reshape(3, 2).astype(np.float32) + sides = sorted([np.linalg.norm(pts3[1]-pts3[0]), + np.linalg.norm(pts3[2]-pts3[1]), + np.linalg.norm(pts3[0]-pts3[2])]) + avg_l = (sides[0]+sides[1])/2 + reason = f"avg_leg={avg_l:.1f} range=[{min_leg},{max_leg}] legs={sides[0]:.1f},{sides[1]:.1f} hyp={sides[2]:.1f} exp_hyp={avg_l*1.414:.1f}" + _dbg_fail_shape.append(reason) continue + _n_shape_ok += 1 if not _color_ok(approx): + if len(_dbg_fail_color) < 3 and orig_gray is not None: + mask = np.zeros(orig_gray.shape[:2], dtype=np.uint8) + cv2.fillPoly(mask, [approx], 255) + mean_v = cv2.mean(orig_gray, mask=mask)[0] + ys, xs = np.where(mask > 0) + if len(xs) > 0: + dr = float(np.mean(orig_gray[ys, xs] <= dark_pixel_gray)) + else: + dr = 0 + _dbg_fail_color.append(f"mean={mean_v:.1f}(<={max_interior_gray}?) dark_r={dr:.2f}(>={min_dark_ratio}?)") continue + _n_color_ok += 1 right_pt, avg_leg, pts = shape center_px = np.mean(pts, axis=0).tolist() dedup_key = f"{int(center_px[0] // 10)},{int(center_px[1] // 10)}" @@ -229,6 +292,13 @@ def detect_triangle_markers( "avg_leg": avg_leg, "dedup_key": dedup_key, }) + if verbose: + _log(f"[TRI] _extract: total={len(contours)} area_skip={_n_area_skip} " + f"3vert={_n_3vert} shape_ok={_n_shape_ok} color_ok={_n_color_ok}") + if _dbg_fail_shape: + _log(f"[TRI] shape失败原因(前3): {'; '.join(_dbg_fail_shape)}") + if _dbg_fail_color: + _log(f"[TRI] color失败原因(前3): {'; '.join(_dbg_fail_color)}") return found all_candidates = [] @@ -264,6 +334,15 @@ def detect_triangle_markers( # 1. 最快:全局 Otsu(无需逐像素邻域计算,~10ms) _, b_otsu = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) + # ---- 临时调试:保存 Otsu 二值图供人工检查 ---- + try: + import config as _dbg_cfg + if getattr(_dbg_cfg, 'TRIANGLE_SAVE_DEBUG_IMAGE', False): + _dbg_path = getattr(_dbg_cfg, 'PHOTO_DIR', '/root/phot') + '/tri_otsu_debug.jpg' + cv2.imwrite(_dbg_path, b_otsu) + _log(f"[TRI] DEBUG: Otsu 二值图已保存到 {_dbg_path}") + except Exception: + pass _add_from_binary(b_otsu) # 2. 只在 Otsu 不够时才跑自适应阈值(每次 ~100ms,尽早退出) @@ -414,7 +493,7 @@ def try_triangle_scoring( # 缩图加速:嵌入式 CPU 上图像处理耗时与面积成正比,缩到最长边 320px 可获得 ~4× 加速 # 检测完后把像素坐标乘以 inv_scale 还原到原始分辨率,再送入单应性/PnP(与 K 标定分辨率一致) - MAX_DETECT_DIM = 320 + MAX_DETECT_DIM = 640 long_side = max(h_orig, w_orig) if long_side > MAX_DETECT_DIM: det_scale = MAX_DETECT_DIM / long_side diff --git a/version.py b/version.py index 982a32c..4e5fec1 100644 --- a/version.py +++ b/version.py @@ -4,7 +4,7 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '1.2.10' +VERSION = '1.2.11' # 1.2.0 开始使用C++编译成.so,替换部分代码 # 1.2.1 ota使用加密包 @@ -17,6 +17,7 @@ VERSION = '1.2.10' # 1.2.8 (1) 加快 wifi 下数据传输的速度。(2) 调整射箭时处理的逻辑,优先上报数据,再存照片之类的操作。(3)假如是用户打开激光的,射箭触发后不再关闭激光,因为是调瞄阶段 # 1.2.9 增加电源板的控制和自动关机的功能 # 1.2.10 config formal +# 1.2.11 增加三角形的单应性算法,适配对应的靶纸