卡牌区域检测裁切与旋转正立流程.md 18 KB

卡牌区域检测 + 透视矫正 + 旋转正立 流程详解

⚠️ 范围声明:本文档只讲卡牌本体区域(整张宝可梦/球星卡卡面)的检测裁切与正立矫正, 对应 modules/yolo_detector.pyCardDetector)。 不是评级封装壳上的评级公司标签条检测(那部分见 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_cropCARD_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_v2ultralytics/runs/segment/card_seg_pn_v2/weights/best.pt),task="segment"(yolo26s 分割模型,不是纯 detect)
    • TensorRT:CARD_SEG_USE_TRT=1 时优先加载同目录同名 best.engine(73 服务器已导出 FP16,检测延迟 728ms→49ms,约 15×),加载失败自动回退 .pt
    • 训练数据:176 张旧标注 + 1897 张新标注(宝可梦为主,标签点数不强制 4 点,按原始描边多边形训练)
  • 关键参数config.py):conf=0.25imgsz=640
  • 输入:整张原图(交易图/eBay图/测试图),可能带背景、桌面、手、评级壳边框等干扰
  • 取框逻辑:一张图可能检出多个候选框,只取面积最大的一个np.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 矩形裁会带一圈背景或裁歪。矫正流程:

  1. mask_to_corners(mask):二值 mask → cv2.findContours 取最大轮廓 → convexHull 凸包 → 多次放宽 epsilonapproxPolyDP 直到拟合出 4 个角点;拟合不出兜底用 minAreaRect 的 4 角点。
  2. order_corners(pts):把 4 点按图像坐标排序为 TL, TR, BR, BL(此时只是几何排序,不代表卡牌"上下")。
  3. 护栏(查询侧 vs 旧版差异)
    • valid_quad(ordered):4 个内角是否接近 90°(容差 ±40°)、边长比是否不过分离奇(<3倍)——挡住 mask 抖动/角点崩坏的情况,这道护栏仍保留
    • has_perspective(ordered):旧版第二道护栏("梯形度/最大偏角超阈值才算值得矫正,否则跳过 warp") 查询侧默认路径(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
  4. valid_quad 通过就 warp_perspective(img, corners)cv2.getPerspectiveTransform + warpPerspective 把梯形拉正为矩形;输出尺寸 _dst_size() 忠实保留角点实测的宽高(不强制卡牌标准比例 0.714)。
  5. 任一步失败(mask 为空 / 角点拟合失败 / valid_quad 不通过)→ 回退到 bbox 矩形裁切crop_bbox); bbox 也没有 → 直接用原图。

这一步产出"梯形拉正后的矩形图",方向可能仍是横躺的(是否倒置 180° 查询侧不处理,见第五节)。


四、③方向角来源(PaddleOCR,另一环境,独立脚本)——查询侧默认不接入

透视矫正不解决"卡牌到底哪边朝上",这个信息来自 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——原图上的 eBay 页面边框、评级壳文字、 卖家水印都会干扰文字方向判断和语种统计。

当前定位:这条 OCR angle 链路服务于 cat16 三属性识别、语种判断等独立子系统(见第九节表格), 查询侧主检索链路(CardDetector.detect_and_crop)不调用它——大角度朝向错误接受人工修正, 不为此额外付出跨环境 OCR 调用的延迟/复杂度。


五、④正立处理——查询侧 vs 旧版 OCR-angle 版本

5.1 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 是几度就按对应方向转,没有其它判断。

5.2 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 判成 180 但其实卡是端正的""angle 判成 90 但实际已经竖向"这类 OCR 误判场景(实测 case:tx38/tx65)。
  • 横向图只信 angle∈{90,270}:因为只有 90/270 明确指出了"往哪边转";angle=0 或 180 时横向图保持原样 不转——实测 case tx86(angle=0 但图是横向)如果盲目转会转错(相似度从 0.86 掉到 0.45), 不转反而是对的。也就是"方向不确定时不赌,把决定权交给下游 DINOv2"。

5.3 angle_geometry_disagree(mask, angle) —— 一致性校验(用于人工复核分支,OCR-angle 链路专用)

比较"mask 几何朝向"(W>H 视为横放)与"OCR angle 朝向"(angle∈{90,270} 视为横放)是否矛盾:

  • 一致 → 可以放心自动转正(如 tx2);
  • 不一致 → 说明模型对"卡牌到底正不正"没把握,应该走人工判断分支,而不是硬转(如 tx86、tx38/65)。

5.4 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。


六、统一入口对照

6.1 查询侧主路径 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

6.2 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_maskCardDetector 共用一套 model_path/TRT 开关),只是 rectify 的护栏逻辑保持不变,未采用"始终 warp"。


七、生产链路里的落地方式

7.1 语种判断(yolo_crop.py + ocr_judge.py

只做矩形裁切boxes.xyxy 直接切图),不做透视矫正,因为这一路的目的只是让 OCR 输入更干净 (去掉背景/评级壳文字),不追求几何精度;angle 在这一路只是随手记录,不会拿回来对裁切图做旋转。 底座模型同样已随 config.YOLO_MODEL_PATH 切到 card_seg_pn_v2

7.2 卡牌检索匹配查询侧(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 转正,大角度朝向错误接受人工/后续复核修正。

7.3 cat16 三属性识别 / 评级卡等仍需 OCR-angle 的子系统

沿用 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_LEGACYCARD_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.angle0/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.ptYOLO26 检测,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。


十一、效果对比(A/B 实测,2026-08)

在 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 服务)。