| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- # -*- coding: utf-8 -*-
- """
- OCR 准确率核对工具 — 用截图当标准答案,核对 Excel 采集数据
- =============================================================================
- 用法:
- python audit_ocr.py # 默认核对 采集数据/ 最新一天的 xlsx
- python audit_ocr.py 采集数据/20260909.xlsx # 指定文件
- python audit_ocr.py xxx.xlsx --max 10 # 最多核对10张截图(PP-OCR较慢,约30~60秒/张)
- 核对口径:
- A. 店名字准率: Excel里 列表页OCR店名 vs 详情页AI店名(独立来源互为答案)
- B. 现场覆盖率: 来源截图列 追溯到原图 → 本地PP-OCR重新识别 → 图上枚举出的完整卡片
- 是否都进了 Excel(漏采检测);Excel行记录的价格是否真的出现在原图上(追溯检测)
- 说明:
- B 口径的图上枚举用本地PP-OCR+几何规则(价格块+下方店铺块+上方标题块),与GLM无关,
- 所以能发现 GLM 漏卡/错配;字级别的标题准确率仍以人工抽查为准。
- """
- import json
- import random
- import re
- import sys
- from pathlib import Path
- ROOT = Path(__file__).parent
- sys.path.insert(0, str(ROOT))
- MAX_GAP = 216 # 价格→店铺窗口(与采集端校验一致,按1220x2712标定)
- TITLE_WIN = 330 # 标题窗口(价格上方)
- PROMO_RE = re.compile(r"起送|满\d|配送|发货|日达|红包|已售|月售|预估|减\d|处方|OTC|同款|低价|原研|新客|超值")
- def norm(s):
- return re.sub(r"\s+", "", str(s or "")).lower()
- def cy(b):
- return (b["box"][1] + b["box"][3]) // 2
- def cx(b):
- return (b["box"][0] + b["box"][2]) // 2
- def price_num(p):
- m = re.search(r"[\d.]+", str(p or ""))
- return m.group(0).rstrip(".") if m else ""
- def edit_dist(a, b):
- if abs(len(a) - len(b)) > 2:
- return 99
- prev = list(range(len(b) + 1))
- for i, ca in enumerate(a, 1):
- cur = [i]
- for j, cb in enumerate(b, 1):
- cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (ca != cb)))
- prev = cur
- return prev[-1]
- def shop_match(a, b):
- na, nb = norm(a), norm(b)
- if len(na) < 3 or len(nb) < 3:
- return False
- if na in nb or nb in na:
- return True
- return edit_dist(na, nb) <= 1
- def load_excel(path):
- from openpyxl import load_workbook
- wb = load_workbook(path, read_only=True)
- rows = []
- for ws in wb.worksheets:
- for r in list(ws.iter_rows(values_only=True))[1:]:
- t, p, s1, s2, snap, link, src = (list(r) + [None] * 7)[:7]
- rows.append({"sheet": ws.title, "title": str(t or ""), "price": str(p or ""),
- "shop": str(s1 or ""), "shop_ai": str(s2 or ""), "src": str(src or "")})
- return rows
- def shop_accuracy(rows):
- """口径A: 列表OCR店名 vs 详情页AI店名"""
- comparable = [r for r in rows if norm(r["shop"]) and norm(r["shop_ai"])]
- exact = sum(1 for r in comparable if norm(r["shop"]) == norm(r["shop_ai"]))
- fuzzy = [r for r in comparable
- if norm(r["shop"]) != norm(r["shop_ai"]) and shop_match(r["shop"], r["shop_ai"])]
- diff = [r for r in comparable
- if norm(r["shop"]) != norm(r["shop_ai"]) and not shop_match(r["shop"], r["shop_ai"])]
- print("=" * 78)
- print(f"口径A 店名字准率(可比 {len(comparable)}/{len(rows)} 行)")
- print(f" 完全一致 {exact:>4} {exact/len(comparable)*100 if comparable else 0:.1f}%")
- print(f" 同店近似 {len(fuzzy):>4} (错1~2字/前后缀差)")
- print(f" 不一致 {len(diff):>4} ← 逐条人工判断是'读错字'还是'配错卡'")
- for r in diff:
- print(f" [{r['sheet']}] {r['title'][:30]} {r['price']}")
- print(f" 列表OCR: {r['shop'][:24]} ↔ 详情AI: {r['shop_ai'][:24]} 来源:{r['src']}")
- n = len(comparable)
- if n:
- print(f" → 店名识别成功率 ≈ {(exact + len(fuzzy)) / n * 100:.1f}%")
- return diff
- def split_right_col(blocks):
- """定位右列分界x(同采集端:¥块左边缘中位数-15,图片区在左、文字区在右)"""
- price_x = sorted(b["box"][0] for b in blocks if "¥" in b["text"] or "¥" in b["text"])
- if len(price_x) < 3:
- return 0
- return max(price_x[len(price_x) // 2] - 15, 0)
- def enumerate_cards(blocks):
- """几何规则枚举图上的完整卡片: 价格块 + 下方店铺块 + 上方标题块(与GLM无关的独立读图)
- 先裁掉左列商品图(包装文字会污染店铺/标题),价格块必须以¥开头(排除'起送¥0'类促销行)"""
- split = split_right_col(blocks)
- blocks = [b for b in blocks if cx(b) >= split] if split else blocks
- cards = []
- used_shop = []
- prices = [b for b in blocks if norm(b["text"]).startswith(("¥", "¥"))
- and re.search(r"¥[\d.]+|¥[\d.]+", b["text"])]
- prices.sort(key=cy)
- for pb in prices:
- py = cy(pb)
- pn = price_num(pb["text"])
- if not pn:
- continue
- # 店铺: 价格下方最近的有效块
- shop = None
- for b in sorted(blocks, key=cy):
- y = cy(b)
- if not (py + 5 < y < py + MAX_GAP):
- continue
- t = norm(b["text"])
- if len(t) < 4 or "¥" in b["text"] or "¥" in b["text"] or PROMO_RE.search(t):
- continue
- if any(abs(y - u) < 25 for u in used_shop):
- continue
- shop = b
- break
- if shop is None:
- continue
- used_shop.append(cy(shop))
- # 标题: 价格上方最长有效块
- title, tlen = "", 0
- for b in blocks:
- y = cy(b)
- if not (py - TITLE_WIN < y < py - 5):
- continue
- t = norm(b["text"])
- if len(t) < 6 or "¥" in b["text"] or "¥" in b["text"] or PROMO_RE.search(t):
- continue
- if len(t) > tlen:
- title, tlen = b["text"], len(t)
- cards.append({"price": pn, "shop": re.split(r"[((]", re.sub(r"\s+", "", shop["text"]))[0],
- "title": re.sub(r"\s+", "", title), "py": py})
- return cards
- def field_audit(rows, max_images):
- """口径B: 来源截图追溯 + 图上卡片漏采检测"""
- src_rows = [r for r in rows if r["src"]]
- if not src_rows:
- print("\n口径B 跳过:该Excel没有「来源截图」列数据(列是今天新加的,明天采集后生效)")
- return
- try:
- from ai_helper_vision1 import VisionParser1
- except Exception as e:
- print(f"\n口径B 跳过:PP-OCR加载失败({e})")
- return
- # 来源 → 截图文件(取 base 文件;若被覆盖可用同名时间戳副本里最新的一张)
- shots = {}
- for r in src_rows:
- dev, batch = r["src"].split("/step3_b", 1)
- base = ROOT / "screenshots" / dev / "step3" / f"step3_b{batch}.png"
- if base.exists():
- shots.setdefault(str(base), (dev, batch))
- else: # 找时间戳副本
- cands = sorted((ROOT / "screenshots" / dev / "step3").glob(f"step3_b{batch}_*.png"),
- key=lambda p: p.stat().st_mtime)
- if cands:
- shots.setdefault(str(cands[-1]), (dev, batch))
- parser = VisionParser1()
- keys = list(shots.keys())
- if max_images and len(keys) > max_images:
- keys = random.sample(keys, max_images) # 跨设备随机抽样,避免样本偏斜
- total_cards = matched = 0
- missing_report, trace_fail = [], []
- audited_srcs = {shots[k] for k in keys}
- print("\n" + "=" * 78)
- print(f"口径B 现场核对({len(keys)}/{len(shots)} 张截图,PP-OCR本地识别,每张约30~60秒)")
- for k in keys:
- dev, batch = shots[k]
- try:
- blocks = parser._ppocr_blocks(k, 0) # 整图识别,由几何规则自行分列
- except Exception as e:
- print(f" [跳过] {dev}/b{batch}: OCR失败 {e}")
- continue
- cards = enumerate_cards(blocks)
- # 图上卡片 → Excel 全量匹配(价格+店名)
- hit = 0
- for c in cards:
- rows_p = [r for r in rows if price_num(r["price"]) == c["price"]]
- if any(shop_match(c["shop"], r["shop"]) for r in rows_p):
- hit += 1
- else:
- missing_report.append((dev, batch, c))
- total_cards += len(cards)
- matched += hit
- print(f" {dev}/b{batch}: 图上{len(cards)}卡, 入Excel {hit}")
- # 追溯: 来源=这张图的Excel行,其价格必须出现在图上
- for r in src_rows:
- if (r["src"].split("/step3_b")[0], r["src"].split("/step3_b")[1]) == (dev, batch):
- pn = price_num(r["price"])
- if pn and not any(pn in norm(b["text"]) for b in blocks if "¥" in b["text"] or "¥" in b["text"]):
- trace_fail.append(r)
- print("-" * 78)
- if total_cards:
- print(f"图上卡片共 {total_cards} 张, 已入Excel {matched} 张 → 现场覆盖率 {matched/total_cards*100:.1f}%")
- for dev, batch, c in missing_report:
- print(f" [图上有但Excel没有] {dev}/b{batch} ¥{c['price']} {c['shop'][:14]} {c['title'][:26]}")
- if trace_fail:
- print(f"追溯异常 {len(trace_fail)} 行(Excel记的价格在来源截图上找不到):")
- for r in trace_fail[:10]:
- print(f" [{r['sheet']}] {r['title'][:26]} {r['price']} 来源:{r['src']}")
- if not trace_fail:
- print("追溯检测: 全部Excel行的价格都能在其来源截图上找到 ✓")
- def main():
- args = [a for a in sys.argv[1:] if not a.startswith("--")]
- max_images = None
- if "--max" in sys.argv:
- try:
- max_images = int(sys.argv[sys.argv.index("--max") + 1])
- except (IndexError, ValueError):
- pass
- if args:
- xlsx = ROOT / args[0]
- else:
- cands = sorted((ROOT / "采集数据").glob("*.xlsx"), key=lambda p: p.stat().st_mtime)
- if not cands:
- print("采集数据/ 下没有 xlsx"); sys.exit(1)
- xlsx = cands[-1]
- if not xlsx.exists():
- print(f"文件不存在: {xlsx}"); sys.exit(1)
- print(f"核对文件: {xlsx}\n")
- rows = load_excel(xlsx)
- print(f"共 {len(rows)} 行, {len(set(r['sheet'] for r in rows))} 个药品\n")
- shop_accuracy(rows)
- field_audit(rows, max_images)
- print("\n提示: 口径B依赖「来源截图」列(今天新加),旧数据该列为空会自动跳过。")
- if __name__ == "__main__":
- main()
|