语种判断逻辑说明.md 10 KB

卡牌语种判断逻辑说明文档

作用:从 eBay 交易卡牌图片中,识别出该卡属于哪种语言版本

目标语种(4 类,对齐 cards_master_v2language 字段)

  • 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-73judge() 函数) 按优先级返回语种
简繁判定 ocr_judge.py:67-69 调用 hanzidentifier
OCR 文本提取 ocr_judge.py:36-47ocr_full_text() PaddleOCR 识别
文本截断 500 字符 ocr_judge.py:92 text = full_text[:500]

三、字符统计(判定依据)

代码:ocr_judge.py:61-63

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

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 无法判定

五、逐条规则的设计原理

规则①:假名优先(最可靠)

if kana >= 10:
    return "tcg jp", ...
  • 为什么用假名判日文:假名(平/片假名)是日文独有的音节文字,中文、英文、韩文卡牌都不会出现假名。它是最强的日文信号,零误判
  • 为什么阈值是 10:日文卡上假名密集(技能名、属性说明都是假名),真日文卡轻松几十上百个假名。设 10 是为了过滤 OCR 偶发噪声(比如英文卡偶尔误识出 1~2 个假名),同时不影响真日文卡判定。
  • 为什么放最前面(最高优先级):日文卡上也常混有汉字和少量英文(如卡牌编号、HP),如果先比 cjk/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 "繁中"
  • 为什么先比 cjk > latin:先确认"这是一张中文卡"(汉字比英文多),再谈简繁。英文卡虽然也可能有零星汉字误识,但 latin 占绝对多数,不会进入这里。
  • 为什么用 hanzidentifier 判简繁:前面说过,简体字和繁体字在 Unicode 里共用同一个码位(如"龙/龍"是两个不同码位,但很多字简繁同码位只是字形不同)。hanzidentifier 内部维护了简繁字形对照表,能逐字判断这个字是简体写法还是繁体写法:
    • hz.identify(ch) == 2 → 简体字
    • hz.identify(ch) == 1 → 繁体字
  • 为什么用 s >= t 投票:一张卡上汉字很多,逐字判简繁后按数量投票,少数服从多数。简繁字数相等时偏向简中(实际几乎不会相等)。

规则③:拉丁字母为主 → 英文

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 条

每条记录结构(键的含义见第三、四节):

{
  "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.onnxbbox_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)。