grading_detector.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. """
  2. 评级卡检测模块(评级封装壳上的公司标签识别)
  3. 用 YOLO26 训练的 card_v1/best.pt 检测交易图上的评级公司标签:
  4. PSA / BGS / CGC / SGC(含子类 AUTHENTIC / AUTO / OLD),共 11 类。
  5. ⚠️ 必须在【原图】上检测,不能在卡牌裁切图上:
  6. 卡牌 YOLO(yolov11n_card_seg) 会把卡面裁出来、丢掉评级壳,
  7. 评级标签(印在壳的边框/正面)会随之被裁掉,所以在裁切图上几乎检不到。
  8. 正确做法:在 match_transactions.py 里,卡牌 YOLO 裁切【之前】对原图跑一次本检测。
  9. 定位:本模块的输出是匹配结果的【额外字段】,用于回填/校验交易表的 grade_company,
  10. 不参与 DINOv2 特征提取与向量检索(与语种 detected_lang 同级)。
  11. """
  12. import os
  13. import numpy as np
  14. from modules.ultralytics_compat import import_yolo
  15. # 11 个类别,顺序必须与 ultralytics/data.yaml 的 names 一致(card_v1 训练时定的)
  16. # 0:BGS 1:BGS-AUTHENTIC 2:BGS-AUTO 3:CGC 4:CGC-AUTO 5:CGC-OLD
  17. # 6:PSA 7:PSA-AUTHENTIC 8:SGC 9:SGC-AUTO 10:SGC-OLD
  18. GRADING_NAMES = [
  19. "BGS", "BGS-AUTHENTIC", "BGS-AUTO",
  20. "CGC", "CGC-AUTO", "CGC-OLD",
  21. "PSA", "PSA-AUTHENTIC",
  22. "SGC", "SGC-AUTO", "SGC-OLD",
  23. ]
  24. def parse_label(name):
  25. """把检测类名拆成 (公司, 子类)。
  26. 'PSA-AUTHENTIC' -> ('PSA', 'AUTHENTIC');'PSA' -> ('PSA', None)。
  27. 公司即交易表 grade_company 字段的取值(PSA/BGS/CGC/SGC)。"""
  28. parts = name.split("-", 1)
  29. return parts[0], (parts[1] if len(parts) > 1 else None)
  30. class GradingDetector:
  31. """评级公司标签检测器(YOLO26 detect)"""
  32. def __init__(self, model_path, conf=0.25, imgsz=640, device=None):
  33. YOLO = import_yolo() # 规避框架根下同名目录对 ultralytics 的命名空间包遮蔽
  34. if not os.path.exists(model_path):
  35. raise FileNotFoundError(f"评级检测模型不存在: {model_path}")
  36. self.model = YOLO(model_path, task="detect")
  37. self.conf = conf
  38. self.imgsz = imgsz
  39. self.device = device
  40. def detect(self, img, return_box=False):
  41. """
  42. 在【原图】上检测评级公司标签,取置信度最高的一个。
  43. Args:
  44. img: 图片路径(str) 或 numpy 数组(路径输入用 BGR,数组按原样,颜色不影响类别)
  45. return_box: 是否返回标签框
  46. Returns:
  47. company: 'PSA'/'BGS'/'CGC'/'SGC' 或 None(未检出)
  48. subtype: 'AUTHENTIC'/'AUTO'/'OLD' 或 None
  49. conf: float 或 None
  50. box: (x1,y1,x2,y2) 或 None(仅 return_box=True)
  51. """
  52. none = (None, None, None, None) if return_box else (None, None, None)
  53. is_path = isinstance(img, str)
  54. results = self.model.predict(
  55. img if is_path else img,
  56. conf=self.conf, imgsz=self.imgsz, device=self.device, verbose=False,
  57. )
  58. if not results:
  59. return none
  60. boxes = results[0].boxes
  61. if boxes is None or len(boxes) == 0:
  62. return none
  63. confs = boxes.conf.cpu().numpy()
  64. clses = boxes.cls.cpu().numpy().astype(int)
  65. xyxy = boxes.xyxy.cpu().numpy()
  66. best = int(np.argmax(confs))
  67. if clses[best] >= len(GRADING_NAMES):
  68. return none
  69. company, subtype = parse_label(GRADING_NAMES[clses[best]])
  70. c = float(confs[best])
  71. if return_box:
  72. return company, subtype, c, tuple(float(v) for v in xyxy[best])
  73. return company, subtype, c