| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 |
- # -*- coding: utf-8 -*-
- """
- attribute_detector.py - 宝可梦卡牌属性检测(火/水/草/电...)
- 与 cat16 三属性流水线(ebay_sync/infer_cat16_pipeline.py)同款配方:
- 裁卡图 → roi_box.json 的 box3 归一化框(右上角元素图标区)裁小图
- → 4x LANCZOS 放大 → yolo26s_det_attribute 检测取 top1 类别名
- 整卡直接喂模型检不出框(图标太小),必须先裁 ROI 再放大。
- 轻量常驻,加载 20MB pt,推理 ~10ms。
- """
- import os
- import json
- import numpy as np
- from PIL import Image
- from modules.ultralytics_compat import import_yolo
- _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- ATTRIBUTE_MODEL_PATH = os.path.join(
- _ROOT, "ultralytics/runs/detect/yolo26s_det_attribute/weights/best.pt"
- )
- ROI_JSON_PATH = os.path.join(
- _ROOT, "data_all/卡牌区域/images_pokemon_card_crops/roi_box.json"
- )
- BOX3_LABEL = "box3" # 右上角元素图标区(归一化 x1,y1,x2,y2)
- UPSCALE = 4 # LANCZOS 放大倍数(同 cat16 / OCR v5 方案)
- def _load_box3(path=None):
- """roi_box.json → box3 归一化框 (x1, y1, x2, y2);缺失返回 None。"""
- try:
- with open(path or ROI_JSON_PATH, encoding="utf-8-sig") as f:
- rois = json.load(f)["rois_normalized"]
- for item in rois:
- if item["label"] == BOX3_LABEL:
- return (item["x1"], item["y1"], item["x2"], item["y2"])
- except Exception:
- pass
- return None
- class AttributeDetector:
- def __init__(self, model_path=None, roi_json_path=None):
- self.roi = _load_box3(roi_json_path)
- if self.roi is None:
- print("[AttributeDetector] roi_box.json 缺失或无 box3,attribute 将始终为 None", flush=True)
- self.model = None
- return
- YOLO = import_yolo()
- path = model_path or ATTRIBUTE_MODEL_PATH
- if not os.path.exists(path):
- print("[AttributeDetector] 模型不存在: %s,attribute 将始终为 None" % path, flush=True)
- self.model = None
- return
- self.model = YOLO(path, task="detect")
- self.device = "" # 空串走auto分支: vendored select_device(device=0)会把CVD覆盖成0从而撞GTX1060
- self.names = self.model.model.names
- print("[AttributeDetector] loaded %d classes: %s" % (len(self.names), list(self.names.values())), flush=True)
- def _crop_roi_4x(self, img_rgb):
- """裁卡 RGB → box3 ROI 裁块 → 4x LANCZOS 放大(cat16 同款)。"""
- h, w = img_rgb.shape[:2]
- x1, y1, x2, y2 = self.roi
- px1, py1 = max(0, int(x1 * w)), max(0, int(y1 * h))
- px2, py2 = min(w, int(x2 * w)), min(h, int(y2 * h))
- if px2 <= px1 or py2 <= py1:
- return None
- patch = Image.fromarray(img_rgb[py1:py2, px1:px2])
- return np.array(patch.resize((patch.size[0] * UPSCALE, patch.size[1] * UPSCALE), Image.LANCZOS))
- def predict(self, img_rgb):
- """输入裁卡 RGB numpy 数组,返回元素标签字符串(如 '草'/'水')或 None。"""
- if self.model is None or img_rgb is None:
- return None
- roi_img = self._crop_roi_4x(img_rgb)
- if roi_img is None:
- return None
- results = self.model.predict(
- roi_img, conf=0.25, imgsz=640,
- device=self.device, verbose=False,
- )
- if not results:
- return None
- res = results[0]
- boxes = res.boxes
- if boxes is None or len(boxes) == 0:
- return None
- # 取置信度最高的实例类别名
- confs = boxes.conf.cpu().numpy()
- clses = boxes.cls.cpu().numpy()
- best_idx = int(np.argmax(confs))
- cls_id = int(clses[best_idx])
- return self.names.get(cls_id)
|