refine the triangle algo
This commit is contained in:
2
app.yaml
2
app.yaml
@@ -1,6 +1,6 @@
|
||||
id: t11
|
||||
name: t11
|
||||
version: 1.2.10
|
||||
version: 1.2.11
|
||||
author: t11
|
||||
icon: ''
|
||||
desc: t11
|
||||
|
||||
17
config.py
17
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
|
||||
|
||||
# 三角形检测性能/鲁棒性参数(偏向速度的默认值)
|
||||
# 说明:
|
||||
|
||||
@@ -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"
|
||||
opencv_interactive-calibration -t=chessboard -w=9 -h=6 -sz=0.025 -v="http://192.168.1.81:8000/stream" 2>nul
|
||||
|
||||
@@ -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,
|
||||
|
||||
36
test/test_camera_rtsp.py
Normal file
36
test/test_camera_rtsp.py
Normal 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_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://<MaixCAM_IP>:8000/stream")
|
||||
|
||||
while not app.need_exit():
|
||||
img = cam.read()
|
||||
jpg = img.to_jpeg() # 将 RGB 图像编码为 JPEG
|
||||
stream.write(jpg) # 推送到 HTTP 客户端
|
||||
@@ -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
|
||||
|
||||
@@ -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 增加三角形的单应性算法,适配对应的靶纸
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user