refine the triangle algo

This commit is contained in:
gcw_4spBpAfv
2026-04-21 21:14:12 +08:00
parent ba5ca7e0b3
commit 1bace88f37
7 changed files with 143 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
id: t11 id: t11
name: t11 name: t11
version: 1.2.10 version: 1.2.11
author: t11 author: t11
icon: '' icon: ''
desc: t11 desc: t11

View File

@@ -130,10 +130,23 @@ CAMERA_CALIB_XML = APP_DIR + "/cameraParameters.xml"
TRIANGLE_POSITIONS_JSON = APP_DIR + "/triangle_positions.json" TRIANGLE_POSITIONS_JSON = APP_DIR + "/triangle_positions.json"
# 检测到的三角形边长在图像中的像素范围,分辨率或靶纸占比变化时可微调 # 检测到的三角形边长在图像中的像素范围,分辨率或靶纸占比变化时可微调
TRIANGLE_SIZE_RANGE = (8, 500) TRIANGLE_SIZE_RANGE = (8, 500)
# 三角形检测兜底增强CLAHE更鲁棒但更慢默认关闭以优先速度。 # 三角形检测兜底增强CLAHE更鲁棒但更慢颜色阈值修复后通常不需要,保持关闭以优先速度。
TRIANGLE_ENABLE_CLAHE_FALLBACK = False 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
# 三角形检测性能/鲁棒性参数(偏向速度的默认值) # 三角形检测性能/鲁棒性参数(偏向速度的默认值)
# 说明: # 说明:

View File

@@ -81,5 +81,6 @@ printf 'AT+MHTTPDLFILE="http://static.shelingxingqiu.com/shoot/v1/main.py","down
sudo apt install mtools sudo apt install mtools
6. 相机标定: 6. 相机标定:
然后在板子上跑 test 目录下的 test_camera_rtsp.py 让相机启动了一个服务然后在电脑上接收这个视频流并且跑opencv 内置的标定程序:
set OPENCV_FFMPEG_CAPTURE_OPTIONS="rtsp_transport;tcp" 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" opencv_interactive-calibration -t=chessboard -w=9 -h=6 -sz=0.025 -v="http://192.168.1.81:8000/stream" 2>nul

View File

