#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 本地图片 → Maix YOLOv5 检测 → 画框保存(用于核对坐标 mode / 多框 union)。 运行环境:MaixCAM / MaixPy(需 maix.image / maix.nn),在项目根或任意目录执行均可。 示例: python test/test_yolo_draw_boxes.py /root/phot/shot_xxx.jpg python test/test_yolo_draw_boxes.py shot.jpg --loader cv2_rgb --conf 0.25 python test/test_yolo_draw_boxes.py shot.jpg --debug python -h # 查看 --loader / --debug / --union 等全部参数 脚本版本(与设备同步用):20260206-yolo-vis """ from __future__ import annotations import argparse import os import sys _ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if _ROOT not in sys.path: sys.path.insert(0, _ROOT) def _load_maix_image(path: str, image_mod): """maix.image.load(部分 JPEG 解码后与 camera.read() 像素布局不一致,可能导致 NPU 全空)。""" return image_mod.load(path) def _load_cv2_rgb_as_maix(path: str, image_mod): """ OpenCV 读盘为 BGR → 转 RGB → 与 shoot_manager 里 image2cv 逆过程一致,供 YOLO input type: rgb。 """ import cv2 arr = cv2.imread(path, cv2.IMREAD_COLOR) if arr is None: raise FileNotFoundError(f"cv2.imread 失败: {path}") arr = cv2.cvtColor(arr, cv2.COLOR_BGR2RGB) return image_mod.cv2image(arr, False, False) def main(): ap = argparse.ArgumentParser( description="YOLO 画框测试(Maix)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="若提示 unrecognized arguments: --debug,说明设备上脚本未更新,请同步仓库中的 test/test_yolo_draw_boxes.py", ) ap.add_argument("image", help="输入图片路径") ap.add_argument("-o", "--output", default="", help="输出图片路径;默认 原名_yolo_vis.jpg") ap.add_argument("-m", "--model", default="", help="覆盖 config.TRIANGLE_YOLO_MODEL_PATH") ap.add_argument("--conf", type=float, default=None, help="置信度阈值") ap.add_argument("--iou", type=float, default=None, help="NMS IoU") ap.add_argument( "--coord", choices=["native", "letterbox"], default="", help="坐标映射;默认读 config.TRIANGLE_YOLO_COORD_MODE", ) ap.add_argument( "--union", action="store_true", help="按 TRIANGLE_YOLO_RING_CLASS_IDS 过滤后画合并外接矩形(与线上 ROI merge=union 一致)", ) ap.add_argument( "--loader", choices=["auto", "maix", "cv2_rgb"], default="auto", help="auto: 先 maix.load,0 框则改用 cv2 RGB(推荐排查「有图但始终 0 框」)", ) ap.add_argument( "--debug", action="store_true", help="打印 detect 原始返回类型与 repr(截断)", ) args = ap.parse_args() try: from maix import image, nn except ImportError: print("[ERR] 需要 MaixPy(maix.image / maix.nn),请在 MaixCAM 上运行。") sys.exit(1) import config as cfg import target_roi_yolo as yroi img_path = os.path.abspath(args.image) if not os.path.isfile(img_path): print(f"[ERR] 找不到图片: {img_path}") sys.exit(1) model_path = (args.model or getattr(cfg, "TRIANGLE_YOLO_MODEL_PATH", "") or "").strip() if not os.path.isfile(model_path): print(f"[ERR] 模型文件不存在: {model_path}") sys.exit(1) conf_th = ( float(args.conf) if args.conf is not None else float(getattr(cfg, "TRIANGLE_YOLO_CONF_TH", 0.5)) ) iou_th = ( float(args.iou) if args.iou is not None else float(getattr(cfg, "TRIANGLE_YOLO_IOU_TH", 0.45)) ) coord_mode = (args.coord or getattr(cfg, "TRIANGLE_YOLO_COORD_MODE", "native")).lower() out_path = args.output.strip() if not out_path: base, ext = os.path.splitext(img_path) ext = ext if ext else ".jpg" out_path = base + "_yolo_vis" + ext det = nn.YOLOv5(model=model_path, dual_buff=False) net_w = int(det.input_width()) net_h = int(det.input_height()) def _run_detect(maix_img, tag: str): r = det.detect(maix_img, conf_th=conf_th, iou_th=iou_th) if args.debug: rlen = len(r) if r is not None and hasattr(r, "__len__") else "n/a" rrepr = repr(r) if len(rrepr) > 300: rrepr = rrepr[:300] + "..." print(f"[DEBUG] loader={tag} raw_type={type(r)} len={rlen} repr={rrepr}") return yroi._normalize_objs(r if r is not None else []), maix_img, tag img = None load_tag = "" objs = [] if args.loader == "cv2_rgb": img = _load_cv2_rgb_as_maix(img_path, image) load_tag = "cv2_rgb" objs, img, load_tag = _run_detect(img, load_tag) elif args.loader == "maix": img = _load_maix_image(img_path, image) load_tag = "maix_load" objs, img, load_tag = _run_detect(img, load_tag) else: # auto img = _load_maix_image(img_path, image) load_tag = "maix_load" objs, img, load_tag = _run_detect(img, load_tag) if len(objs) == 0: print( "[WARN] maix.image.load 在 conf_th=%s 下仍为 0 框,改用 cv2 BGR→RGB→cv2image 重试(常见可恢复)" % conf_th ) img2 = _load_cv2_rgb_as_maix(img_path, image) objs, img, load_tag = _run_detect(img2, "cv2_rgb_retry") src_w, src_h = img.width(), img.height() labels = getattr(det, "labels", None) def _label(cid: int) -> str: if labels is None: return str(cid) try: return str(labels[int(cid)]) except Exception: return str(cid) print( f"[INFO] loader={load_tag} image={src_w}×{src_h}, net_in={net_w}×{net_h}, " f"coord={coord_mode}, conf_th={conf_th}, iou_th={iou_th}" ) print(f"[INFO] NMS 后检测框数量={len(objs)} → {out_path}") if len(objs) == 0: print( "[HINT] 仍为 0 框时常见原因:\n" " 1) 强制 cv2 路径: --loader cv2_rgb\n" " 2) NMS 过严: --iou 0.95\n" " 3) 图与训练分布差太大 / 模型未见过该场景\n" " 4) 用 camera.read() 一帧存盘再测,对比 file 与实时是否一致" ) # 颜色:按类别轮换(仅有 COLOR_* 时常量时用) color_cycle = [] for name in ("RED", "GREEN", "BLUE", "ORANGE", "YELLOW", "CYAN", "MAGENTA"): c = getattr(image, f"COLOR_{name}", None) if c is not None: color_cycle.append(c) if not color_cycle: color_cycle = [getattr(image, "COLOR_RED", 0)] for i, o in enumerate(objs): cid = yroi._det_obj_class_id(o) if cid is None: cid = -1 try: sc = float(o.score) except Exception: sc = 0.0 x0, y0, x1, y1 = yroi._det_to_src_xyxy(o, coord_mode, src_w, src_h, net_w, net_h) ix = int(max(0, min(x0, src_w - 1))) iy = int(max(0, min(y0, src_h - 1))) iw = int(max(1, min(x1 - x0, src_w - ix))) ih = int(max(1, min(y1 - y0, src_h - iy))) col = color_cycle[cid % len(color_cycle)] if cid >= 0 else color_cycle[0] img.draw_rect(ix, iy, iw, ih, color=col) ty = max(0, iy - 14) msg = f"{_label(cid)} {sc:.2f}" img.draw_string(ix, ty, msg, color=col) print(f" #{i} cls={cid} {_label(cid)} score={sc:.3f} xywh=({ix},{iy},{iw},{ih})") if args.union: class_ids = getattr(cfg, "TRIANGLE_YOLO_RING_CLASS_IDS", (0,)) if isinstance(class_ids, int): class_ids = (class_ids,) cand = [o for o in objs if yroi._det_obj_class_id(o) in class_ids] if cand: xy_list = [ yroi._det_to_src_xyxy(o, coord_mode, src_w, src_h, net_w, net_h) for o in cand ] merged = yroi._merge_roi_xyxy(xy_list, "union") if merged: mx0, my0, mx1, my1 = merged mx0 = max(0, min(mx0, src_w - 1)) my0 = max(0, min(my0, src_h - 1)) mx1 = max(mx0 + 1, min(mx1, src_w)) my1 = max(my0 + 1, min(my1, src_h)) uw, uh = int(mx1 - mx0), int(my1 - my0) ucol = getattr(image, "COLOR_GREEN", color_cycle[0]) # 画粗一点的 union:描两遍错位矩形简易模拟加粗 for d in (0, 2): img.draw_rect( int(mx0) - d, int(my0) - d, uw + 2 * d, uh + 2 * d, color=ucol, ) img.draw_string( int(mx0), max(0, int(my0) - 28), f"UNION ({len(cand)} boxes)", color=ucol, ) print(f"[INFO] UNION [{int(mx0)},{int(my0)},{int(mx1)},{int(my1)}] from {len(cand)} boxes") else: print("[WARN] --union 但 RING_CLASS_IDS 过滤后无框") try: img.save(out_path, quality=95) except TypeError: img.save(out_path) print(f"[OK] saved: {out_path}") if __name__ == "__main__": main()