243 lines
9.7 KiB
Python
243 lines
9.7 KiB
Python
#!/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 约定输入为 RGB;OpenCV 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()
|
||
|