⚠️ 范围声明:本文档只讲卡牌本体区域(整张宝可梦/球星卡卡面)的检测裁切与正立矫正, 对应
modules/yolo_detector.py(CardDetector)。 不是评级封装壳上的评级公司标签条检测(那部分见modules/grading_detector.py+best.pt(YOLO26), 是在原图上单独跑的另一个模型,用于识别 PSA/BGS/CGC/SGC,二者互不影响,详见文末「区别」一节)。2026-08 更新:查询侧(待匹配拍摄图)的检测/裁切模型由
yolov11n_card_seg01.onnx(YOLOv11 分割,ONNX/CPU) 升级为card_seg_pn_v2(yolo26s-seg,2073 张宝可梦+球星卡混合数据全新训练,73 服务器已导出 TensorRT FP16 engine), 裁切策略由"护栏式 warp(has_perspective阈值判断是否值得矫正)"改为始终 warp(valid_quad通过即摆正)。 图库/向量库特征不受影响——图库本来就是正立清晰图,本次只优化"待匹配拍摄图"的裁切几何,不改建库预处理、不重建 78272 张图库。 旧模型/旧护栏逻辑仍保留作CARD_CROP_MODE=bbox_legacy回退路径,见第十节。
卡牌区域的"裁切→正立"不是一个模型/一步完成的,而是两个独立环境接力:
原图
│
│ ①【pytorch 环境】yolo26s-seg 分割检测卡牌区域(TRT可选)
│ modules/yolo_detector.py (CardDetector)
│ 模型: card_seg_v2/best.pt(或同目录 best.engine) task=segment
▼
box(x1,y1,x2,y2) + mask(二值图,与原图同尺寸)
│
│ ②【pytorch 环境】mask → 4角点 → 透视矫正(warpPerspective),valid_quad通过即摆正
│ modules/rectifier.py: mask_to_corners / order_corners / warp_perspective
▼
拉正后的卡牌矩形图(几何已摆正;细小/大角度朝向仍可能横躺/倒置)
│
│ ③【paddleocr 环境,跨进程/跨环境,独立于①②】PaddleOCR 识别文字方向(可选,未接入查询主链路)
│ ocr_judge.py / ebay_sync/ebay_lang_judge.py
│ doc_preprocessor_res.angle ∈ {0, 90, 180, 270}
▼
angle
│
│ ④【pytorch 环境】用 angle 把卡牌转到竖向正立(可选);查询侧默认只用几何 ensure_portrait
│ modules/rectifier.py: rotate_to_portrait(img, angle) / ensure_portrait(img)
▼
正立竖向卡牌图 → 供 DINOv2 提特征 / 向量检索
为什么裁切和判方向要拆两个环境:①②在 pytorch 环境(依赖 ultralytics),③在 paddleocr 环境
(依赖 paddleocr==3.2.0 + paddlepaddle==3.2.0),两套包不能装在同一 conda 环境,只能拆成独立步骤、
分别产出中间文件(裁切图 / JSON),再在④里合流。
查询侧(
CardDetector.detect_and_crop,CARD_CROP_MODE=seg_warp默认路径)目前不接入③④的 OCR angle—— 大角度朝向错误(90°/180°/270°级别)本轮明确判断为"可人工修正"、性价比不高,不做自动纠正; 只做①②的几何检测+透视摆正+ensure_portrait(纯几何 w>h 判断转90°),细节见第五节 5.4。 ③④这条 OCR 链路仍保留给 cat16 三属性识别等其它子系统使用(见第九节表格)。
modules/yolo_detector.py · CardDetector)card_seg_pn_v2(ultralytics/runs/segment/card_seg_pn_v2/weights/best.pt),task="segment"(yolo26s 分割模型,不是纯 detect)
CARD_SEG_USE_TRT=1 时优先加载同目录同名 best.engine(73 服务器已导出 FP16,检测延迟 728ms→49ms,约 15×),加载失败自动回退 .ptconfig.py):conf=0.25,imgsz=640np.argmax(areas)),不严格按类别过滤两种取值方式,服务于不同下游:
| 方法 | 返回 | 用途 |
|---|---|---|
detect_and_crop(img) |
按 crop_mode 处理后的 RGB 图(详见第五节) |
查询侧主入口——crop_mode=seg_warp(默认)走 mask→4点→warp;bbox_legacy 只矩形裁切 |
detect_box_mask(img) |
(box, mask, orig_rgb),不裁切 |
底层方法,detect_and_crop(seg_warp) 内部调用;也可供其它脚本自行组合 rectifier |
detect_box_mask 里 mask 的取法:res.masks.data(分割置信度>0.5 二值化),并 resize 回原图尺寸,与 box 对应同一个最大面积实例。
图库/建库侧不受影响:本次升级只针对"待匹配拍摄图"(查询侧)的裁切;78272 张图库的历史特征沿用已有向量,不需要、也没有重新用新模型跑一遍建库。
modules/rectifier.py)——只用 mask,与 OCR 无关卡牌拍摄时常有透视形变(斜着拍、镜头畸变),直接按 bbox 矩形裁会带一圈背景或裁歪。矫正流程:
mask_to_corners(mask):二值 mask → cv2.findContours 取最大轮廓 → convexHull 凸包 →
多次放宽 epsilon 跑 approxPolyDP 直到拟合出 4 个角点;拟合不出兜底用 minAreaRect 的 4 角点。order_corners(pts):把 4 点按图像坐标排序为 TL, TR, BR, BL(此时只是几何排序,不代表卡牌"上下")。valid_quad(ordered):4 个内角是否接近 90°(容差 ±40°)、边长比是否不过分离奇(<3倍)——挡住 mask
抖动/角点崩坏的情况,这道护栏仍保留;has_perspective(ordered)seg_warp)已不再使用——新数据集+新模型实测"始终 warp"裁切效果更好(三方对比人工评估结论,
seg mask→4点→warp 优于 OBB 与 legacy bbox),has_perspective 函数仍在 rectifier.py 保留,仅 CardRectifier.rectify()
(给 OCR-angle 链路/cat16 等其它子系统用)继续沿用两道护栏,查询侧 CardDetector._detect_and_crop_seg_warp 只判 valid_quad。valid_quad 通过就 warp_perspective(img, corners):cv2.getPerspectiveTransform + warpPerspective
把梯形拉正为矩形;输出尺寸 _dst_size() 忠实保留角点实测的宽高(不强制卡牌标准比例 0.714)。valid_quad 不通过)→ 回退到 bbox 矩形裁切(crop_bbox);
bbox 也没有 → 直接用原图。这一步产出"梯形拉正后的矩形图",方向可能仍是横躺的(是否倒置 180° 查询侧不处理,见第五节)。
透视矫正不解决"卡牌到底哪边朝上",这个信息来自 OCR 的文档方向分类,与卡牌几何完全独立:
ocr_judge.py(本地/单机版)、ebay_sync/ebay_lang_judge.py(ebay 批量版),均跑在 paddleocr 环境PaddleOCR(use_doc_orientation_classify=True, ...) —— 必须开,实测关闭时旋转卡的 OCR
会直接乱码;开启后不仅方向判断准,识别文本也顺带修正了。result.doc_preprocessor_res.angle,取值 0/90/180/270,语义是"输入图相对正立顺时针转了多少度"语种判断逻辑说明.md),angle 是顺带产出,
单独存进 language_labels_320.json / ebay_lang.csv 里的 angle 字段,供下游正立步骤读取。当前定位:这条 OCR angle 链路服务于 cat16 三属性识别、语种判断等独立子系统(见第九节表格), 查询侧主检索链路(
CardDetector.detect_and_crop)不调用它——大角度朝向错误接受人工修正, 不为此额外付出跨环境 OCR 调用的延迟/复杂度。
rotate_upright(img, angle) —— 直接按 angle 转(简单版,不判几何,供 OCR-angle 链路用)_ANGLE_TO_CV = {
0: None,
90: cv2.ROTATE_90_COUNTERCLOCKWISE, # 输入顺时针倒90° → 逆时针转回来
180: cv2.ROTATE_180,
270: cv2.ROTATE_90_CLOCKWISE, # 输入逆时针倒90° → 顺时针转回来
}
angle 是几度就按对应方向转,没有其它判断。
rotate_to_portrait(img, angle=0) —— OCR-angle 链路用的保守版(几何优先,供 cat16/CardRectifier 用)h, w = img.shape[:2]
if w <= h: # 已经是竖向 → 不转
return img
a = int(angle or 0) % 360
if a == 90: return cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
if a == 270: return cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
return img # angle=0/180 时,横向图也不转——方向不明时不赌
设计原因(代码注释里有实测数据支撑):
angle_geometry_disagree(mask, angle) —— 一致性校验(用于人工复核分支,OCR-angle 链路专用)比较"mask 几何朝向"(W>H 视为横放)与"OCR angle 朝向"(angle∈{90,270} 视为横放)是否矛盾:
ensure_portrait(img) —— ★查询侧默认路径实际使用的版本def ensure_portrait(img):
"""保证竖向:宽>高则逆时针转90°(仅当无 angle 信息或兜底时生效)。"""
h, w = img.shape[:2]
if w > h:
return cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
return img
纯几何判断,不依赖 OCR,不跨环境:宽>高就转 90° 到竖向,不判断是否倒置 180°。
CardDetector._detect_and_crop_seg_warp()(查询侧主路径)在 warp/bbox 裁切后统一调这个函数,
链路里没有 ③OCR-angle 步骤,延迟只在 pytorch 单环境内,不跨进程调用 PaddleOCR。
CardDetector.detect_and_crop()(crop_mode=seg_warp,默认)box, mask, orig_rgb = self.detect_box_mask(img) # ① yolo26s-seg
out = None
if mask is not None:
corners = mask_to_corners(mask)
if corners is not None:
ordered = order_corners(corners)
if valid_quad(ordered): # ② 只判 valid_quad,始终 warp
out = warp_perspective(orig_rgb, corners)
if out is None and box is not None:
out = crop_bbox(orig_rgb, box) # 退回 bbox
if out is None:
out = orig_rgb
out = ensure_portrait(out) # ④' 纯几何竖放,无 OCR angle
CardRectifier.rectify()(OCR-angle 链路,供 cat16 等其它子系统用,未改动)class CardRectifier:
def rectify(self, img, mask=None, box=None, angle=0):
out = None
if self.use_perspective:
corners = mask_to_corners(mask) if mask is not None else None
if corners is not None:
ordered = order_corners(corners)
if valid_quad(ordered) and has_perspective(ordered): # 两道护栏都要过
out = warp_perspective(img, corners)
if out is None and box is not None:
out = crop_bbox(img, box)
if out is None:
out = img
out = rotate_to_portrait(out, angle) # 用 OCR angle 正立
return out
调用方准备好 img(原图) + mask/box(来自 CardDetector.detect_box_mask) + angle(来自 PaddleOCR),
一次调用拿到"正立竖向的卡牌图"。这条路径的模型底座已同步升级为 card_seg_pn_v2(因为 detect_box_mask
和 CardDetector 共用一套 model_path/TRT 开关),只是 rectify 的护栏逻辑保持不变,未采用"始终 warp"。
yolo_crop.py + ocr_judge.py)只做矩形裁切(boxes.xyxy 直接切图),不做透视矫正,因为这一路的目的只是让 OCR 输入更干净
(去掉背景/评级壳文字),不追求几何精度;angle 在这一路只是随手记录,不会拿回来对裁切图做旋转。
底座模型同样已随 config.YOLO_MODEL_PATH 切到 card_seg_pn_v2。
scripts/cascade_match.py / serve_card_match.py / serve_recognition_api.py)detector = CardDetector(config.YOLO_MODEL_PATH) # 默认 seg_warp + card_seg_pn_v2(TRT 可选)
crop = detector.detect_and_crop(img) # 见 6.1,内部已含 warp + ensure_portrait
无需手动传 angle;查询侧不做 OCR 转正,大角度朝向错误接受人工/后续复核修正。
沿用 CardRectifier.rectify(img, mask, box, angle)(6.2),逻辑未变,只是底层 CardDetector 的检测模型
已统一升级(因为共用 config.CARD_SEG_MODEL_PATH),检测/mask 质量随之提升,但护栏与转正策略不变。
| 参数 | 值 | 位置 |
|---|---|---|
| 卡牌区域检测模型(现役) | card_seg_pn_v2(yolo26s-seg,ultralytics/runs/segment/card_seg_pn_v2/weights/best.pt) |
config.CARD_SEG_MODEL_PATH |
| TensorRT engine | 同目录 best.engine(73 已导出 FP16) |
环境变量 CARD_SEG_USE_TRT=1 |
| 卡牌区域检测模型(旧,仅回退) | yolov11n_card_seg01.onnx(YOLOv11 分割) |
config.YOLO_MODEL_PATH_LEGACY,CARD_CROP_MODE=bbox_legacy 时生效 |
| 裁切策略开关 | seg_warp(默认,mask→4点→始终warp→ensure_portrait) / bbox_legacy(仅矩形裁切) |
config.CARD_CROP_MODE / 环境变量 CARD_CROP_MODE |
| 置信度阈值 | 0.25 |
config.YOLO_CONF_THRESHOLD |
| 输入尺寸 | 640 |
config.YOLO_IMG_SIZE |
| 取框规则 | 面积最大的一个检出实例 | yolo_detector.py: np.argmax(areas) |
| 角点合理性容差(查询侧唯一护栏) | 内角偏差 <40°、边长比 <3 |
rectifier.py: valid_quad |
CardRectifier.rectify OCR-angle 链路仍用,查询侧已不用) |
梯形度 trap>0.10 或 最大偏角 maxdev>3.5° |
rectifier.py: has_perspective |
| 方向角来源(OCR-angle 链路专用,查询侧不接入) | PaddleOCR doc_preprocessor_res.angle,0/90/180/270 |
ocr_judge.py / ebay_lang_judge.py |
| 查询侧正立策略 | 纯几何 ensure_portrait:w>h 则转90°,不判180°倒置 |
rectifier.py: ensure_portrait |
OCR-angle 链路正立策略(CardRectifier 用) |
仅横向图 + angle∈{90,270} 才转;angle=0/180 或已竖向不转 | rectifier.py: rotate_to_portrait |
| 维度 | 卡牌区域(本文档) | 评级卡区域(评级公司标签条) |
|---|---|---|
| 模块 | modules/yolo_detector.py (CardDetector) |
modules/grading_detector.py (GradingDetector) |
| 模型 | card_seg_v2/best.pt(TRT engine 可选),yolo26s 分割 |
card_v1/best.pt,YOLO26 检测,11 类 |
| 检测对象 | 整张卡面(宝可梦卡/球星卡本体) | 评级封装壳上印的公司标签条(PSA/BGS/CGC/SGC 及子类) |
| 必须在什么图上检测 | 原图(检出后即裁掉背景) | 必须在原图上检测——卡牌检测裁完会把评级壳裁掉,标签条随之丢失,裁切图上几乎检不到 |
| 后续处理 | mask→透视矫正(始终warp)→几何正立(ensure_portrait) → 送 DINOv2 检索;或 mask→护栏式warp→(OCR定角度)→旋转正立 → 送 cat16 等 | 裁出标签条→送 PaddleOCR 识别评级公司/分数/serial_no,不参与 DINOv2 特征提取 |
| 对应子系统 | 子系统 A(卡牌检索匹配)+ C(语种/方向前置,仅部分子系统使用) | 子系统 B(评级卡识别) |
两者是并行、互不依赖的两次独立 YOLO 推理,分别服务两条产出线(card_id 匹配 vs 评级信息识别), 本文档只覆盖前者。
若需回退到升级前的行为(旧 yolov11n_card_seg01.onnx + 仅矩形 bbox 裁切):
export CARD_CROP_MODE=bbox_legacy
或在调用处显式传参:
CardDetector(config.YOLO_MODEL_PATH_LEGACY, crop_mode="bbox_legacy")
此时行为等价于旧版第二节 detect_and_crop 的矩形裁切逻辑,不使用 mask/warp/ensure_portrait。
在 618 张真实生产查询图上,对比升级前后:
| 指标 | 旧(yolov11n bbox,CPU ONNX) | 新(card_seg_v2 TRT + seg_warp) |
|---|---|---|
| 检测耗时 | 728.8 ms/张 | 49.3 ms/张(约 15×) |
| fusion 均值 | 0.4316 | 0.4524(+4.8%) |
| fusion 中位 | 0.4374 | 0.4461(+2.0%) |
| 高置信 ≥0.6 张数 | 0/618 | 25/618 |
| Top-1 card_id 更高 fusion | 253 张 | 364 张 |
结论:新方案在速度和检索质量上均全面优于旧方案,已作为查询侧默认路径上线(8000/8010 服务)。