# -*- coding: utf-8 -*-
"""
pokemon_report.py - Pokemon 评级卡识别 技术汇报 HTML (学习 report.html 风格)
主题: 评级卡区域识别 (best.pt 检测评级公司 + PaddleOCR 识别分数/编号)
不含卡牌匹配(DINOv2)展示, 仅评级。
运行: pytorch/任意有 cv2 环境, 在服务器跑(图在服务器)
"""
import os, sys, csv, json, hashlib, random, base64
from collections import Counter
sys.stdout.reconfigure(encoding="utf-8")
OUT_DIR = "/home/martin/顾工交接/wzj/images_pokemon_out"
random.seed(42)
def b64(img, h=240, q=78):
import cv2
if img is None:
return ""
if img.shape[0] != h:
sc = h / img.shape[0]
img = cv2.resize(img, (max(1, int(img.shape[1] * sc)), h))
ok, buf = cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, q])
return "data:image/jpeg;base64," + base64.b64encode(buf.tobytes()).decode() if ok else ""
def load(p):
import cv2
return cv2.imread(p) if p and os.path.exists(p) else None
print("[1] 读数据 ...", flush=True)
rows = list(csv.DictReader(open(f"{OUT_DIR}/match_result_sorted.csv", encoding="utf-8-sig")))
ocr = json.load(open(f"{OUT_DIR}/ocr_result.json", encoding="utf-8"))
print(f" {len(rows)} 张", flush=True)
# ---- 评级统计 ----
graded = [r for r in rows if r["评级公司"] != "非评级卡"]
ungraded = [r for r in rows if r["评级公司"] == "非评级卡"]
correct = [r for r in graded if r["评级公司"] == r["true_label"]]
wrong = [r for r in graded if r["评级公司"] != r["true_label"]]
acc = len(correct) / len(graded) * 100
COMPANIES = ["PSA", "BGS", "CGC", "SGC"]
comp_stat = {}
for cp in COMPANIES:
sub = [r for r in rows if r["true_label"] == cp]
c = sum(1 for r in sub if r["评级公司"] == cp)
comp_stat[cp] = (c, len(sub), len(sub) - c) # 正确, 总数, 误识别/漏检
# 误识别混淆: 真实 -> 识别成什么
confuse = Counter((r["true_label"], r["评级公司"]) for r in wrong)
# ---- OCR 分数/编号统计 ----
score_cnt = Counter(r["评级分数"] for r in graded)
n_serial = sum(1 for r in graded if r["serial_no"])
n_authentic = sum(1 for r in graded if r["评级分数"] == "authentic")
n_score_num = sum(1 for r in graded if r["评级分数"] and r["评级分数"] != "authentic")
print(f"[2] 评级准确率 {acc:.2f}% (评级卡{len(graded)}, 非评级卡{len(ungraded)}, 误识别{len(wrong)})", flush=True)
print(f" 分数识别: 数字{n_score_num} authentic{n_authentic} | serial {n_serial}/{len(graded)}", flush=True)
# ---- 案例渲染 ----
def render(r, tag):
fld, name = r["true_label"], r["filename"]
lines = ocr.get(name, {}).get("lines", [])
p_anno = b64(load(f"{OUT_DIR}/predict/{fld}/{name}"), h=280, q=82)
p_crop = b64(load(f"{OUT_DIR}/crops/{fld}_{name}"), h=120, q=85)
ocr_html = "
".join(lines[:14]) if lines else '(OCR 无文本)'
ok_mark = "✅" if r["评级公司"] == r["true_label"] else "❌"
return (f'
{ocr_html}| 真实类别 | 样本数 | 正确识别 | 误识别/漏检 | 准确率 | 可视化 |
|---|---|---|---|---|---|
| {cp} | {t} | {c} | {w} | ' f'{pct:.2f}% | |
| 合计(仅评级卡) | {len(graded)} | ' f'{len(correct)} | {len(wrong)} | {acc:.2f}% |
| 真实类别 | 误识别为 | 数量 |
|---|---|---|
| {t} | {p or "(空)"} | {n} |
' f'共 {len(wrong)} 张误识别(评级卡中)。另 {len(ungraded)} 张非评级卡未检出评级框(不计入准确率)。
评级卡 {len(graded)} 张中:识别出数字分数 {n_score_num} 张,' f'评级卡但分数 OCR 漏识(填 authentic) {n_authentic} 张,' f'编号 serial_no(≥6位) 识别 {n_serial}/{len(graded)} = {n_serial/len(graded)*100:.1f}%。
' '| 评级分数 | 数量 | 占比 |
|---|---|---|
| {s or "空"} | {n} | {n/len(graded)*100:.1f}% |
for ln in lines: # 从上到下逐行扫
if 纯数字 and 1.0 ≤ 该数 ≤ 10.0:
return ln # 返回第一个命中的
即"取 OCR 文本里第一个落在 1-10 范围的纯数字行",完全靠行序, 不理解标签布局、不认 BGS 四维分数、不区分评级公司、不看语义。
2025 POKEMON MEGA PROMOS 10 ← 第2行, 第一个1-10纯数字 → 判为分数 B #020MP PIKACHU MCDONALD'S JAPAN PURCHASE INCENTIVE CENTERING 9.5 ← CENTERING 维度分, 但已在10之后, 轮不到 CORNERS 10 PRISTINE BECKETT EDGES 10 SURFACE 10
10 在第 2 行、9.5 在第 9 行,逻辑先碰到 10 就返回—— 纯粹"谁排前面",不是因为 10 更像总分。
旁证矛盾:文本出现 PRISTINE(BGS 黑标,对应总分 10),
但 CENTERING 9.5(PRISTINE 要求四维全 10)→
要么总分其实是 9.5(OCR 误读描述),要么行序错位。无 ground truth 无法定论。
每张:best.pt 评级检测图 | 评级标签条(OCR 输入) | OCR 分行文本 + 识别结果
') for title, color, items in cases: if not items: continue total = len(correct if "正确" in title else wrong if "误" in title else ungraded) H.append(f'