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

@@ -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