目录
概要
一、起因
二、第一件事不是写代码,是造一把尺子
三、Tesseract:从 53% 到 80%,然后卡死卡在哪
四、换成 PP-OCR:一次到位在浏览器里跑
五、换引擎不是免费的
六、几条可以复用的经验
1. 先有尺子,再谈优化
2. 一个变量一次实验
3. 警惕"看起来快好了"
4. 不要用置信度过滤
5. 诚实报数
七、最后
概要
一个开源小项目的技术复盘:先造评测再优化,四轮实验把召回从 53% 提到 80%,发现瓶颈在版面分析后换用 PP-OCR,一次到位 98.7%。附完整实验数据表。
一、起因
发截图之前打码,这件事每个人都做过,但很少有人做得对。
我用过的方式:截图 → 打开画图工具 → 拉几个黑框 → 检查一遍 → 发出去。问题在于你永远不知道自己漏了什么——手机号、身份证、内网 IP、一个Authorization: Bearer xxx,藏在满屏文字里,靠眼睛找靠不住。
现有工具要么让你上传图片(可截图里往往就是密钥),要么只给一支马赛克笔刷。
所以我做了Scrubshot:粘贴截图 → 自动识别敏感信息 → 人工确认 → 导出。图片全程留在浏览器里,不上传。
- 在线体验:http://121.40.98.30:8080
- 源码:GitHub - strugglesky/scrubshot: 粘贴一张截图,它找出手机号、身份证、银行卡、Token、地址,一键涂掉。 图片全程留在浏览器里——不上传、不联网、可断网使用。 · GitHub
本文不聊产品,只聊一个工程问题:为什么我把 OCR 引擎从 Tesseract 换成了 PP-OCR,以及这个决定是怎么做出来的。
二、第一件事不是写代码,是造一把尺子
我一开始就做了一个决定,后来被证明是整个项目里最值钱的:先造评测,再优化。
做法很简单——准备一批截图,再准备一份"这张图里有哪些敏感字符串"的清单。评测页导入两者,直接算出召回率和多报数。
标注格式刻意做得极轻,只写字符串,不画框:
json
{ "shop.png": ["18967676767", "1422622230020608295"], "admin.png": ["admin@example.com", "sk-abc123def456ghi789"], "code.png": [] }为什么不画框?因为画框的成本太高了。一张图 5 个敏感项,画框要 30 秒,写字符串只要 5 秒。成本差 6 倍,决定了你会不会去补第 31 张图。
最终数据集:30 张截图、157 个标注项,覆盖聊天记录、快递单、登记表、身份证、表格、便签、订单、医疗单、小票 10 类场景z
三、Tesseract:从 53% 到 80%,然后卡死
第一版用 Tesseract 5(tesseract.js,chi_sim+eng)。
第一轮评测:53%。
于是开始了一轮又一轮排查,每一步都跑一次评测:
| 阶段 | 改动 | 召回率 |
|---|---|---|
| 1 | 默认 PSM 3 | 53% |
| 2 | 补银行卡软匹配、长数字规则 | 77% |
| 3 | PSM 3 + 自适应擦除表格线 | 80% |
| 4 | 2×2 分块识别 + 双通道结果并集 | 80% |
| 5 | 换 RapidOCR(PP-OCRv5) | 98.7% |
前四步是"打磨",最后一步是"换引擎"。
卡在哪
问题出在版面分析(layout analysis)这一层,而不是字符识别。
Tesseract 会先把整张图切成"段落/表格/图片"等区域,再逐块识别。中文表单、便签这类"线条 + 大量留白"的排版,经常被它判定成非正文区域,整块跳过。
一个典型的例子:
代码块
0xxxxxxxxx ← 已识别 ───────────────── 商户名称: xxx ← 整块被跳过 订单号: xxx 金额: xxx ───────────────── 备注: xxx ← 又识别了10 行文字,只读出来 3 行。这种情况下,你把 PSM 从 3 调到 6 再到 11、擦掉表格线、甚至切成 2×2 分块分别识别再合并结果——能救回一部分,但救不全。我试过所有这些,天花板就是 80%。
第 4 步的"分块 + 双通道并集"其实已经把能做的都做了:整图识别一遍、分块再识别一遍,两次结果取并集。分数纹丝不动,说明瓶颈不在"识别不全",而在"整块没进识别器"。
结论:当瓶颈在 OCR 的预处理/版面分析层时,调识别参数是无效的。
四、换成 PP-OCR:一次到位
PP-OCR(PaddleOCR 的移动端系列)用的是逐行检测 + 逐行识别:
- 检测网络直接输出每一行文字的四点框(不需要版面分析)
- 每个框裁出来单独送进识别网络
- CTC 解码成字符串
不依赖"先判断这块是什么区域",所以中文表单、便签、聊天记录这些排版不规整的场景,优势非常明显。
换过去第一天,同一个 30 张数据集:155/157 = 98.7%。
在浏览器里跑
关键约束是"不上传图片",所以推理必须在浏览器内完成:
ts
// 单线程:多线程需要 COOP/COEP 响应头,静态托管往往配不了 ort.env.wasm.numThreads = 1 const ocr = await Ocr.create({ models: { detectionPath: '/models/PP-OCRv5_mobile_det_infer.onnx', recognitionPath: '/models/PP-OCRv5_mobile_rec_infer.onnx', dictionaryPath: '/models/ppocr_keys_v5.txt', }, })代价是体积:检测模型 + 识别模型约 20MB,ONNX Runtime 的 wasm 约 28MB,加上引擎本身约 10MB——首次识别要下载约 58MB。
对"粘贴一张图"这种场景,58MB 不算小。所以做了两件事:
- 懒加载:应用壳只有 630KB,重资源在第一次点识别时才下载
- 强缓存:带 hash 的资源永久缓存,模型文件 30 天
首次慢,之后快。
五、换引擎不是免费的
换完之后,召回率上去了,但出现了新的问题:打码的框变糙了。
原因很简单:Tesseract 的data.words提供了词级坐标,而 PP-OCR 只给"一行一个框"。于是收件人:孙艳佳 137-5506-1108这种"标签 + 值"在同一行的,整行都会被框住。
这属于体验问题,但还有更严重的:聊天框把 18 位身份证折成两行时,只遮住了前 12 位,后 6 位还露着——而评测指标把它算成了"命中"。
这两个问题(框的位置精度、跨行拼接)我在另一篇文章里详细写了,这里只说结论:指标涨了,不等于产品变好了。换引擎带来的回归,只能靠人眼发现。
六、几条可以复用的经验
1. 先有尺子,再谈优化
没有评测之前,我改代码全凭"感觉这张图准了"。有了评测之后,每一次改动都变成"分数动没动"。
这也是为什么我在项目里把评测做成了内置功能,而不是一个临时脚本——它必须足够便宜,便宜到你会随手跑。
2. 一个变量一次实验
上面那张表,每一行只改一个东西。如果同时改 PSM 和规则,你永远不知道是哪一项起了作用。
3. 警惕"看起来快好了"
项目里最危险的时刻,是分数到 80% 的时候。那时候每一张图看起来都还行,只有评测数据告诉你:还有 30 个敏感项漏在外面。
4. 不要用置信度过滤
很多人会写if (confidence < 60) continue。这个项目里我明确禁止了这件事——镜像图和低质量截图的行置信度普遍很低,但里面的数字可能是完全正确的。置信度只用来决定"要不要自动打码",不用来决定"要不要让你看见"。
5. 诚实报数
README 里我写的是"155/157 = 98.7%",而不是"99%"。漏掉的 2 条是 10 位数字(和 Unix 时间戳形态相同,刻意不覆盖),这个限制也写进了「已知限制」。
数字写得越保守,别人越信。
七、最后
这个项目最后的样子:
| 项 | 值 |
|---|---|
| 召回(30 张标注截图 / 157 项) | 98.7% |
| 误报 | 0(5 处「多报」经核对全是标注遗漏) |
| 单测 | 46 个 |
| 首屏 | 630KB |
| 首次识别 | 按需加载约 58MB,之后离线可用 |
如果你也在做 OCR 相关的项目,我的建议只有一句:
先用一小批数据算出一个数字,再决定改什么。没有数字的时候,所有的"优化"都是在赌。
代码和数据都在仓库里,评测集是 30 张合成截图(虚构数据),你可以 clone 下来跑出同一个数字。
- 仓库:GitHub - strugglesky/scrubshot: 粘贴一张截图,它找出手机号、身份证、银行卡、Token、地址,一键涂掉。 图片全程留在浏览器里——不上传、不联网、可断网使用。 · GitHub
- 在线体验:http://121.40.98.30:8080
下一篇会写另一个更硬的话题:OCR 只给你行框,怎么把框精确套在敏感词上——包括字符宽度估算、镜像后的坐标反转、以及跨行拼接的三个收紧条件。