# 卡牌语种判断逻辑说明文档 > **作用**:从 eBay 交易卡牌图片中,识别出该卡属于哪种语言版本 > > **目标语种(4 类,对齐 `cards_master_v2` 的 `language` 字段)**: > - `tcg us`(英文卡) > - `tcg jp`(日文卡) > - `简中`(简体中文卡) > - `繁中`(繁体中文卡) --- ## 一、整体流程 ``` ┌──────────────────────────────────────────────────────────┐ │ 第一步:卡牌区域裁剪(pytorch 环境) │ │ 原图 → card_seg_v2(yolo26s-seg) → 卡牌区域裁剪图 │ │ 脚本: scripts/yolo_crop.py(即 yolo_crop.py) │ │ 产物: data/query_crops/*.jpg │ └───────────────────────────┬────────────────--------------┘ │ 中间切换 conda 环境 ▼ ┌──────────────────────────────────────────────────────────┐ │ 第二步:OCR + 语种判定(paddleocr 环境) │ │ 裁剪图 → PaddleOCR 3.2.0 识别 → 统计字符 → 判定语种 │ │ 脚本: ocr_judge.py │ │ 产物: data/language_labels_320.json │ └──────────────────────────────────────────────────────────┘ ``` > **为什么分两步、还切环境?** > YOLO 依赖 `ultralytics`(在 `pytorch` 环境),OCR 依赖 `paddleocr==3.2.0 + paddlepaddle==3.2.0`(在 `paddleocr` 环境),两套包互不兼容,无法共存于同一环境,故拆成两个独立步骤。 --- ## 二、核心代码位置 语种判定的全部逻辑在 **`ocr_judge.py`** 中: | 功能 | 代码位置 | 说明 | |------|----------|------| | 字符统计 | `ocr_judge.py:61-63` | 统计 kana / cjk / latin 数量 | | 判定主逻辑 | `ocr_judge.py:55-73`(`judge()` 函数) | 按优先级返回语种 | | 简繁判定 | `ocr_judge.py:67-69` | 调用 hanzidentifier | | OCR 文本提取 | `ocr_judge.py:36-47`(`ocr_full_text()`) | PaddleOCR 识别 | | 文本截断 500 字符 | `ocr_judge.py:92` | `text = full_text[:500]` | --- ## 三、字符统计(判定依据) > 代码:`ocr_judge.py:61-63` ```python kana = sum(1 for ch in text if 0x3040 <= ord(ch) < 0x3100) cjk = sum(1 for ch in text if 0x4E00 <= ord(ch) < 0x9FFF) latin = sum(1 for ch in text if ch.isascii() and ch.isalpha()) ``` | 统计量 | 含义 | Unicode 范围 / 判定方式 | |--------|------|------------------------| | **`kana`** | 假名数量(日文) | `0x3040 ~ 0x3100`,含平假名(ぁ~ゎ)+片假名(ァ~ヶ) | | **`cjk`** | CJK 汉字数量(中日韩) | `0x4E00 ~ 0x9FFF`,统一表意文字区(一~龥) | | **`latin`** | 拉丁字母数量 | ASCII 字母 `A-Z a-z` | > **`s` / `t`**(仅中文卡才计算):见下文「简繁判定」。 ### 字符范围原理(Unicode 区段) - **假名区 `0x3040~0x30FF`**:日文独有,平假名和片假名在这个区段集中存放。**出现假名几乎 100% 是日文卡**(中文卡、英文卡不可能有假名)。 - **CJK 统一表意文字 `0x4E00~0x9FFF`**:中、日、韩共用的汉字都挤在这一个区段。问题在于**简体字和繁体字共用同一个码位**(字形不同但 Unicode 相同),所以光靠码位无法区分简繁,必须靠 `hanzidentifier` 比对字形。 - **ASCII 字母**:英文字母,英文卡必然以此为主。 --- ## 四、判定主逻辑(按优先级,短路返回) > 代码:`ocr_judge.py:55-73` ```python def judge(text): kana = sum(...) # 假名数 cjk = sum(...) # 汉字数 latin = sum(...) # 拉丁字母数 # ① 假名 ≥ 10 → 日文(铁证) if kana >= 10: return "tcg jp", {"kana": kana, "cjk": cjk, "latin": latin} # ② 汉字比拉丁字母多 → 是中文卡,进一步判简繁 if cjk > latin: s = sum(1 for ch in text if hz.identify(ch) == 2) # 简体字数 t = sum(1 for ch in text if hz.identify(ch) == 1) # 繁体字数 script = "简中" if s >= t else "繁中" return script, {"kana": kana, "s": s, "t": t, "cjk": cjk} # ③ 拉丁字母不少于汉字 → 英文卡 if latin >= cjk: return "tcg us", {"kana": kana, "latin": latin, "cjk": cjk} # ④ 都不满足 → 无法判定 return None, {"kana": kana, "cjk": cjk, "latin": latin} ``` ### 规则一览 | 优先级 | 条件 | 结果 | 原理 | |--------|------|------|------| | ① | `kana >= 10` | **tcg jp** | 假名是日文铁证 | | ② | `cjk > latin` | **简中 / 繁中** | 汉字占主导 | | ③ | `latin >= cjk` | **tcg us** | 拉丁字母占主导 | | ④ | 否则 | **None** | 无法判定 | --- ## 五、逐条规则的设计原理 ### 规则①:假名优先(最可靠) ```python if kana >= 10: return "tcg jp", ... ``` - **为什么用假名判日文**:假名(平/片假名)是日文**独有的音节文字**,中文、英文、韩文卡牌都不会出现假名。它是最强的日文信号,**零误判**。 - **为什么阈值是 10**:日文卡上假名密集(技能名、属性说明都是假名),真日文卡轻松几十上百个假名。设 10 是为了过滤 OCR 偶发噪声(比如英文卡偶尔误识出 1~2 个假名),同时不影响真日文卡判定。 - **为什么放最前面(最高优先级)**:日文卡上也常混有汉字和少量英文(如卡牌编号、HP),如果先比 cjk/latin 可能被汉字带偏。先用假名"一票否决"最稳。 ### 规则②:汉字为主 → 判简繁 ```python if cjk > latin: s = sum(1 for ch in text if hz.identify(ch) == 2) t = sum(1 for ch in text if hz.identify(ch) == 1) script = "简中" if s >= t else "繁中" ``` - **为什么先比 `cjk > latin`**:先确认"这是一张中文卡"(汉字比英文多),再谈简繁。英文卡虽然也可能有零星汉字误识,但 `latin` 占绝对多数,不会进入这里。 - **为什么用 `hanzidentifier` 判简繁**:前面说过,简体字和繁体字在 Unicode 里**共用同一个码位**(如"龙/龍"是两个不同码位,但很多字简繁同码位只是字形不同)。`hanzidentifier` 内部维护了简繁字形对照表,能逐字判断这个字是简体写法还是繁体写法: - `hz.identify(ch) == 2` → 简体字 - `hz.identify(ch) == 1` → 繁体字 - **为什么用 `s >= t` 投票**:一张卡上汉字很多,逐字判简繁后**按数量投票**,少数服从多数。简繁字数相等时偏向简中(实际几乎不会相等)。 ### 规则③:拉丁字母为主 → 英文 ```python if latin >= cjk: return "tcg us", ... ``` - **原理**:既没有假名(不是日文),汉字也不占多数,那拉丁字母为主的卡就是英文卡。宝可梦英文卡的技能描述、HP、招式名全是英文。 --- ## 六、为什么这么设计(核心原理总结) ### 1. 用「字符特征」而非「语言模型」判定 不用 GlotLID 之类的语言分类模型,而是直接数字符,原因: - **快**:纯 Unicode 比较,无需模型推理; - **准**:假名、汉字、拉丁字母的 Unicode 区段是**确定性的**,比 OCR 噪声下跑语言模型更稳; - **可解释**:`detail` 里的 kana/cjk/latin/s/t 全是数字,判定过程完全透明,方便人工核对。 ### 2. 「铁证优先」的短路设计 判定顺序是 **日文 → 中文 → 英文**,按"信号强度"从强到弱排列。每命中一条就 `return`,不再往下走。这样: - 日文卡不会因为汉字多被误判成中文; - 中文卡不会因为混了英文被误判成英文。 ### 3. 简繁区分用「字形投票」而非「码位」 这是整套逻辑里**最巧妙也最依赖外部库**的一环。因为 Unicode 不区分简繁,必须借助 `hanzidentifier` 的字形表,再按数量投票,才能区分简中卡和繁中卡。 ### 4. 输入用 YOLO 裁剪图而非原图 OCR 前先用 YOLO 把卡牌区域裁出来,**去掉 eBay 页面边框、PSA/CGC 评级壳文字、卖家水印等干扰**,让 OCR 文本更纯净,字符统计更准确。 --- ## 七、输出格式 > 文件:`data/language_labels_320.json`,100 条 每条记录结构(键的含义见第三、四节): ```json { "1": { "language": "tcg us", // 判定结果 "detail": { // 判定依据 "kana": 0, // 假名数 "latin": 228, // 拉丁字母数 "cjk": 0 // 汉字数(中文卡还会有 s/t) }, "text": "BASICCharmander..." // OCR 文本(≤500 字符) } } ``` > **注意**:`detail` 的键不固定。日文卡只有 `kana/cjk/latin`(命中规则①即返回);中文卡才会有 `s/t`(进入规则②才算);英文卡是 `kana/latin/cjk`。 --- ## 八、最终统计结果(100 张 eBay 卡牌) | 语种 | 数量 | 占比 | |------|------|------| | tcg us(英文) | 81 | 81% | | tcg jp(日文) | 18 | 18% | | 繁中 | 1 | 1% | --- ## 九、相关文件清单 | 文件 | 路径 | 用途 | |------|------|------| | YOLO 裁剪脚本 | `yolo_crop.py` | 第一步,pytorch 环境运行 | | OCR+判定脚本 | `ocr_judge.py` | 第二步,paddleocr 环境运行 | | 裁剪图产物 | `data/query_crops/` | 100 张卡牌裁剪图 | | 判定结果 | `data/language_labels_320.json` | 最终输出 | | 卡牌检测模型 | ★`ultralytics/runs/segment/card_seg_pn_v2/weights/best.pt`(TRT: 同目录 `best.engine`;旧 `yolov11n_card_seg01.onnx` 仅 `bbox_legacy` 回退) | 卡牌检测分割 | | OCR 模型 | PaddleOCR 3.2.0 内置 PP-OCRv5 | 文本识别 | | 简繁库 | `hanzidentifier` (pip) | 简繁字形判定 | > **环境**:服务器 `192.168.77.249`,YOLO 步骤用 `pytorch` 环境,OCR 步骤用 `paddleocr` 环境(paddleocr 3.2.0 + paddlepaddle 3.2.0)。