离线识别现在不需要任何接口。一条 pip 命令装完,31.76 MB 的模型已经躺在包目录里,我把代理环境变量指到一个不存在的端口,识别照样跑,中文 OCR 走本地这条路是通的。
但装上不等于能用。我在本机(WSL,分到 2 核,3.8 GB 内存)用 5 张自渲染的中文卡片跑了 12 档降质条件,实测下来:默认参数单张 900×258 的卡片要 2.49 秒,其中 2.39 秒花在检测上;把检测的最短边下限从 736 调到 224,同一张图掉到 0.41 秒,字准确率没变。反过来,重模糊图上默认参数是最差的一档,一张 3 行的卡片 0 行都没检出来。
装完包里有什么
装的是一套 ONNX 推理的 OCR 工具库,检测、方向分类、识别三个模型都是小的那一档,随 wheel 一起分发。体积账目如下,都是装完后实测的。
| 项目 | 体积 | 说明 |
|---|---|---|
| rapidocr 3.9.2 的 wheel | 27.3 MB | 单个 whl,纯 Python |
| 装完的包目录 | 33.1 MB | 除代码外含 3 个 onnx |
| 检测模型 PP-OCRv6_det_small | 9.93 MB | 找文字框 |
| 识别模型 PP-OCRv6_rec_small | 21.23 MB | 认字 |
| 方向分类 ch_ppocr_mobile_v2.0_cls_mobile | 0.59 MB | 判断有没有 180 度倒置 |
| 依赖(rapidocr 自身除外) | 265.5 MB | opencv_python 121.2、numpy 71.4、onnxruntime 68.2、pyclipper 3.7 |
离线这件事是验证过的:把 HTTP 和高低代理环境变量都指到 127.0.0.1:1,加载模型时只打印 “File exists and is valid”,然后识别出 3 行,耗时 2883 ms。模型文件在包的 models 目录里,只有换成包里没带的档位才会去下载。
默认参数里真正影响结果的就这么几个,都取自包内 config.yaml:
| 参数 | 默认值 | 作用 |
|---|---|---|
| Det.limit_side_len / limit_type | 736 / min | 图像最短边不足 736 就整体放大 |
| Global.max_side_len | 2000 | 最长边超过就整体缩小 |
| Det.thresh / box_thresh | 0.3 / 0.5 | 文字区域二值化与文本框置信门槛 |
| Det.unclip_ratio | 1.6 | 文本框外扩比例 |
| Global.text_score | 0.5 | 低于这个分数的识别结果直接丢 |
| Rec.rec_batch_num | 6 | 识别一次喂几行 |
| Det / Rec 版本与档位 | PP-OCRv6 small | 另有 v4、v5 三代和 tiny、medium 档 |
跑一张图
调用只有三行,返回值里带着每一行的置信度和各阶段耗时。
fromrapidocrimportRapidOCR engine=RapidOCR()# 默认 PP-OCRv6 small,模型在包的 models 目录里out=engine("imgs/card1.png")fort,sinzip(out.txts,out.scores):print(round(float(s),3),t)print("阶段耗时(秒):",[round(x,3)forxinout.elapse_list])本机真实输出:
0.997 把离线识别接进截图流程 1.0 模型随安装包分发,不用联网 0.934 一张 900×300 的卡片不到 1 秒跑完 阶段耗时(秒): [2.533, 0.003, 0.144]三段耗时对应检测、方向分类、识别。第一次调用会带上模型加载,2.69 秒;之后稳定在 2.49 秒。也就是说这张卡片的 2.5 秒里,检测吃掉 96%,方向分类 3 毫秒,识别 144 毫秒。
检测为什么吃掉 2.39 秒
看它的预处理就明白了:min 模式下,图像最短边不足 limit_side_len 就按比例放大,再把高宽取整到 32 的倍数。900×258 的卡片会被放大到 2560×736,像素量变成原来的 8.1 倍;字号小的图更夸张。
| 输入图 | 原图 | 最短边 736(默认) | 最短边 320 | 最短边 224 |
|---|---|---|---|---|
| 28 px 卡片 | 900×258 | 2560×736,8.1 倍像素 | 1120×320,1.5 倍 | 896×256,1.0 倍 |
| 12 px 卡片 | 900×156 | 4256×736,22.3 倍 | 1856×320,4.2 倍 | 1280×224,2.0 倍 |
| 缩到 33% 的卡片 | 297×85 | 2560×736,74.6 倍 | 1120×320,14.2 倍 | 768×224,6.8 倍 |
| 手机长截图 | 1080×1608 | 1088×1600,1.0 倍 | 同左 | 同左 |
顺着这张表往下调,同一批卡片的耗时是这样的(每档跑 2 次取最快):
| 最短边下限 | 28 px 卡片单图 | 12 px 卡片单图 | 两张的字准确率 |
|---|---|---|---|
| 736(默认) | 2468 ms | 4014 ms | 100% / 100% |
| 480 | 1193 ms | 1748 ms | 100% / 100% |
| 320 | 579 ms | 842 ms | 100% / 100% |
| 224 | 411 ms | 454 ms | 100% / 100% |
| 160 | 409 ms | 283 ms | 100% / 100% |
736 到 224,28 px 卡片快了 6 倍,准确率一分没掉。默认这档是给整页扫描件准备的,截图、界面图、卡片图这类文字本来就大的输入,把最短边降下来更划算。长截图则完全不受这个参数影响,最短边 1080 本来就超过 736。
十二档降质:汉字一个没错,掉的全是标点
测试集是 5 张 900 像素宽的卡片,用微软雅黑渲染,每张 3 行中文,混了日期、英文和标点。字准确率 = 1 - 编辑距离(真值, 识别) / 真值字符数,比对前去空白;每个条件 5 张图共 217 个字。
| 条件 | 检出行数 | 字准确率 | 单图最快 |
|---|---|---|---|
| 原图 28 px | 15/15 | 100.00% | 2488 ms |
| 字号 20 px | 15/15 | 100.00% | 3038 ms |
| 字号 14 px | 15/15 | 99.08% | 3784 ms |
| 字号 12 px | 15/15 | 99.54% | 4054 ms |
| 字号 10 px | 15/15 | 100.00% | 4381 ms |
| 整图缩到 50% | 15/15 | 99.54% | 2451 ms |
| 整图缩到 33% | 15/15 | 100.00% | 2462 ms |
| JPEG 质量 20 | 15/15 | 98.16% | 2457 ms |
| 模糊 σ=1.2 | 15/15 | 100.00% | 2472 ms |
| 模糊 σ=2.4 | 15/15 | 100.00% | 2450 ms |
| 旋转 3° | 15/15 | 100.00% | 2466 ms |
| 浅灰字 #969696 | 15/15 | 100.00% | 2470 ms |
所有非 100% 的分数都来自同一件事:把全角括号写成了半角,把全角冒号写成半角,把“RapidOCR 3.9.2”中间的空格吃掉。逐条对过原始输出,这一批 217 个字里没有一个汉字被认错。真拿识别结果做关键词检索的话,标点归一化这一步省不掉,不然查“(甲)”永远命中不了。
还有一个反直觉的地方:字号越小反而越慢。12 px 卡片 4014 ms,比 28 px 卡片慢了六成,因为最短边被撑到 736 时放大倍数更大,检测输入从 2560×736 涨到 4256×736。想省时间,先看放大倍数,别只看文件大小。
重模糊图上默认参数最差,模型也不是越大越好
把高斯模糊加到 σ=4.0,结果翻了个面:默认的 736 下,3 张卡片里两张 0 行、一张只认出 1 个字;最短边降到 224 反而捡回来大部分。每张图每档各跑 2 次,两次结果完全一致。
| 最短边下限 | σ=3.0 三张卡(字准确率) | σ=4.0 三张卡(字准确率) |
|---|---|---|
| 736(默认) | 100% / 98.31% / 100% | 1 行只认出 1 字 / 0 行 / 0 行 |
| 480 | 95.24% / 100% / 97.22% | 69.05% / 89.83% / 88.89% |
| 320 | 100% / 98.31% / 100% | 66.67% / 0 行 / 47.22% |
| 224 | 100% / 98.31% / 100% | 90.48% / 44.07% / 50.00% |
| 224 且 thresh 0.2 / box_thresh 0.3 | — | 85.71% / 91.53% / 88.89% |
调低检测阈值单独用没用:在 736 下把 thresh 从 0.3 降到 0.2、box_thresh 从 0.5 降到 0.3,三张图依旧 0 行。它得和调小最短边一起用才有效,224 加低阈值那一行是这套组合里最稳的。渲染出来的图经过放大后模糊边缘跟着放大,检测反而更难出框,这个链路我没有继续挖到根因,只把观测记在这里。
再说模型换档。包里带的 small 不是唯一选择,tiny 和 medium 都能换,换上会去模型仓下载。同样 4 张图(28 px 卡片、12 px 卡片、JPEG q20、模糊 σ=2.4)跑下来:
| 档位 | 检测 + 识别模型体积 | 4 张图总耗时 | 字准确率 | 单张 28 px 卡片 |
|---|---|---|---|---|
| tiny | 1.83 MB + 4.49 MB | 2.06 s | 97.77% | 490 ms |
| small(默认,随包) | 9.93 MB + 21.23 MB | 12.20 s | 97.77% | 2686 ms |
| medium | 62.12 MB + 76.63 MB | 159.63 s | 100.00% | 35590 ms |
medium 在 2 核机器上单张 35.6 秒,准的那 2.23 个百分点全是标点符号,汉字该对的本来就对。小机器上换 medium 不划算;tiny 倒是值得一试,4 张图 2.06 秒,准确率和默认档持平。
这几个取舍是我最后定下来的:
- 截图、界面图、卡片图统一把 Det.limit_side_len 设成 224 到 320,实测不丢字,速度是默认档的 4 到 6 倍。
- 长截图别去动最短边。我试过把 limit_side_len 提到 1536 想看清小字,26 行原文一个字没多认对,耗时从 3414 ms 涨到 6107 ms。
- 方向分类只有 3 毫秒,别关。关掉之后长截图那批从 3414 ms 只降到 3342 ms,省 2%,但倒置的图会整行认错。
- 图糊的时候先调参数再考虑换模型:224 加低阈值能救回大部分,比下载 76 MB 的识别模型快得多。
可以落地的三条
- 装完之后先拿自己最有代表性的 5 张图跑一遍,把 elapse_list 的三段耗时记下来,看时间到底花在检测还是识别上,再决定动不动模型档位。
- 截图类输入把 Det.limit_side_len 设成 224 到 320;输入偏糊就再补上 Det.thresh 0.2、Det.box_thresh 0.3,两者要一起用。
- 识别结果入库前做一次标点归一化(全角转半角、去掉数字与英文之间的多余空格),否则检索会因为这些符号漏掉本该命中的记录。