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