ocr_judge.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """第二步:对 YOLO 裁剪图做 OCR + 语种判定(PaddleOCR 3.2.0)
  4. 流程: 裁剪图 → PaddleOCR 识别 → 文本截断500字符 → 语种判定
  5. 运行环境: paddleocr conda 环境
  6. 输出 language_labels_320.json,每条含:
  7. - language: 语种判断结果 (tcg us / tcg jp / 简中 / 繁中 / None)
  8. - detail: 判定依据 (kana/cjk/latin/s/t 计数)
  9. - text: OCR 识别文本 (最多500字符)
  10. - angle: 图像方向角度 0/90/180/270(供主流程做卡牌正立翻转,不影响 OCR/语种)
  11. 注: use_doc_orientation_classify=True 是必要的——实测关闭时旋转卡的 OCR 会乱码,
  12. 开启后旋转卡文本识别正确(语种判定跟着对),并顺带给出方向角度。
  13. """
  14. import os, sys, csv, hashlib, json
  15. sys.stdout.reconfigure(encoding="utf-8")
  16. sys.path.insert(0, "/home/martin/顾工交接/wzj")
  17. import config
  18. # ============ 配置 ============
  19. MAX_TEXT_LEN = 500 # OCR 文本最大字符数
  20. DATA = config.DATA_DIR
  21. def md5n(u): return hashlib.md5(u.encode()).hexdigest() + ".jpg"
  22. CROP_DIR = os.path.join(DATA, "query_crops")
  23. # ============ OCR 初始化 ============
  24. print("[OCR] 初始化 PaddleOCR 3.2.0...", flush=True)
  25. from paddleocr import PaddleOCR
  26. ocr = PaddleOCR(
  27. use_doc_orientation_classify=True,
  28. use_doc_unwarping=False,
  29. use_textline_orientation=True,
  30. )
  31. print("[OCR] 加载完成", flush=True)
  32. def ocr_full_text(img_path):
  33. """OCR 识别,返回 (完整文本, 方向角度)。
  34. 角度来自 doc_preprocessor_res.angle(0/90/180/270,输入相对正立顺时针的旋转)。"""
  35. result = ocr.predict(img_path)
  36. texts, angle = [], 0
  37. for res in result:
  38. # res 是 dict-like,用 dict 访问
  39. if isinstance(res, dict):
  40. t = res.get("rec_texts")
  41. if t:
  42. texts.extend(list(t))
  43. dpr = res.get("doc_preprocessor_res")
  44. if dpr is not None:
  45. a = dpr.get("angle") if isinstance(dpr, dict) else getattr(dpr, "angle", None)
  46. if a is not None:
  47. try:
  48. angle = int(a)
  49. except Exception:
  50. angle = a
  51. elif hasattr(res, "rec_texts") and res.rec_texts:
  52. texts.extend(list(res.rec_texts))
  53. dpr = getattr(res, "doc_preprocessor_res", None)
  54. if dpr is not None:
  55. a = getattr(dpr, "angle", None)
  56. if a is not None:
  57. try:
  58. angle = int(a)
  59. except Exception:
  60. angle = a
  61. return "".join(texts), angle
  62. # ============ 语种判定 ============
  63. import hanzidentifier as hz
  64. def judge(text):
  65. """语种判定逻辑:
  66. ① 假名>=10 → tcg jp (日文铁证)
  67. ② 中文占比>25% (cjk*3 > latin) → 是中文卡 → hanzidentifier 判简繁
  68. ③ 否则 (cjk*3 <= latin) → tcg us (英文)
  69. 卡牌上英文元素(HP/Weakness/版权/编号)天然拉高latin,故中文阈值放宽到25%。
  70. """
  71. kana = sum(1 for ch in text if 0x3040 <= ord(ch) < 0x3100)
  72. cjk = sum(1 for ch in text if 0x4E00 <= ord(ch) < 0x9FFF)
  73. latin = sum(1 for ch in text if ch.isascii() and ch.isalpha())
  74. if kana >= 10:
  75. return "tcg jp", {"kana": kana, "cjk": cjk, "latin": latin}
  76. # 中文判定阈值: 中文字符*3 > 英文字符 (中文相对英文占比>25%) → 判为中文卡
  77. if cjk * 3 > latin:
  78. s = sum(1 for ch in text if hz.identify(ch) == 2) # 简体字
  79. t = sum(1 for ch in text if hz.identify(ch) == 1) # 繁体字
  80. script = "简中" if s >= t else "繁中"
  81. return script, {"kana": kana, "s": s, "t": t, "cjk": cjk}
  82. # cjk*3 <= latin: 英文占主导 → 英文卡
  83. return "tcg us", {"kana": kana, "latin": latin, "cjk": cjk}
  84. # ============ 主循环 ============
  85. txs = list(csv.DictReader(open(os.path.join(DATA, "transactions_ebay.csv"), encoding="utf-8-sig")))
  86. print("[OCR] 处理 %d 张裁剪图(文本上限%d字符)" % (len(txs), MAX_TEXT_LEN), flush=True)
  87. print("=" * 60, flush=True)
  88. labels = {}
  89. empty_count = 0
  90. for i, tx in enumerate(txs):
  91. tid = tx["tx_id"]
  92. crop_path = os.path.join(CROP_DIR, md5n(tx["img_url"]))
  93. if not os.path.exists(crop_path):
  94. labels[tid] = {"language": None, "detail": {"err": "no_crop"}, "text": ""}
  95. continue
  96. try:
  97. full_text, angle = ocr_full_text(crop_path)
  98. # 截断到最大字符数
  99. text = full_text[:MAX_TEXT_LEN]
  100. if not text:
  101. empty_count += 1
  102. # 基于截断后的文本做语种判定
  103. lang, det = judge(text)
  104. except Exception as e:
  105. lang, det = None, {"err": str(e)[:60]}
  106. text, angle = "", 0
  107. labels[tid] = {"language": lang, "detail": det, "text": text, "angle": angle}
  108. print("[%3d] %-30s -> %-7s ang=%s %s" % (i + 1, tx["title"][:30], lang or "?", angle, det), flush=True)
  109. # ============ 保存 ============
  110. out = os.path.join(DATA, "language_labels_320.json")
  111. with open(out, "w", encoding="utf-8") as f:
  112. json.dump(labels, f, ensure_ascii=False, indent=1)
  113. from collections import Counter
  114. cnt = Counter(v["language"] for v in labels.values())
  115. print("=" * 60)
  116. print("===== 语种判断模型结果 (PaddleOCR 3.2.0 + YOLO裁剪, 文本≤%d) =====" % MAX_TEXT_LEN)
  117. for k, v in cnt.most_common():
  118. print(" %-8s %d" % (k, v))
  119. print("OCR空文本: %d" % empty_count)
  120. print("已保存: %s" % out)