audit_detect_all.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 真实拍摄图多卡检测审计(249 上跑):对比"关闭合并 vs 开启合并"的检出差异,
  5. 校准 CARD_MERGE_DIST_PX 阈值。
  6. 数据源二选一:
  7. --minio N MinIO grading/capp_img_data/ 最新 N 张(C 端真实拍摄图,默认 50)
  8. --dir PATH 本地目录(如 data/query_images/)
  9. 每张图输出:关闭合并的原始检出数 → 开启合并后检出数、合并发生明细
  10. (谁吸收了谁、中心距、conf)。结尾汇总:合并触发的图占比、误杀风险(原始多实例
  11. 但合并后变少的例子需要人工过目)。
  12. 用法(pytorch env):
  13. python tools/audit_detect_all.py --minio 50
  14. python tools/audit_detect_all.py --dir data/query_images
  15. """
  16. import os
  17. import sys
  18. import argparse
  19. sys.stdout.reconfigure(encoding="utf-8")
  20. _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  21. sys.path.insert(0, _ROOT)
  22. import numpy as np
  23. from PIL import Image
  24. import config
  25. from modules.yolo_detector import CardDetector
  26. def _fetch_minio(limit, out_dir, prefix=None):
  27. """拉 grading 桶指定前缀最新 limit 张到 out_dir,返回本地图路径列表。"""
  28. import config as _cfg
  29. cfg = _cfg.CAPP_MINIO
  30. from minio import Minio
  31. client = Minio(cfg["endpoint"], access_key=cfg["access_key"],
  32. secret_key=cfg["secret_key"], secure=bool(cfg.get("secure")))
  33. objs = []
  34. for o in client.list_objects(cfg["bucket"], prefix=(prefix or cfg["prefix"]) + "/", recursive=True):
  35. if o.object_name.lower().endswith((".jpg", ".jpeg", ".png", ".webp")):
  36. objs.append(o)
  37. objs.sort(key=lambda o: o.last_modified or 0, reverse=True)
  38. os.makedirs(out_dir, exist_ok=True)
  39. paths = []
  40. for o in objs[:limit]:
  41. dst = os.path.join(out_dir, os.path.basename(o.object_name))
  42. if not os.path.exists(dst) or os.path.getsize(dst) == 0:
  43. try:
  44. client.fget_object(cfg["bucket"], o.object_name, dst)
  45. except Exception as e:
  46. print(f" [skip] {o.object_name}: {e}", flush=True)
  47. continue
  48. paths.append(dst)
  49. return paths
  50. def _audit_one(detector, path, thr):
  51. """对一张图跑两遍(关/开合并),返回 dict。"""
  52. try:
  53. arr = np.array(Image.open(path).convert("RGB"))
  54. except Exception as e:
  55. return {"path": path, "error": str(e)[:100]}
  56. detector.merge_dist_px = 0.0
  57. raw = detector.detect_and_crop_all(arr)
  58. detector.merge_dist_px = thr
  59. merged = detector.detect_and_crop_all(arr)
  60. return {
  61. "path": path,
  62. "w": arr.shape[1], "h": arr.shape[0],
  63. "raw_n": len(raw),
  64. "merged_n": len(merged),
  65. "raw_confs": [round(it["conf"], 3) for it in raw],
  66. "merged_confs": [round(it["conf"], 3) for it in merged],
  67. "merged_centers": [(round(c[0]), round(c[1])) for c in (it["center"] for it in merged)],
  68. "merged": len(merged) < len(raw),
  69. }
  70. def main():
  71. ap = argparse.ArgumentParser()
  72. ap.add_argument("--minio", type=int, default=0, help="拉取 grading 桶最新 N 张")
  73. ap.add_argument("--prefix", type=str, default=None,
  74. help="MinIO 对象前缀(默认 config.CAPP_MINIO.prefix=capp_img_data;"
  75. "拍摄图更多可用 raspi_img_data)")
  76. ap.add_argument("--dir", type=str, default=None, help="本地图片目录")
  77. ap.add_argument("--limit", type=int, default=50)
  78. args = ap.parse_args()
  79. if args.dir:
  80. paths = sorted(
  81. os.path.join(args.dir, f) for f in os.listdir(args.dir)
  82. if f.lower().endswith((".jpg", ".jpeg", ".png", ".webp", ".bmp")))[:args.limit]
  83. elif args.minio:
  84. paths = _fetch_minio(args.minio, os.path.join(config.DATA_DIR, "audit_imgs"), args.prefix)
  85. else:
  86. ap.error("需 --minio N 或 --dir PATH")
  87. detector = CardDetector(config.YOLO_MODEL_PATH)
  88. thr = float(getattr(detector, "merge_dist_px", 50.0))
  89. print(f"[Audit] 阈值 CARD_MERGE_DIST_PX={thr} 图片数={len(paths)}\n", flush=True)
  90. n_merged = n_multi = n_zero = n_err = 0
  91. for i, p in enumerate(paths, 1):
  92. r = _audit_one(detector, p, thr)
  93. if "error" in r:
  94. n_err += 1
  95. print(f"[{i}/{len(paths)}] {os.path.basename(p)} 读取失败: {r['error']}", flush=True)
  96. continue
  97. n_merged += int(r["merged"])
  98. n_multi += int(r["merged_n"] > 1)
  99. n_zero += int(r["merged_n"] == 0)
  100. flag = " ← 合并触发" if r["merged"] else ""
  101. print(f"[{i}/{len(paths)}] {os.path.basename(p)} {r['w']}×{r['h']} "
  102. f"raw={r['raw_n']}({r['raw_confs']}) → merged={r['merged_n']}"
  103. f"({r['merged_confs']}) centers={r['merged_centers']}{flag}", flush=True)
  104. n_ok = len(paths) - n_err
  105. print(f"\n[Summary] 有效 {n_ok} 张:合并触发 {n_merged}({n_merged / max(1, n_ok):.1%}) "
  106. f"多卡图 {n_multi} 未检出 {n_zero} 读取失败 {n_err}", flush=True)
  107. print("[Hint] 合并触发的样例请人工过目:若 raw 里本就是两张紧贴的不同卡被并成一张,"
  108. "属误杀 → 调小 CARD_MERGE_DIST_PX 或启用 CARD_MERGE_MIN_IOU。", flush=True)
  109. if __name__ == "__main__":
  110. main()