batch_multicard_xml.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """单图多卡批量推理 → XML(含 YOLO 分割框 + 每卡 Top-5 card_id)+ 裁剪图。
  4. 与生产 query_all 同逻辑(batch 提特征 + backend 检索 + rank),额外透传 conf。
  5. 用法(249):
  6. CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=1 \
  7. ~/miniconda3/envs/pytorch/bin/python batch_multicard_xml.py \
  8. --images /tmp/mc_batch/in --out /tmp/mc_batch/out
  9. """
  10. import os
  11. import sys
  12. import time
  13. import argparse
  14. import traceback
  15. from xml.sax.saxutils import escape
  16. sys.stdout.reconfigure(encoding="utf-8")
  17. _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  18. sys.path.insert(0, _ROOT)
  19. sys.path.insert(0, os.path.join(_ROOT, "scripts"))
  20. import numpy as np
  21. import cv2
  22. from PIL import Image
  23. import config
  24. import torch
  25. from scripts.cascade_match import DualCascadeMatcher
  26. from modules.feature_extractor_dual import letterbox392, split_half
  27. IMG_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".webp")
  28. def analyze(matcher, path):
  29. """单图 → list[card];与 query_all 同管线,额外带 conf/box/crop。"""
  30. arr = np.array(Image.open(path).convert("RGB"))
  31. instances = matcher.detector.detect_and_crop_all(arr)
  32. if not instances:
  33. return []
  34. ups, los = [], []
  35. for inst in instances:
  36. up, lo = split_half(letterbox392(Image.fromarray(inst["crop"])))
  37. ups.append(up)
  38. los.append(lo)
  39. q_us = matcher.extractor.extract_upper(ups)
  40. q_ls = matcher.extractor.extract_lower(los)
  41. out = []
  42. for i, inst in enumerate(instances):
  43. q_u, q_l = q_us[i], q_ls[i]
  44. if q_u is None or q_l is None or np.isnan(q_u).any() or np.isnan(q_l).any():
  45. continue
  46. cand_ids, su, sl = matcher._query_raw(q_u, q_l)
  47. ranked = matcher.rank({"cand_ids": cand_ids, "upper_sim": su, "lower_sim": sl},
  48. matcher.alpha)
  49. if not ranked:
  50. continue
  51. out.append({
  52. "box": inst["box"], "label": inst.get("label"), "conf": inst.get("conf"),
  53. "crop": inst["crop"],
  54. "top_k": [(r[0], round(r[1], 4)) for r in ranked[:5]],
  55. })
  56. return out
  57. def main():
  58. ap = argparse.ArgumentParser()
  59. ap.add_argument("--images", required=True)
  60. ap.add_argument("--out", required=True)
  61. args = ap.parse_args()
  62. os.makedirs(args.out, exist_ok=True)
  63. crops_dir = os.path.join(args.out, "crops")
  64. os.makedirs(crops_dir, exist_ok=True)
  65. files = sorted(f for f in os.listdir(args.images)
  66. if f.lower().endswith(IMG_EXTS))
  67. print(f"[Batch] {len(files)} 张图", flush=True)
  68. matcher = DualCascadeMatcher(device=torch.device("cuda:0"),
  69. detector_device=torch.device("cuda:0"),
  70. milvus_alias="batch")
  71. xml_path = os.path.join(args.out, "多卡识别结果.xml")
  72. n_cards_total = 0
  73. n_err = 0
  74. t0 = time.time()
  75. with open(xml_path, "w", encoding="utf-8") as fx:
  76. fx.write('<?xml version="1.0" encoding="utf-8"?>\n')
  77. fx.write(f'<results source="full_images" total="{len(files)}">\n')
  78. for idx, fname in enumerate(files, 1):
  79. path = os.path.join(args.images, fname)
  80. stem = os.path.splitext(fname)[0]
  81. try:
  82. cards = analyze(matcher, path)
  83. n_cards_total += len(cards)
  84. except Exception as e:
  85. n_err += 1
  86. print(f" [ERR] {fname}: {e}", flush=True)
  87. traceback.print_exc()
  88. fx.write(f' <image name="{escape(fname)}" error="{escape(str(e)[:200])}"/>\n')
  89. continue
  90. fx.write(f' <image name="{escape(fname)}" cards="{len(cards)}">\n')
  91. for ci, c in enumerate(cards, 1):
  92. # 裁剪图(cv2 质量 100;文件名 = 图名__c序号)
  93. crop_name = f"{stem}__c{ci}.jpg"
  94. ok_img = cv2.imwrite(os.path.join(crops_dir, crop_name),
  95. cv2.cvtColor(c["crop"], cv2.COLOR_RGB2BGR),
  96. [int(cv2.IMWRITE_JPEG_QUALITY), 100])
  97. x1, y1, x2, y2 = c["box"]
  98. fx.write(f' <card index="{ci}" crop="{escape(crop_name)}" saved="{int(bool(ok_img))}"'
  99. f' card_type="{escape(str(c["label"]))}" conf="{c["conf"]:.3f}">\n')
  100. fx.write(f' <box x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}"/>\n')
  101. fx.write(f' <top1 card_id="{c["top_k"][0][0]}"/>\n')
  102. fx.write(' <matches>\n')
  103. for r, (cid, fus) in enumerate(c["top_k"], 1):
  104. fx.write(f' <match rank="{r}" card_id="{cid}"'
  105. f' fusion="{fus}" match_rate="{int(round(fus * 100))}"/>\n')
  106. fx.write(' </matches>\n')
  107. fx.write(' </card>\n')
  108. fx.write(' </image>\n')
  109. if idx % 20 == 0 or idx == len(files):
  110. rate = idx / (time.time() - t0)
  111. print(f" [{idx}/{len(files)}] {rate:.2f} 张/s 卡数累计 {n_cards_total}", flush=True)
  112. fx.write('</results>\n')
  113. print(f"\n[Done] {len(files)} 图 / {n_cards_total} 卡 / {n_err} 错误 "
  114. f"总耗时 {time.time()-t0:.0f}s", flush=True)
  115. print(f"[Done] XML: {xml_path}", flush=True)
  116. print(f"[Done] 裁剪图: {crops_dir}", flush=True)
  117. if __name__ == "__main__":
  118. main()