target_roi_yolo.py

This commit is contained in:
gcw_4spBpAfv
2026-05-11 16:26:05 +08:00
parent a090579db9
commit bd5ebdaa43
5 changed files with 2385 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
单张图片快速测试:三角形四角标记识别 + 单应性落点 + PnP 估距
用法(在板子上):
python3 test/test_triangle_one_image.py --image /root/phot/xxx.jpg --out /root/phot/tri_out.jpg
调参对比(不改代码,临时覆盖 config.TRIANGLE_*
python3 test/test_triangle_one_image.py --image /root/phot/xxx.jpg --preset shadow
python3 test/test_triangle_one_image.py --image /root/phot/xxx.jpg --max-interior-gray 160 --min-dark-ratio 0.20
"""
import argparse
import json
import os
import time
from typing import Any, Dict, Tuple
import cv2
import numpy as np
import config
import triangle_target as tri_mod
from triangle_target import (
detect_triangle_markers,
load_camera_from_xml,
load_triangle_positions,
try_triangle_scoring,
)
def _apply_overrides(args) -> None:
# 预设:阴影/低对比度场景更宽松(尽量保持速度:不启 CLAHE
if args.preset == "shadow":
setattr(config, "TRIANGLE_ENABLE_CLAHE_FALLBACK", False)
setattr(config, "TRIANGLE_MIN_CONTRAST_DIFF", 0)
setattr(config, "TRIANGLE_MAX_INTERIOR_GRAY", 160)
setattr(config, "TRIANGLE_DARK_PIXEL_GRAY", 160)
setattr(config, "TRIANGLE_MIN_DARK_RATIO", 0.20)
# adaptive 只在 Otsu 失败时尝试,保持尝试次数很少
setattr(config, "TRIANGLE_ADAPTIVE_BLOCK_SIZES", (21,))
# 手动覆盖(优先级高于 preset
if args.max_interior_gray is not None:
setattr(config, "TRIANGLE_MAX_INTERIOR_GRAY", int(args.max_interior_gray))
if args.dark_pixel_gray is not None:
setattr(config, "TRIANGLE_DARK_PIXEL_GRAY", int(args.dark_pixel_gray))
if args.min_dark_ratio is not None:
setattr(config, "TRIANGLE_MIN_DARK_RATIO", float(args.min_dark_ratio))
if args.min_contrast_diff is not None:
setattr(config, "TRIANGLE_MIN_CONTRAST_DIFF", int(args.min_contrast_diff))
if args.detect_scale is not None:
setattr(config, "TRIANGLE_DETECT_SCALE", float(args.detect_scale))
if args.adaptive_blocks is not None:
bs = tuple(int(x) for x in args.adaptive_blocks.split(",") if x.strip())
setattr(config, "TRIANGLE_ADAPTIVE_BLOCK_SIZES", bs)
def _dump_config() -> Dict[str, Any]:
keys = [
"TRIANGLE_DETECT_SCALE",
"TRIANGLE_SIZE_RANGE",
"TRIANGLE_MAX_INTERIOR_GRAY",
"TRIANGLE_DARK_PIXEL_GRAY",
"TRIANGLE_MIN_DARK_RATIO",
"TRIANGLE_MIN_CONTRAST_DIFF",
"TRIANGLE_ADAPTIVE_BLOCK_SIZES",
"TRIANGLE_MAX_FILTERED_FOR_COMBO",
"TRIANGLE_EARLY_EXIT_CANDIDATES",
"TRIANGLE_ENABLE_CLAHE_FALLBACK",
]
out = {}
for k in keys:
out[k] = getattr(config, k, None)
return out
def _draw_tri_debug(img_bgr: np.ndarray, tri: Dict[str, Any]) -> np.ndarray:
out = img_bgr.copy()
markers = tri.get("markers") or []
# 画三角形轮廓 + center + id
for m in markers:
corners = np.array(m.get("corners", []), dtype=np.int32)
if corners.size == 0:
continue
cv2.polylines(out, [corners], True, (0, 255, 0), 2)
c = m.get("center") or (corners[:, 0].mean(), corners[:, 1].mean())
cx, cy = int(c[0]), int(c[1])
cv2.circle(out, (cx, cy), 4, (0, 0, 255), -1)
mid = m.get("id", "?")
cv2.putText(out, f"T{mid}", (cx - 18, cy - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 1)
# 若有 homography画靶心把 (0,0) 反投影到图像)
H = tri.get("homography")
if H is not None:
try:
H = np.array(H, dtype=np.float64)
H_inv = np.linalg.inv(H)
c_img = cv2.perspectiveTransform(np.array([[[0.0, 0.0]]], dtype=np.float32), H_inv)[0][0]
ocx, ocy = int(c_img[0]), int(c_img[1])
cv2.circle(out, (ocx, ocy), 5, (0, 0, 255), -1)
cv2.circle(out, (ocx, ocy), 10, (0, 0, 255), 1)
except Exception:
pass
# 叠加结果信息
lines = []
if tri.get("ok"):
lines.append("tri_ok=True")
if tri.get("dx_cm") is not None and tri.get("dy_cm") is not None:
lines.append(f"dx,dy=({tri['dx_cm']:.2f},{tri['dy_cm']:.2f})cm")
if tri.get("distance_m") is not None:
lines.append(f"dist={float(tri['distance_m']):.2f}m")
else:
lines.append("tri_ok=False")
y0 = 22
for i, t in enumerate(lines):
cv2.putText(out, t, (10, y0 + i * 18), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 1)
return out
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--image", required=True, help="输入图片路径jpg/png")
ap.add_argument("--out", default="", help="输出标注图片路径(可选)")
ap.add_argument("--laser-x", type=int, default=-1, help="激光点 x像素默认用图像中心")
ap.add_argument("--laser-y", type=int, default=-1, help="激光点 y像素默认用图像中心")
ap.add_argument("--preset", choices=["", "shadow"], default="", help="调参预设shadow=阴影更鲁棒,不启 CLAHE")
ap.add_argument("--max-interior-gray", type=int, default=None)
ap.add_argument("--dark-pixel-gray", type=int, default=None)
ap.add_argument("--min-dark-ratio", type=float, default=None)
ap.add_argument("--min-contrast-diff", type=int, default=None)
ap.add_argument("--detect-scale", type=float, default=None)
ap.add_argument("--adaptive-blocks", default=None, help="例如: 11,21 ;为空表示不改")
ap.add_argument("--verbose", action="store_true", help="输出更多检测阶段信息")
args = ap.parse_args()
_apply_overrides(args)
# triangle_target.py 的日志默认写到 logger_manager在离线脚本里 logger 可能未初始化。
# verbose 模式下把 _log 重定向为 print方便直接看到诊断信息。
if args.verbose:
try:
tri_mod._log = lambda msg: print(str(msg))
except Exception:
pass
img_bgr = cv2.imread(args.image, cv2.IMREAD_COLOR)
if img_bgr is None:
raise SystemExit(f"读图失败:{args.image}")
# triangle_target.try_triangle_scoring 约定输入为 RGBOpenCV imread 返回 BGR
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
h, w = img_bgr.shape[:2]
if args.laser_x >= 0 and args.laser_y >= 0:
laser_point = (int(args.laser_x), int(args.laser_y))
else:
laser_point = (w // 2, h // 2)
K, dist = load_camera_from_xml(getattr(config, "CAMERA_CALIB_XML", ""))
pos = load_triangle_positions(getattr(config, "TRIANGLE_POSITIONS_JSON", ""))
print("[tri-test] image:", args.image, "shape:", (h, w))
print("[tri-test] laser_point:", laser_point)
print("[tri-test] calib_ok:", bool(K is not None and dist is not None), "pos_ok:", bool(pos))
print("[tri-test] config:", json.dumps(_dump_config(), ensure_ascii=False))
# 先单独跑一次三角形候选检测,便于区分“没找到候选” vs “找到候选但评分/单应性失败”
scale = float(getattr(config, "TRIANGLE_DETECT_SCALE", 0.5) or 0.5)
if not (0.05 <= scale <= 1.0):
scale = 0.5
long_side = max(h, w)
max_dim = max(64, int(long_side * scale))
if long_side > max_dim:
det_scale = max_dim / long_side
det_w = int(w * det_scale)
det_h = int(h * det_scale)
img_det = cv2.resize(img_bgr, (det_w, det_h), interpolation=cv2.INTER_LINEAR)
inv_scale = 1.0 / det_scale
size_range_det = (
max(4, int(getattr(config, "TRIANGLE_SIZE_RANGE", (8, 500))[0] * det_scale)),
max(8, int(getattr(config, "TRIANGLE_SIZE_RANGE", (8, 500))[1] * det_scale)),
)
else:
img_det = img_bgr
inv_scale = 1.0
size_range_det = getattr(config, "TRIANGLE_SIZE_RANGE", (8, 500))
gray = cv2.cvtColor(img_det, cv2.COLOR_BGR2GRAY)
markers_det = detect_triangle_markers(
gray,
orig_gray=gray,
size_range=size_range_det,
verbose=bool(args.verbose),
)
if inv_scale != 1.0 and markers_det:
for m in markers_det:
m["center"] = [m["center"][0] * inv_scale, m["center"][1] * inv_scale]
m["corners"] = [[c[0] * inv_scale, c[1] * inv_scale] for c in m["corners"]]
print("[tri-test] markers_found:", len(markers_det), "ids:", [m.get("id") for m in markers_det])
t0 = time.time()
tri = try_triangle_scoring(
img_rgb, # try_triangle_scoring 期望 RGB
laser_point,
pos,
K,
dist,
size_range=getattr(config, "TRIANGLE_SIZE_RANGE", (8, 500)),
)
dt_ms = int(round((time.time() - t0) * 1000))
print("[tri-test] elapsed_ms:", dt_ms)
print(json.dumps(tri, ensure_ascii=False, indent=2))
if args.out:
out_path = args.out
# 允许传目录(如 ./),自动生成文件名;未带扩展名时默认 .jpg
if out_path.endswith("/") or out_path.endswith("\\") or os.path.isdir(out_path):
out_path = os.path.join(out_path, "tri_out.jpg")
root, ext = os.path.splitext(out_path)
if not ext:
out_path = root + ".jpg"
# 若 try_triangle_scoring 失败且没带回 markers至少把候选 markers 画出来,方便肉眼判断
tri_for_draw = tri if isinstance(tri, dict) else {"ok": False}
if not tri_for_draw.get("markers") and markers_det:
tri_for_draw = dict(tri_for_draw)
tri_for_draw["markers"] = markers_det
out_img = _draw_tri_debug(img_bgr, tri_for_draw)
ok = cv2.imwrite(out_path, out_img)
if not ok:
raise SystemExit(f"写图失败(可能是不支持的扩展名):{out_path}")
print("[tri-test] wrote:", out_path)
if __name__ == "__main__":
main()