159 lines
4.8 KiB
Python
159 lines
4.8 KiB
Python
|
|
"""测试拍照录入在课本词表照片上的识别成功率。"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import subprocess
|
|||
|
|
import sys
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
|||
|
|
sys.path.insert(0, str(BACKEND_DIR))
|
|||
|
|
|
|||
|
|
from services.word_scan_service import word_scan_service # noqa: E402
|
|||
|
|
|
|||
|
|
IMAGE = Path(
|
|||
|
|
"/Users/john/.cursor/projects/Users-john-PycharmProjects-wordloop/assets/"
|
|||
|
|
"bf2e15b4e1a36836efb4e9a72b200d8e-f357a710-6d8d-4f5b-a52c-0bea4420aada.png"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
GROUND_TRUTH: list[tuple[str, str]] = [
|
|||
|
|
("feed the cows", "喂奶牛"),
|
|||
|
|
("see a film", "看电影"),
|
|||
|
|
("visit Grandma and Grandpa", "看望爷爷奶奶"),
|
|||
|
|
("have a good time", "过得愉快"),
|
|||
|
|
("do the housework", "做家务"),
|
|||
|
|
("tidy my room", "整理我的房间"),
|
|||
|
|
("make the bed", "铺床"),
|
|||
|
|
("dish", "碟;盘"),
|
|||
|
|
("happy", "高兴的"),
|
|||
|
|
("excited", "激动的;兴奋的"),
|
|||
|
|
("upset", "难过的"),
|
|||
|
|
("angry", "愤怒的;生气的"),
|
|||
|
|
("play music", "放音乐"),
|
|||
|
|
("sing and dance", "唱歌跳舞"),
|
|||
|
|
("talk with the baby", "和婴儿说话"),
|
|||
|
|
("play a game", "玩游戏"),
|
|||
|
|
("share", "分享"),
|
|||
|
|
("have a go", "尝试"),
|
|||
|
|
("nice", "令人愉快的;吸引人的"),
|
|||
|
|
("cook", "厨师"),
|
|||
|
|
("great", "伟大的"),
|
|||
|
|
("teacher", "教师"),
|
|||
|
|
("tree", "树"),
|
|||
|
|
("leaf (pl. leaves)", "叶子"),
|
|||
|
|
("flower", "花"),
|
|||
|
|
("grass", "草"),
|
|||
|
|
("duck", "鸭"),
|
|||
|
|
("pond", "池塘;水池"),
|
|||
|
|
("frog", "青蛙"),
|
|||
|
|
("lake", "湖"),
|
|||
|
|
("fish", "鱼"),
|
|||
|
|
("river", "河;江"),
|
|||
|
|
("shark", "鲨鱼"),
|
|||
|
|
("sea", "海;海洋"),
|
|||
|
|
("wash my hands", "(我)洗手"),
|
|||
|
|
("before eating", "吃东西前"),
|
|||
|
|
("after using the toilet", "上厕所后"),
|
|||
|
|
("dirty", "脏的"),
|
|||
|
|
("clean", "干净的"),
|
|||
|
|
("drink some milk", "喝一些牛奶"),
|
|||
|
|
("say good night", "道晚安"),
|
|||
|
|
("sleep", "睡觉;入睡"),
|
|||
|
|
("read a story", "读故事"),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_en(text: str) -> str:
|
|||
|
|
return re.sub(r"\s+", " ", text.strip().lower())
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_zh(text: str) -> str:
|
|||
|
|
return re.sub(r"\s+", "", text.strip())
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fuzzy_zh_match(pred: str, truth: str) -> bool:
|
|||
|
|
p = normalize_zh(pred)
|
|||
|
|
t = normalize_zh(truth)
|
|||
|
|
if not p or not t:
|
|||
|
|
return False
|
|||
|
|
if p == t:
|
|||
|
|
return True
|
|||
|
|
# 允许 OCR/截断导致的中文释义部分匹配(>=70% 字符重合)
|
|||
|
|
common = sum(1 for ch in t if ch in p)
|
|||
|
|
return common / max(len(t), 1) >= 0.7
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fuzzy_en_match(pred: str, truth: str) -> bool:
|
|||
|
|
p = normalize_en(pred)
|
|||
|
|
t = normalize_en(truth)
|
|||
|
|
if p == t:
|
|||
|
|
return True
|
|||
|
|
# 短语轻微 OCR 错误:去掉空格后前缀匹配
|
|||
|
|
p2 = re.sub(r"[^a-z()\.]", "", p)
|
|||
|
|
t2 = re.sub(r"[^a-z()\.]", "", t)
|
|||
|
|
if not p2 or not t2:
|
|||
|
|
return False
|
|||
|
|
return t2 in p2 or p2 in t2 or p2.startswith(t2[: max(4, len(t2) - 2)])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_tesseract(image_path: Path, psm: str = "6") -> str:
|
|||
|
|
result = subprocess.run(
|
|||
|
|
["tesseract", str(image_path), "stdout", "-l", "chi_sim+eng", "--psm", psm],
|
|||
|
|
capture_output=True,
|
|||
|
|
text=True,
|
|||
|
|
check=False,
|
|||
|
|
)
|
|||
|
|
return result.stdout or ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def evaluate(items: list[dict[str, str]], label: str) -> dict:
|
|||
|
|
preds = [(i["source_text"], i["target_text"]) for i in items]
|
|||
|
|
matched = []
|
|||
|
|
missed = []
|
|||
|
|
for en, zh in GROUND_TRUTH:
|
|||
|
|
hit = any(fuzzy_en_match(p_en, en) and fuzzy_zh_match(p_zh, zh) for p_en, p_zh in preds)
|
|||
|
|
if hit:
|
|||
|
|
matched.append((en, zh))
|
|||
|
|
else:
|
|||
|
|
missed.append((en, zh))
|
|||
|
|
|
|||
|
|
recall = len(matched) / len(GROUND_TRUTH)
|
|||
|
|
precision = len(matched) / max(len(preds), 1)
|
|||
|
|
print(f"\n=== {label} ===")
|
|||
|
|
print(f"标准词条: {len(GROUND_TRUTH)}")
|
|||
|
|
print(f"识别词条: {len(preds)}")
|
|||
|
|
print(f"命中: {len(matched)} 漏识: {len(missed)}")
|
|||
|
|
print(f"召回率(成功率): {recall * 100:.1f}%")
|
|||
|
|
print(f"精确率: {precision * 100:.1f}%")
|
|||
|
|
if missed:
|
|||
|
|
print("漏识样例:", ", ".join(f"{en}" for en, _ in missed[:12]))
|
|||
|
|
if len(missed) > 12:
|
|||
|
|
print(f"... 另有 {len(missed) - 12} 条")
|
|||
|
|
return {"recall": recall, "matched": len(matched), "pred_count": len(preds), "missed": missed}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> None:
|
|||
|
|
if not IMAGE.exists():
|
|||
|
|
print(f"图片不存在: {IMAGE}")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
ocr_raw = run_tesseract(IMAGE)
|
|||
|
|
print("--- OCR 原文(前 1200 字)---")
|
|||
|
|
print(ocr_raw[:1200])
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
items = word_scan_service.parse_scan_text(ocr_raw)
|
|||
|
|
result = evaluate(items, "当前流水线: 原始 OCR + 解析")
|
|||
|
|
except Exception as exc:
|
|||
|
|
print(f"\n解析失败: {exc}")
|
|||
|
|
result = {"recall": 0.0}
|
|||
|
|
|
|||
|
|
passed = result["recall"] >= 0.9
|
|||
|
|
print(f"\n结论: {'达到' if passed else '未达到'} 90% 成功率目标(召回 {result['recall']*100:.1f}%)")
|
|||
|
|
sys.exit(0 if passed else 1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|