OCR 实测:把字符集从"合理猜测"变成"有数据"
spec/aiqr-id-v1.1.md §12.1 承认:字符集是靠排版学分簇挑的,从没在真实 OCR 上验过。
这份文档是那次验证,以及它推翻的东西。
结论先说:测量找到了一个真实的安全缺陷(三擦除是伪造不是修复),也推翻了我自己写在 规范里的两条推理。规范据此改到 v1.1。
1. 方法
node scripts/ocr-confusion.mjs # 混淆矩阵 + 端到端
node scripts/ocr-whitelist-test.mjs # OCR 白名单的影响
node scripts/ocr-fold-test.mjs # 折叠表变体对照
node scripts/gen-vision-sheet.mjs # 视觉模型盲测表
- 引擎:tesseract.js (eng),WASM 版 Tesseract
- 字体:Consolas、Courier New、Arial、Verdana(含等宽与无衬线)
- 劣化梯度:12 档,从干净大字号到 0.18 倍缩放 + 模糊 + JPEG q20 + 旋转
- 样本:1,728 次单字识别(36 字符 × 4 字体 × 12 条件)+ 1,200 次整串识别
一个方法论陷阱先记下来:第一版只测了干净大字号,混淆图几乎是空的,于是"推导"出 34 个符号都安全。 如果测量条件下不发生混淆,就会证明没有字符会混淆。梯度必须够狠。
2. 混淆矩阵:字符集内部有漏网
现行 17 符字符集里各符号的识别正确率:
| 符号 | 正确率 | 被误读为 |
|---|---|---|
| C | 52% | 4(8%) 9(6%) 3(4%) G(2%) |
| 0 | 79% | 4(2%) |
| M | 81% | 1(2%) |
| P | 88% | 3(2%) |
| H | 92% | — |
| 5 / 8 | 94% | — |
| 6 / J / T / X | 96% | Y / 3 / Y / 3 |
| 3 / A / E / Y | 98% | S / — / — / 2 |
| 2 / 9 | 100% | — |
字符集内部残留的混淆对:C/9 C/3 J/3 P/3 T/Y X/3 Y/2。
我的排版学分簇漏掉了这些。其中 C 尤其糟——52% 正确率,是全字符集最差,它同时和
4、9、3、G 混。按数据 C 不该留在字符集里。
集外方面数据确认了核心判断:O 的正确率是 0%,永远被读成 0。排除 O、折叠 O→0 是对的。
同时数据否定了折叠表的三条方向:实测 4 是误读的 C(不是我写的 4→A)、
G 是误读的 C(不是 G→6)、S 是误读的 3(不是 S→5)。
另外 12 条折叠零观测——不是被否定,是样本量(每字符 48 次)不足以观测到 2% 量级的事件。
3. 端到端发现了一个真实缺陷
1,200 次整串识别 → 走完整解析器:
正确解析 963 (80.3%) 正确拒绝 230 (19.2%) 解析错误 7 (0.6%) ← 规范说这该是 0
7 例全部是 repaired 状态,没有一例是 valid。
所以 §7.3「修复结果必须二次确认」能拦下全部 7 例——这条不是建议,是系统安全的支柱。
但根因是设计缺陷。典型样例:
真值 0TYJ-29JH-ACJJ-MX9T
OCR OTYJ29IHACIIMXOT ← 3 个 I
解析 0TYJ-292H-ACEM-MX0T ← "修复"成一个完全不同的合法标识符
I 是擦除字符。3 个擦除吃光全部 3 个校验式,剩下零个用于验证。
三擦除的求解必然成功——它根本没被检验过。那不是修复,是伪造:
其余 13 个字符同样被读错,却完全没有被检查。
修复:擦除上限降到 2
MAX_ERASURES = CHECK − 1 = 2
解 k 个擦除消耗 k 个校验式,留下 3−k 个做验证。k=3 时无从验证。
同一批图重测:
| 正确 | 拒绝 | 错误 | |
|---|---|---|---|
| 擦除上限 3(原) | 74.7% | 24.3% | 7 |
| 擦除上限 2(现) | 74.1% | 25.9% | 0 |
代价 0.6 个百分点,正是原先被误判为"正确"的那 7 例。近乎免费。
4. 被推翻的推理之一:OCR 白名单
同一批图,只改 Tesseract 的字符白名单:
| 白名单 | 正确 | 拒绝 | 错误 |
|---|---|---|---|
| 全部 36 字母数字 | 74.7% | 24.3% | 7 |
| AIQR 17 符字符集 | 66.3% | 33.7% | 0 |
限制白名单后引擎输不出集外字符 → 不产生擦除 → 3 个校验式全部用于验证。 正确率降 8.4 点,但那 8.4 点变成拒绝而不是错误。
→ 规范 v1.1 新增 §7.4:解析器 SHOULD 把 OCR 约束到字符集。 这是零成本的, 任何字符集调优都替代不了它。
5. 被推翻的推理之二:折叠表
我在代码注释里写过"折错比擦除更糟"。实测把同一批 700 次 OCR 读数 在不同折叠表上重放(消除引擎随机性):
| 折叠表 | 正确 | 拒绝 | 错误 |
|---|---|---|---|
| 现行 16 条(全靠猜) | 74.1% | 25.9% | 0 |
| 只留有证据的(O→0) | 70.4% | 29.6% | 0 |
| 按数据修正(O→0,4→C,G→C,S→3) | 66.9% | 33.1% | 0 |
| 全不折叠,一律擦除 | 69.6% | 30.4% | 0 |
| 追加 1,I,L→M(消灭擦除) | 72.6% | 27.4% | 0 |
| 追加 1,I,L→J | 78.3% | 21.3% | 3 |
| 追加 1,I,L→T | 72.6% | 27.1% | 2 |
我猜的那版最好,"按数据修正"最差。 机制不是我想的那样:
折叠的价值不在于折得对,而在于把字符挡在擦除计数之外。 折错只变成一个替换错误,而单个替换错误是 100% 可修复的;三个擦除则完全不可修复。 有条目胜过条目准。
顺着这个机制推到极端——把 1/I/L 也折叠,擦除就永不发生——正确率能到 78.3%,
但重新出现 3 例错误解析。因为没有擦除后,任何 16 字符垃圾串只靠校验把关,
约 5% 会蒙混过关(随机串通过三校验式的概率加上单错修复路径的接受面)。
→ 保留现行 16 条 + 擦除上限 2。 那多出来的 4.2 个百分点,是一个会打开物理设备的
标准必须拒绝的交易。1 I L 不可折叠这一条,正是让这笔交易无法发生的护栏。
6. 视觉模型臂(盲测)
Tesseract 会先切分字符再逐个识别;视觉模型整体读、带上下文、有先验。失效模式不同, 所以上面的混淆矩阵不能外推。单独测。
盲测协议:标识符由加盐 URL 的 SHA-256 派生,脚本从不打印,答案只写进文件; 读图者必须先写下读数再开答案。
2026-07-27 · Claude Opus 5 · 8 行梯度 · n=8
| 行 | 条件 | 逐字符 | 解析 |
|---|---|---|---|
| 1 | clean | ✓ | valid |
| 2 | photo(1.5° 倾斜 + JPEG q70) | ✓ | valid |
| 3 | small(0.34×) | ✓ | valid |
| 4 | blurred | ✓ | valid |
| 5 | faded | ✓ | valid |
| 6 | ink-spread | ✓ | valid |
| 7 | tiny+jpeg(0.22× + q30) | ✓ | valid |
| 8 | worn(0.24× + 旋转 + 模糊 + q35) | ✓ | valid |
8/8 逐字符全对,包括读图时自评"信心很低"的第 8 行。
对比参考:Tesseract 在量级相当的劣化下是 74.1%。这支持 AIQR 的核心命题—— 目标消费者是视觉模型,而它们读字幕读得很好。
7. 这次测量不能证明什么
- n=8 的视觉臂太小,且评分者即作者(尽管做了盲化)。跨模型(GPT / Gemini / 豆包 / 通义)复测仍未做。
- 单引擎。 Tesseract 不是 AIQR 的目标消费者,只是唯一能自动化跑几千次的东西。
- 每字符 48 次样本,只够观测 ~2% 量级的混淆。12 条零观测的折叠未被证实也未被否定。
- 全是合成劣化。 没有真实印刷、真实纸张、真实反光、真实曲面(贴在圆柱机身上)、 真实遮挡。这些是工业现场的常态。
- 两臂的劣化梯度不完全可比(视觉表统一放大回同宽),不能直接对比两个百分比。
8. 待办
-
C的去留。 数据说 52% 正确率、和 4/9/3/G 混。但移除后字符集变 16(非质数), 需要补一个符号并重新推导。补哪个必须由更大样本的数据决定,不能再猜。 - 提高样本量到每字符 ≥500 次,让 2% 量级的混淆可观测,据此重建折叠表
- 跨引擎:PaddleOCR(中文场景主流)、真实 Tesseract(非 WASM)
- 跨视觉模型盲测,n ≥ 50
- 真实印刷样张:打印贴纸 → 贴到真设备 → 真实光照手机拍