1. 为什么我要把 PP-OCR 反复“折腾”五遍
PP-OCR 这套东西,但凡做过文字识别落地的,基本都绕不开。百度飞桨开源出来的这套轻量级 OCR 系统,检测加识别两个模型加起来模型体积能压到几兆,中文识别准确率在通用场景下能到 95% 以上,这在几年前是很难想象的。但问题也恰恰出在这里——模型好用,不代表部署好用。飞桨原生的推理框架在服务器上跑没问题,一旦要往边缘设备、往已有的 C 或者 Java 工程里塞,麻烦就来了。
我前前后后做了五个版本的 PP-OCR 部署项目,从最省事的 OpenCV DNN 推理,到性能拉满的 TensorRT 加速,再到完全不依赖任何第三方推理库的纯 C 实现,最后到给 Java 后端团队用的纯 Java 推理引擎。这一路踩的坑,比我看过的官方文档多得多。这篇文章就把这五个项目的设计思路、核心实现、参数计算和排查经验完整摊开讲一遍,不管你是刚接触 OCR 部署的新手,还是正在纠结选型的老手,应该都能捞到点能直接用的东西。
先说清楚这五个项目分别是什么定位,方便你对号入座:
| 项目 | 推理后端 | 依赖 | 适用场景 | 性能量级 |
|---|---|---|---|---|
| 项目一 | OpenCV DNN | OpenCV 4.x | 快速验证、跨平台 | 中等 |
| 项目二 | TensorRT | CUDA + TensorRT | NVIDIA GPU 服务器 | 高 |
| 项目三 | 纯 C 自研 | 无(仅标准库) | 嵌入式、单片机级 | 低但可控 |
| 项目四 | 纯 Java 自研 | 无(仅 JDK) | Java 后端服务 | 中等 |
| 项目五 | OpenCV + 后处理优化 | OpenCV 4.x | 复杂版面 | 中等偏上 |
这五个项目不是简单的重复劳动,每一个解决的都是前一个解决不了的问题。下面我按“为什么这么设计”和“具体怎么做”两条线交叉着讲。
2. 项目一:OpenCV DNN 推理,最省事的起步方案
2.1 为什么第一个项目选 OpenCV DNN
做 OCR 部署,第一件事是让模型能跑起来。这时候最不该干的事就是上来就啃 TensorRT 或者自己写推理引擎——你连模型输入输出长什么样都没搞清楚,直接上底层就是给自己找罪受。OpenCV 的 DNN 模块支持直接加载 ONNX 模型,而 PP-OCR 的模型可以很方便地导出成 ONNX 格式,这就形成了一个最短路径。
OpenCV DNN 的优势在于:安装简单(pip install opencv-python 或者系统包管理器一行搞定)、跨平台(Windows、Linux、macOS、甚至树莓派都能跑)、API 友好(几行代码就能完成一次前向推理)。对于验证模型效果、做原型 demo、或者对性能要求不极致的场景,这套方案完全够用。
但它的短板也很明显:CPU 推理速度一般,GPU 加速需要编译时开启 CUDA 支持且配置繁琐,而且 OpenCV DNN 对某些算子(尤其是动态 shape 相关的)支持不完整,PP-OCR 的识别模型里有几个算子需要特别处理。
2.2 模型转换与输入输出对齐
PP-OCR 的检测模型(DBNet)和识别模型(CRNN)需要分别导出。飞桨提供了 paddle2onnx 工具,转换命令大致如下:
paddle2onnx --model_dir ./inference/det \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./onnx/det.onnx \ --opset_version 11 \ --enable_onnx_checker True这里有个关键点:opset_version 建议用 11。我试过 9、10、11、12 几个版本,9 和 10 在识别模型上会出现算子不支持的问题,12 在某些 OpenCV 版本上又有兼容性 bug,11 是最稳的。
转换完成后,用 Netron 打开 ONNX 文件确认输入输出。检测模型的输入通常是[1, 3, H, W],输出是[1, 1, H, W]的概率图;识别模型的输入是[1, 3, 32, W](高度固定 32),输出是[1, T, C]的序列,其中 T 是时间步,C 是字符集大小(PP-OCRv3 中文是 6623 类)。
2.3 检测后处理的完整实现
OpenCV DNN 只负责前向推理,后处理得自己写。DBNet 的后处理核心是把概率图二值化后找连通域,再对连通域做扩张得到文本框。这里有几个参数直接决定效果:
- 二值化阈值:默认 0.3,文本密集场景可以降到 0.2,稀疏场景提到 0.4
- box_thresh:框置信度阈值,默认 0.6,误检多就提高
- unclip_ratio:框扩张比例,默认 1.5,这个值越大框越胖
我实测下来,中文文档场景用thresh=0.3, box_thresh=0.5, unclip_ratio=1.6效果比较均衡。英文场景可以把 unclip_ratio 降到 1.3,因为英文字符间距小,扩张太多会把相邻单词粘在一起。
后处理里最容易踩的坑是轮廓查找的坐标顺序。OpenCV 的findContours返回的点顺序不保证是顺时针或逆时针,直接拿去算最小外接矩形会出问题。正确做法是用minAreaRect拿到旋转矩形,再用boxPoints转回四个顶点,最后按左上、右上、右下、左下的顺序排列。这个排序逻辑我写了不下十遍,每次换项目都得重新确认一遍。
2.4 识别模型的 CTC 解码
识别模型输出的是 CTC 序列,解码逻辑是:对每个时间步取 argmax,然后去掉重复字符和 blank(通常是第 0 类)。听起来简单,但有个细节:PP-OCR 的字符集里,blank 是第 0 位,但实际字符从第 1 位开始。如果你直接把 argmax 的结果当索引去查字典,会整体偏移一位,识别出来全是乱码。
正确的解码代码大概长这样:
def ctc_decode(preds, char_dict): preds_idx = preds.argmax(axis=2) # [1, T] result = [] prev = -1 for idx in preds_idx[0]: if idx != 0 and idx != prev: result.append(char_dict[idx]) prev = idx return ''.join(result)注意char_dict的构建:PP-OCR 的字典文件第一行是 blank,但索引 0 对应的是 blank,实际字符从索引 1 开始。我见过有人把字典直接按行读成 list,然后char_dict[idx],结果第一个字符永远错位。正确做法是在字典前面手动插入一个占位符,或者读的时候跳过第一行再偏移。
3. 项目二:TensorRT 加速,把 GPU 性能榨干
3.1 TensorRT 版本选择的血泪史
TensorRT 的版本兼容性是出了名的坑。我一开始用的是 TensorRT 8.5,配 CUDA 11.8,在 RTX 3090 上跑得好好的。后来换到一台带 GTX 1070 的机器上,发现 TensorRT 10.x 根本不支持这块卡——GTX 1070 是 Pascal 架构,计算能力 6.1,而 TensorRT 10.x 最低要求计算能力 7.0(Volta 架构起)。这个问题在热搜里也有人问,答案很明确:GTX 1070 最高只能用 TensorRT 8.6 左右的版本,再往上就不支持了。
所以选型的时候一定要先确认显卡的计算能力:
| 显卡系列 | 架构 | 计算能力 | 支持的 TensorRT 上限 |
|---|---|---|---|
| GTX 10 系 | Pascal | 6.1 | 8.6 |
| RTX 20 系 | Turing | 7.5 | 10.x |
| RTX 30 系 | Ampere | 8.6 | 10.x |
| RTX 40 系 | Ada | 8.9 | 10.x |
这个表建议直接存下来,选版本的时候先查表,能省掉大量重装环境的时间。
3.2 ONNX 到 TensorRT 引擎的转换
TensorRT 的转换有两种方式:trtexec 命令行和 Python API。trtexec 适合快速验证,Python API 适合集成到构建流程里。我一般先用 trtexec 跑通,确认没问题再用 Python API 写自动化脚本。
trtexec 的典型命令:
trtexec --onnx=det.onnx \ --saveEngine=det.engine \ --fp16 \ --workspace=4096 \ --minShapes=input:1x3x32x32 \ --optShapes=input:1x3x640x640 \ --maxShapes=input:1x3x960x960这里有几个参数需要重点解释:
- --fp16:开启半精度。PP-OCR 的模型用 FP16 推理,精度损失几乎可以忽略(实测识别准确率下降不到 0.5%),但速度能提升 1.5 到 2 倍。如果显卡支持 INT8 且你有校准集,还能再快,但校准过程比较麻烦,一般 FP16 就够了。
- --workspace:显存工作空间,单位 MB。设太小会导致某些层无法用最优算法,设太大浪费显存。4096 是个比较稳妥的值,显存紧张可以降到 2048。
- 动态 shape:检测模型的输入尺寸是可变的,所以必须用 min/opt/max 三个 shape 来定义范围。optShapes 应该设成你最常用的尺寸,TensorRT 会针对这个尺寸做最优优化。
识别模型的高度固定是 32,宽度可变,所以 minShapes 可以设1x3x32x32,maxShapes 设1x3x32x1280(对应很长的文本行)。
3.3 推理时的内存管理
TensorRT 推理最容易被忽略的是内存管理。每次推理都要在 GPU 上分配输入输出缓冲区,如果每次 new 一个,很快就会显存碎片化。正确做法是初始化时分配好固定缓冲区,推理时复用。
CUDA 的内存分配用cudaMalloc,释放用cudaFree。但更推荐用cudaMallocHost分配页锁定内存做主机端缓冲,这样 H2D 和 D2H 的拷贝速度会快很多。我实测过,用页锁定内存比普通内存的拷贝速度快 2 到 3 倍,对于小模型来说,拷贝时间占比不小,这个优化很值。
还有一个坑:TensorRT 的 enqueue 是异步的。调用context->enqueueV2之后,必须调用cudaStreamSynchronize等待完成,否则你拿到的输出可能是上一次的结果。这个 bug 很隐蔽,因为大部分时候结果看起来是对的,只有在高并发或者连续推理时才会偶发错乱。
3.4 批处理与并发优化
TensorRT 支持动态 batch,把多张图拼成一个 batch 推理能显著提升吞吐。但 PP-OCR 的检测模型输入尺寸不一,直接拼 batch 需要 padding 到同一尺寸,padding 太多会浪费计算。我的做法是:按尺寸分桶,相近尺寸的图放一个 batch,桶内 padding 到最大尺寸。
识别模型更适合批处理,因为高度固定 32,宽度 padding 到 batch 内最大宽度即可。实测 batch=8 时,识别吞吐能比 batch=1 提升 4 倍左右,再往上提升就不明显了,因为 GPU 利用率已经接近饱和。
4. 项目三:纯 C 自研推理引擎,不依赖任何库
4.1 为什么要写纯 C 推理
这个项目是被逼出来的。有个客户的环境是定制化的嵌入式 Linux,不能装 OpenCV,不能装 CUDA,连 C++ 标准库都是裁剪过的。这种情况下,任何依赖第三方库的方案都行不通,只能自己从零实现推理。
纯 C 推理的核心工作分三块:模型解析(读 ONNX 或自定义格式)、算子实现(卷积、BN、激活、池化等)、内存管理(手动分配释放)。听起来工作量巨大,但 PP-OCR 的模型结构其实不复杂,检测模型主要是 ResNet 风格的 backbone 加 FPN,识别模型是 CRNN,算子种类有限。
4.2 模型格式的简化
直接解析 ONNX 在纯 C 里是噩梦,protobuf 的解析就够写几百行。我的做法是写一个 Python 脚本,把 ONNX 模型转成自定义的二进制格式:权重按顺序排列,每层记录类型、输入输出维度、权重偏移和长度。这样 C 端只需要按格式读取,不需要任何解析库。
自定义格式的设计大概是这样:
[magic:4][version:4][layer_count:4] [layer1: type(4) + in_dim(16) + out_dim(16) + weight_offset(8) + weight_size(8) + params...] [layer2: ...] ... [weight_data: 连续的 float32 数组]维度用 4 个 int32 表示(NCHW),权重偏移是相对于权重区起始位置的字节偏移。这个格式简单到用 fread 就能读,不需要任何复杂逻辑。
4.3 卷积算子的实现要点
卷积是计算量最大的算子,纯 C 实现要尽量优化。最朴素的是七层循环(N、C、H、W、K、R、S),但这样 cache 命中率极低。我用了几个优化:
- im2col + GEMM:把卷积转成矩阵乘法,虽然多了内存拷贝,但矩阵乘法容易优化,整体反而更快。对于 3x3 卷积,im2col 的展开倍率是 9,内存开销可接受。
- 循环重排:把 K(输出通道)放在最外层,这样权重可以复用,减少内存访问。
- 定点化:如果平台没有 FPU,可以把权重和激活量化成 int8,用整数运算代替浮点。PP-OCR 量化后精度下降约 2%,但速度能提升 3 到 4 倍。
BN 层在推理时可以融合进卷积:y = (x - mean) / sqrt(var + eps) * gamma + beta可以化简成y = x * scale + shift,其中scale = gamma / sqrt(var + eps),shift = beta - mean * scale。融合后 BN 就消失了,少一次内存读写。
4.4 内存池的设计
嵌入式环境内存紧张,不能频繁 malloc/free。我实现了一个简单的内存池:初始化时分配一大块内存,所有中间张量都从池里切分。每层计算前申请,计算完释放回池。因为 PP-OCR 的模型是线性的(没有分支),所以内存可以复用——第 i 层的输出内存,在第 i+1 层用完就可以给第 i+2 层用。
内存池的大小需要预先估算:取所有层中输入输出张量的最大值,再乘以 2(因为要同时存当前层输入和输出)。实测 PP-OCRv3 检测模型在 640x640 输入下,峰值内存约 80MB,识别模型约 20MB,加起来 100MB 左右,一般的嵌入式设备都能承受。
5. 项目四:纯 Java 自研推理引擎,给后端团队用
5.1 Java 做推理的动机与挑战
Java 后端团队要集成 OCR,但不想引入 JNI(维护成本高,跨平台编译麻烦),也不想单独部署一个 Python 服务(网络开销、运维复杂)。那就只能用纯 Java 实现推理。
Java 做推理的挑战在于:没有原生的多维数组,没有 SIMD 指令的直接访问,GC 会带来不可控的停顿。但 Java 也有优势:JIT 编译后的热点代码性能接近 C++,而且开发效率高,团队熟悉。
5.2 用 float[] 模拟张量
Java 里表示张量,最直接的是float[]加维度信息。我定义了一个Tensor类:
public class Tensor { public float[] data; public int[] shape; // NCHW public int size; public Tensor(int... shape) { this.shape = shape; this.size = 1; for (int s : shape) this.size *= s; this.data = new float[size]; } public float get(int n, int c, int h, int w) { return data[((n * shape[1] + c) * shape[2] + h) * shape[3] + w]; } }索引计算是性能关键。每次 get 都做乘法太慢,优化做法是预计算 stride,用n*strideN + c*strideC + h*strideH + w来算。更进一步,在卷积的内层循环里直接用一维索引,避免方法调用开销。
5.3 卷积的 Java 实现与 JIT 优化
Java 的卷积实现和 C 类似,但要注意几点:
- 避免在热点循环里创建对象:所有临时变量都用基本类型,不要用 Integer 或 Float。
- 数组边界检查:JIT 能消除大部分边界检查,但前提是循环写法要规范。用
for (int i = 0; i < len; i++)而不是增强 for 循环。 - 方法内联:把小的工具方法标成
private static final,JIT 更倾向于内联。
我实测下来,Java 版的卷积比 C 版慢 30% 到 50%,但比 Python 版快 5 到 10 倍。对于后端服务来说,这个性能完全够用——单张图识别 200ms 左右,QPS 能到 5,配合线程池还能更高。
5.4 与 Spring Boot 的集成
Java 推理引擎最终是要集成到 Spring Boot 服务里的。我的做法是:
- 引擎初始化放在
@PostConstruct里,只加载一次模型。 - 推理方法用
synchronized或者ReentrantLock保护,因为 Tensor 的缓冲区是复用的,多线程会冲突。 - 如果要支持并发,就搞一个引擎池,每个线程用一个独立引擎。引擎本身不占太多内存(模型权重共享,只有中间缓冲区独立),池化是可行的。
这里有个坑:Java 的 float 运算和 C 的 float 运算结果可能有微小差异,因为 JIT 可能用 FMA(融合乘加)指令,而 C 编译器不一定用。这个差异在单层看不出来,但经过几十层累积后,可能导致最终结果有 1e-3 级别的偏差。对于 OCR 来说,这个偏差通常不影响识别结果,但如果你的后处理有阈值判断,就要注意把阈值留点余量。
6. 项目五:OpenCV 后处理优化,解决复杂版面
6.1 复杂版面的痛点
前四个项目解决的是“能不能跑”和“跑得快不快”,第五个项目解决的是“跑得准不准”。实际业务里的图片往往不是干净的文档扫描件,而是带表格、带图章、带手写批注、带倾斜的复杂版面。默认的后处理参数在这种场景下会出各种问题:表格线被当成文字、图章区域产生大量误检、倾斜文本识别率骤降。
6.2 方向分类与矫正
PP-OCR 自带方向分类模型,可以判断文本行是 0 度还是 180 度。但实际场景里还有 90 度和 270 度的情况,以及任意角度的倾斜。我的做法是:
- 先用检测模型拿到所有文本框。
- 对每个框算最小外接矩形的角度。
- 如果角度接近 90 或 270,旋转图片后重新检测。
- 如果角度在 -15 到 15 度之间,直接用仿射变换矫正。
仿射变换用cv2.getRotationMatrix2D和cv2.warpAffine,注意旋转后的画布要扩大,否则边角会被裁掉。计算新画布尺寸的公式:
new_w = w * |cos| + h * |sin| new_h = w * |sin| + h * |cos|6.3 表格线与文字分离
表格场景下,表格线会被检测成细长的文本框,识别出来全是乱码。解决办法是在检测后加一个过滤:如果框的宽高比超过 20(很细长),且框内像素的方差很小(说明是纯色线),就判定为表格线,剔除掉。
更彻底的做法是先做表格线检测(用形态学操作提取长直线),把表格线从原图里抹掉,再跑 OCR。这样文字检测就不会被表格线干扰。抹线的操作:
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (50, 1)) horizontal = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 50)) vertical = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) table_lines = cv2.add(horizontal, vertical) img_clean = cv2.inpaint(img, table_lines, 3, cv2.INPAINT_TELEA)cv2.inpaint用周围像素填充被抹掉的区域,效果比直接涂白好很多,因为涂白会在文字中间留下白线,影响识别。
6.4 后处理参数的场景化调优
不同场景的后处理参数应该不一样,我整理了一个调优对照表:
| 场景 | thresh | box_thresh | unclip_ratio | 额外处理 |
|---|---|---|---|---|
| 标准文档 | 0.3 | 0.6 | 1.5 | 无 |
| 表格 | 0.3 | 0.5 | 1.3 | 抹表格线 |
| 自然场景 | 0.2 | 0.4 | 1.8 | 方向矫正 |
| 手写体 | 0.25 | 0.35 | 2.0 | 膨胀腐蚀 |
| 密集小字 | 0.2 | 0.3 | 1.2 | 放大 2 倍再检测 |
这个表是我在几十个实际项目里总结出来的,不一定对所有场景都最优,但作为起点能省很多试错时间。
7. 五个项目的横向对比与选型建议
7.1 性能实测数据
我在同一台机器(Intel i7-10700 + RTX 3070)上对五个项目做了对比测试,输入是一张 1280x720 的文档图片,包含约 200 个字符:
| 项目 | 推理后端 | 检测耗时 | 识别耗时 | 总耗时 | 内存占用 |
|---|---|---|---|---|---|
| 项目一 | OpenCV DNN (CPU) | 320ms | 180ms | 500ms | 200MB |
| 项目一 | OpenCV DNN (CUDA) | 80ms | 45ms | 125ms | 500MB |
| 项目二 | TensorRT FP16 | 25ms | 12ms | 37ms | 800MB |
| 项目三 | 纯 C (单线程) | 450ms | 250ms | 700ms | 100MB |
| 项目四 | 纯 Java | 380ms | 220ms | 600ms | 300MB |
| 项目五 | OpenCV + 优化 | 350ms | 190ms | 540ms | 250MB |
TensorRT 版最快,比 OpenCV CPU 版快 13 倍。纯 C 版最慢,但内存占用最低,适合资源受限环境。Java 版比 C 版快,因为 JIT 优化确实有效。
7.2 选型决策树
选哪个方案,按下面的顺序判断:
- 有没有 NVIDIA GPU?有且追求性能,选 TensorRT。有但不追求极致,选 OpenCV CUDA。
- 能不能装第三方库?不能,且是 C 环境,选纯 C。不能,且是 Java 环境,选纯 Java。
- 是不是复杂版面?是,选项目五的优化方案,不管底层用哪个推理后端。
- 只是验证模型效果?选 OpenCV DNN CPU 版,最快能跑起来。
7.3 各方案的维护成本
性能之外,维护成本也很重要:
- OpenCV DNN:维护成本最低,OpenCV 社区活跃,遇到问题好搜。
- TensorRT:维护成本中等,版本兼容性坑多,但一旦跑通很稳定。
- 纯 C:维护成本高,所有算子都要自己调,但可控性最强。
- 纯 Java:维护成本中等,Java 生态好,但推理相关的资料少。
8. 踩坑实录与排查技巧
8.1 模型转换阶段的常见问题
问题一:paddle2onnx 转换后模型输出对不上。
这个通常是 opset 版本问题。PP-OCR 的模型里用了HardSwish激活,这个算子在 opset 11 才支持。如果用了 opset 9,转换会静默失败,输出全零。解决办法就是指定 opset 11 或更高。
问题二:ONNX 模型在 OpenCV 里加载报错。
OpenCV DNN 对 ONNX 的支持不是 100% 的。如果报Unsupported layer type,先用 Netron 看是哪一层,然后要么换 opset 版本重新导出,要么在 OpenCV 里手动注册自定义层。PP-OCR 里最容易出问题的是HardSigmoid,OpenCV 4.5 以后支持了,之前的版本需要自己实现。
8.2 推理阶段的性能问题
问题三:TensorRT 首次推理特别慢。
这是正常的,TensorRT 在首次推理时会做 kernel 自动调优(autotuning),这个过程可能持续几秒到几十秒。解决办法是在初始化时跑一次 dummy 推理,把调优结果缓存下来。如果用了--saveEngine,调优结果会保存在 engine 文件里,下次加载就不用重新调优了。
问题四:Java 版推理越跑越慢。
大概率是 GC 问题。每次推理都创建大量临时 Tensor,年轻代很快满,触发频繁 GC。解决办法是用对象池复用 Tensor,或者调大年轻代(-Xmn)。我一般把年轻代设成 512MB,基本能避免推理过程中的 GC。
8.3 识别效果问题
问题五:识别结果有重复字符。
CTC 解码没去重。检查解码逻辑,确保相邻相同字符只保留一个,且 blank 被正确跳过。
问题六:长文本识别不全。
识别模型的输入宽度有限制,PP-OCRv3 默认最大宽度是 320 个时间步。如果文本行很长,会被截断。解决办法是把长文本行按宽度切分成多段,分别识别后拼接。切分点选在字符间隙处,避免把字符切一半。
问题七:数字和字母识别混淆。
比如 0 和 O、1 和 l、5 和 S。这是模型本身的局限,解决办法是在后处理加规则:如果上下文是纯数字场景,就把字母映射成数字;反之亦然。更彻底的做法是用特定场景的数据微调模型,但成本较高。
8.4 排查速查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 输出全零 | opset 不兼容 | 检查转换日志 | 换 opset 11 |
| 识别乱码 | 字典偏移 | 打印 argmax 索引 | 字典前插 blank |
| 框重叠严重 | unclip_ratio 太大 | 调小到 1.2 | 重新后处理 |
| 漏检小字 | 输入分辨率低 | 放大图片 | 检测前 resize |
| GPU 显存溢出 | workspace 太大 | 降到 2048 | 或减小 batch |
| Java 推理卡顿 | GC 频繁 | 看 GC 日志 | 对象池 + 调大 Xmn |
9. 一些实操心得
做这五个项目最大的体会是:不要追求一步到位。我见过太多人上来就想搞 TensorRT 加速,结果卡在环境配置上好几天,连模型都没跑起来。正确的路径是先让模型跑起来(OpenCV DNN),再优化性能(TensorRT),最后考虑特殊环境(纯 C / 纯 Java)。
另一个体会是:后处理的重要性不亚于推理本身。同样的模型,后处理参数调好了,准确率能差 10 个百分点。我建议每个项目都留出专门的时间调后处理参数,不要指望默认值能适配所有场景。
还有一点:测试集要覆盖真实场景。我一开始用公开数据集测试,效果很好,一上真实业务图片就崩。后来自己标了几百张真实场景的图做测试集,才发现问题所在。这个测试集的投入是值得的,它能帮你快速判断一个改动是变好了还是变坏了。
最后说个具体的技巧:PP-OCR 的检测模型对输入尺寸很敏感。输入尺寸最好是 32 的倍数,因为 backbone 有 5 次下采样,每次除以 2,不是 32 的倍数会在下采样时出现奇数维度,导致特征图对齐问题。我一般把输入 resize 到 640x640 或 960x960,这两个尺寸在精度和速度之间比较平衡。
于 Java 版,还有一个容易被忽略的点:JIT 预热。Java 代码刚启动时是解释执行的,性能很差。我一般会在服务启动后跑 100 次 dummy 推理做预热,让 JIT 把热点代码编译成本地码。预热后性能能提升 3 到 5 倍,这个步骤不能省。
纯 C 版的话,编译器优化选项很关键。-O2和-O3的性能差距能有 30%,-ffast-math能再快 10% 但会牺牲一些精度。嵌入式环境如果用的是老版本 GCC,可能不支持某些优化选项,要先确认编译器版本。