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     # 视觉模型盲测表

一个方法论陷阱先记下来:第一版只测了干净大字号,混淆图几乎是空的,于是"推导"出 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% 正确率,是全字符集最差,它同时和 493G 混。按数据 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. 这次测量能证明什么

  1. n=8 的视觉臂太小,且评分者即作者(尽管做了盲化)。跨模型(GPT / Gemini / 豆包 / 通义)复测仍未做。
  2. 单引擎。 Tesseract 不是 AIQR 的目标消费者,只是唯一能自动化跑几千次的东西。
  3. 每字符 48 次样本,只够观测 ~2% 量级的混淆。12 条零观测的折叠未被证实也未被否定
  4. 全是合成劣化。 没有真实印刷、真实纸张、真实反光、真实曲面(贴在圆柱机身上)、 真实遮挡。这些是工业现场的常态。
  5. 两臂的劣化梯度不完全可比(视觉表统一放大回同宽),不能直接对比两个百分比。

8. 待办