Files
archery/test/test_triangle_one_image.py
2026-05-11 16:26:05 +08:00

243 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()