@@ -166,7 +166,7 @@ def analyze_shot(frame, laser_point=None):
tri = tri_result.get('data', {}) tri = tri_result.get('data', {})
if tri.get('ok'): 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 { return {
"success": True, "success": True,
"result_img": frame, "result_img": frame,

36
test/test_camera_rtsp.py Normal file
View File

@@ -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_RGB888JPEG 编码需要 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://<MaixCAM_IP>:8000/stream")
while not app.need_exit():
img = cam.read()
jpg = img.to_jpeg() # 将 RGB 图像编码为 JPEG
stream.write(jpg) # 推送到 HTTP 客户端

View File

@@ -131,9 +131,9 @@ def detect_triangle_markers(
gray_image, gray_image,
orig_gray=None, orig_gray=None,
size_range=(8, 500), size_range=(8, 500),
max_interior_gray=90, max_interior_gray=None,
dark_pixel_gray=80, dark_pixel_gray=None,
min_dark_ratio=0.70, min_dark_ratio=None,
verbose=True, verbose=True,
): ):
# 读取可调参数(缺省值与 config.py 保持一致) # 读取可调参数(缺省值与 config.py 保持一致)
@@ -142,10 +142,24 @@ def detect_triangle_markers(
early_exit = int(getattr(_cfg, "TRIANGLE_EARLY_EXIT_CANDIDATES", 4)) early_exit = int(getattr(_cfg, "TRIANGLE_EARLY_EXIT_CANDIDATES", 4))
block_sizes = tuple(getattr(_cfg, "TRIANGLE_ADAPTIVE_BLOCK_SIZES", (11, 21, 35))) block_sizes = tuple(getattr(_cfg, "TRIANGLE_ADAPTIVE_BLOCK_SIZES", (11, 21, 35)))
max_combo_n = int(getattr(_cfg, "TRIANGLE_MAX_FILTERED_FOR_COMBO", 10)) 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: except Exception:
early_exit = 4 early_exit = 4
block_sizes = (11, 21, 35) block_sizes = (11, 21, 35)
max_combo_n = 10 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_leg, max_leg = size_range
min_area = 0.5 * (min_leg ** 2) * 0.1 min_area = 0.5 * (min_leg ** 2) * 0.1
@@ -202,23 +216,72 @@ def detect_triangle_markers(
return False return False
interior = orig_gray[ys, xs] interior = orig_gray[ys, xs]
dark_ratio = float(np.mean(interior <= dark_pixel_gray)) 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): def _extract_candidates(binary_img):
contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours, _ = cv2.findContours(binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
found = [] 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: for cnt in contours:
if cv2.contourArea(cnt) < min_area: area = cv2.contourArea(cnt)
if area < min_area:
_n_area_skip += 1
continue continue
peri = cv2.arcLength(cnt, True) 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: if len(approx) != 3:
continue continue
_n_3vert += 1
shape = _check_shape(approx) shape = _check_shape(approx)
if shape is None: 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 continue
_n_shape_ok += 1
if not _color_ok(approx): 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 continue
_n_color_ok += 1
right_pt, avg_leg, pts = shape right_pt, avg_leg, pts = shape
center_px = np.mean(pts, axis=0).tolist() center_px = np.mean(pts, axis=0).tolist()
dedup_key = f"{int(center_px[0] // 10)},{int(center_px[1] // 10)}" 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, "avg_leg": avg_leg,
"dedup_key": dedup_key, "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 return found
all_candidates = [] all_candidates = []
@@ -264,6 +334,15 @@ def detect_triangle_markers(
# 1. 最快:全局 Otsu无需逐像素邻域计算~10ms # 1. 最快:全局 Otsu无需逐像素邻域计算~10ms
_, b_otsu = cv2.threshold(gray_image, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) _, 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) _add_from_binary(b_otsu)
# 2. 只在 Otsu 不够时才跑自适应阈值(每次 ~100ms尽早退出 # 2. 只在 Otsu 不够时才跑自适应阈值(每次 ~100ms尽早退出
@@ -414,7 +493,7 @@ def try_triangle_scoring(
# 缩图加速:嵌入式 CPU 上图像处理耗时与面积成正比,缩到最长边 320px 可获得 ~4× 加速 # 缩图加速:嵌入式 CPU 上图像处理耗时与面积成正比,缩到最长边 320px 可获得 ~4× 加速
# 检测完后把像素坐标乘以 inv_scale 还原到原始分辨率,再送入单应性/PnP与 K 标定分辨率一致) # 检测完后把像素坐标乘以 inv_scale 还原到原始分辨率,再送入单应性/PnP与 K 标定分辨率一致)
MAX_DETECT_DIM = 320 MAX_DETECT_DIM = 640
long_side = max(h_orig, w_orig) long_side = max(h_orig, w_orig)
if long_side > MAX_DETECT_DIM: if long_side > MAX_DETECT_DIM:
det_scale = MAX_DETECT_DIM / long_side det_scale = MAX_DETECT_DIM / long_side

View File

@@ -4,7 +4,7 @@
应用版本号 应用版本号
每次 OTA 更新时,只需要更新这个文件中的版本号 每次 OTA 更新时,只需要更新这个文件中的版本号
""" """
VERSION = '1.2.10' VERSION = '1.2.11'
# 1.2.0 开始使用C++编译成.so替换部分代码 # 1.2.0 开始使用C++编译成.so替换部分代码
# 1.2.1 ota使用加密包 # 1.2.1 ota使用加密包
@@ -17,6 +17,7 @@ VERSION = '1.2.10'
# 1.2.8 1 加快 wifi 下数据传输的速度。2 调整射箭时处理的逻辑优先上报数据再存照片之类的操作。3假如是用户打开激光的射箭触发后不再关闭激光因为是调瞄阶段 # 1.2.8 1 加快 wifi 下数据传输的速度。2 调整射箭时处理的逻辑优先上报数据再存照片之类的操作。3假如是用户打开激光的射箭触发后不再关闭激光因为是调瞄阶段
# 1.2.9 增加电源板的控制和自动关机的功能 # 1.2.9 增加电源板的控制和自动关机的功能
# 1.2.10 config formal # 1.2.10 config formal
# 1.2.11 增加三角形的单应性算法,适配对应的靶纸