news 2026/9/12 13:55:24

Android离线OCR实战:ncnn+PP-OCRv5模型部署与优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android离线OCR实战:ncnn+PP-OCRv5模型部署与优化

要说清楚移动端OCR这事,我折腾过不少方案,最后让我老老实实留在生产环境里的,是 nihui/ncnn-android-ppocrv5 这套项目。它把百度 PP-OCRv5 的模型迁移到了腾讯 ncnn 推理框架上,跑在 Android 端,离线、免授权、CPU/GPU 都能用。这篇文章就是我从拿到源码、转换模型、接进工程,到真机跑通的完整记录,里面包含我实测踩过的坑和修改过的代码逻辑,希望能帮你少走弯路。

这套方案的适用对象很明确:想在 Android 上做离线 OCR 识别的开发者,既不想被各家云识别服务的高昂调用费和隐私问题绑住,又不想用 Tesseract 那种精度和速度都不够看的传统方案。项目适合有一定 Android 开发基础、能摆弄 JNI 和 CMake 的人,纯小白直接照抄也会遇到一堆环境问题,但我后面会尽量把这些坑提前填平。

1. 为什么在这个时间点选 ncnn + PP-OCRv5

先说结论:不是没有别的路,是走了一圈下来,这条路在当前阶段最值。

1.1 移动端 OCR 的几个老选择,都有什么毛病

很多人在移动端做文字识别,第一反应是接云服务。百度、腾讯、阿里都有现成的 OCR API,效果确实好,但你真要落地到产品里,会发现几个绕不过去的坎:敏感数据不方便外传、网络波动直接影响识别耗时、按次计费在用户量大之后是一笔实打实的成本。这些限制在工具类、本地优先的应用里尤其致命。

本地方案里,Tesseract OCR 是很多人踩过的坑。Tesseract 有 Android 移植版,但它的模型年代久远,对中文长文本、倾斜文本、复杂背景的鲁棒性远不如深度学习方案,识别票据、菜单、实拍照片时错误率高得让人头疼。同样是本地跑,专门为中文优化过的深度学习 OCR 方案明显更实用。

还有个选择是 onnxruntime 的移动端版本。onnxruntime 本身很优秀,在桌面端和 server 端用得非常多,但它面向移动端的优化深度不如 ncnn。ncnn 从一开始就是为手机 CPU、GPU 和 NPU 设计的,有汇编级算子优化、内存布局优化、fp16 storage 这类针对移动硬件的特性。两者对比下来,如果你只跑 Android,ncnn 的推理性能和包体积压力都会更友好。

1.2 ncnn 和 PP-OCRv5 是怎么补上这个缺口的

ncnn 是腾讯开源的高性能神经网络推理框架,在移动端生态里非常成熟,很多开源项目都在用。它对 Android 的支持非常完善,CPU 端有 ARM 汇编优化,GPU 端可以通过 Vulkan 做加速。最关键的是,ncnn 有一套成熟的模型转换工具链:PyTorch 模型可以导出 ONNX,再由 onnx2ncnn 转成 ncnn 自己的 param/bin 格式。这意味着 PaddleOCR 训练出来的模型,只要走通导出链路,就能低成本部署到 Android 上。

PP-OCRv5 是我们在本文中重点关注的版本迭代。相比 v3、v4,v5 的检测和识别模型在精度和推理速度的平衡上做得更好:识别模型参数量进一步降低,但精度反而提升,长文本识别和弯曲文本的鲁棒性也增强了。对移动端来说,模型更小,意味着首包加载更快、内存占用更低。

而 nihui/ncnn-android-ppocrv5 这个项目,等于把“PP-OCRv5 模型 + ncnn 框架 + Android 工程”三者直接拼好了。作者 nihui 是 ncnn 的核心维护者之一,项目里已经把 JNI 封装、模型加载、推理管线、前后处理都写好了。你不需要从零去研究怎么把 PaddleOCR 的预处理逻辑翻译成 C++,也不需要自己写文本框检测、方向分类、文字识别三个模型的状态衔接,直接基于它改就行。

1.3 这个开源项目到底解决了什么问题

我拿到这个项目后的第一感受是:省掉了三件最麻烦的事。

第一是模型拼接。PP-OCRv5 完整识别链路是“文本检测(det)→ 方向分类(cls)→ 文本识别(rec)”。三个模型之间各有各的预处理和后处理,检测给分类送什么、分类给识别送什么,顺序、尺寸、通道顺序稍有差池,最终结果就全是乱的。这个项目把整条管线封装在一个 OCRPipeline 里,调用方只需要喂进一张 Bitmap,它给你返回一堆文本块坐标和内容。

第二是模型格式转换和裁减。PaddleOCR 官方提供的是 inference 模型,不能直接给 ncnn 用,需要依次导出 ONNX、再用 onnx2ncnn 转成 ncnn 格式,这个过程里涉及动态 shape、注意力机制算子兼容等一堆问题。项目里作者在文档里说明了测试过的转换路径,照着做能少碰很多钉子。

第三是 Android 端的图像预处理。直接拿 Bitmap 给模型推理是不行的,需要做 RGB 拆分、归一化、缩放、letterbox 填充。用 OpenCV 也能做,但回答到你最终打包体积多一个库又是一笔开销。这个项目用的是纯 ncnn 的 Mat 操作,不额外依赖 OpenCV,这一点我特别喜欢。

2. 环境准备与项目结构

这一步看起来基础,但我见过太多人卡在这里。不是说不会装 Android Studio,而是项目里有好几处隐形的版本依赖,对不齐就是编译不过去。

2.1 需要准备的三个东西

准备一台主流配置的开发机,Windows、Linux、macOS 都行,我自己在 Ubuntu 和 Windows 上各编译过一次,流程没有本质区别。然后装好 Android Studio,SDK 版本建议 API 27 或以上,否则项目的 minSdk 可能把你卡住。

其次要准备 NDK 和 CMake。项目通过 JNI 调用 ncnn,所以编译本地代码必须用到 NDK。我在 Android Studio 里通过 SDK Manager 装了 NDK 25,CMake 3.22,实测可以顺利编译。这里多说一句:NDK 版本太新可能踩到 clang 和高版本 glibc 相关的编译坑,太旧又可能不支持项目用的 C++17 特性,所以尽量用项目 README 里指定的版本。

第三个是模型文件。项目 assets 目录里应该自带测试模型,但如果你想换成自己从 PaddleOCR 导出的模型,或者版本对不上,后面就得手动走一遍转换流程。我建议无论自带模型能不能跑通,都把模型转换流程学一遍,因为换业务场景时几乎一定要换模型。

2.2 拿到源码后先看哪几个文件

克隆 nihui/ncnn-android-ppocrv5 之后,我建议你按下面的顺序看代码,而不是一上来就点 Run。

app/src/main/cpp/ocr_ppocrv5.cpp // JNI 入口和识别管线封装 app/src/main/cpp/ocr_ppocrv5.h app/src/main/cpp/ocr_det.cpp // 文本检测模型的包装 app/src/main/cpp/ocr_cls.cpp // 方向分类模型的包装 app/src/main/cpp/ocr_rec.cpp // 文本识别模型的包装 app/src/main/assets/ // 模型存放目录

cpp 目录里的结构非常像一套 MVC,view 对应 JNI 方法,model 对应三个模型的包装类。先看 ocr_ppocrv5.cpp 的 native 方法列表,再看 MainActivity 里调用它时传了什么参数,基本就能把数据流理干净。我一开始犯的错就是先去看模型包装类,结果被预处理里的归一化系数绕了半天,其实那些细节在项目里封装得都很好,不到万不得已不用改。

2.3 模型文件放哪里

模型默认放在 assets 目录下,常见命名是:

  • det_ppocrv5.param / det_ppocrv5.bin
  • cls_ppocrv5.param / cls_ppocrv5.bin
  • rec_ppocrv5.param / rec_ppocrv5.bin

项目在初始化时会把这些文件从 assets 复制到 app 的私有目录,再通过路径加载。这样做有个好处:模型不依赖外部存储,不给存储权限也能用。坏处是 APK 包体积会变大,三个模型加起来体积不小,如果你介意,可以在首次启动时从服务端拉取模型,但这就违背离线理念了,所以我的建议是直接打进 assets。

3. 模型转换:从 PaddleOCR 到 ncnn 的完整链路

这是整个项目里最容易出问题、也最值得深入讲的一步。我见过有人拿 PaddleOCR v3 的模型直接塞进项目里跑,识别效果差到没法看,最后发现是模型的预处理参数对不上。所以转换流程不仅要会执行命令,还得知道每一步在干什么。

3.1 先从 PP-OCRv5 导出推理模型

PaddleOCR 官方仓库提供 PP-OCRv5 的预训练模型,但你从官方仓库拉到的往往是训练权重,不能直接用。你需要用 PaddlePaddle 和 PaddleOCR 的 Python 环境,把权重导出成推理模型。流程大致是:

git clone https://github.com/PaddlePaddle/PaddleOCR.git cd PaddleOCR python tools/export_model.py \ -c configs/det/ch_PP-OCRv5_det_student.yml \ -o Global.pretrained_model=ch_PP-OCRv5_det_train \ Global.save_inference_dir=./output/det

注意这里用的是模型配置文件,不同模型的 yml 路径不一样,rec 和 cls 也要分别导出。导出后的 inference 模型主要包含两个文件:inference.pdmodel 和 inference.pdiparams,前者是网络结构,后者是权重参数。在线转换时,后面要用 PaddleOCR 提供的 paddle2onnx 工具把它转成 ONNX。

3.2 ONNX 导出与检查

拿到推理模型之后,接着转 ONNX。PaddleOCR 提供了现成的脚本:

python tools/export_onnx.py \ -c configs/det/ch_PP-OCRv5_det_student.yml \ -o Global.pretrained_model=your_inference_model \ Global.save_inference_dir=./output_onnx

转完之后最好用 Netron 打开 ONNX 模型看一眼输入输出的形状。PaddleOCR 模型通常有x作为输入名,输出可能是sigmoid_0.tmp_0softmax_0.tmp_0这种名字。你需要在代码里把输入输出名对上,否则 ncnn 会报找不到 blob。这一步很多人忽略,结果转到 Android 上一跑就崩,崩了也不知道报错在说什么。

3.3 onnx2ncnn 转换与 ncnnoptimize

Ubuntu 下生成 ncnn 格式的转换工具,需要从 ncnn 源码编译。先把 ncnn 仓库克隆下来,然后按官方文档用 CMake 生成 onnx2ncnn 和 ncnnoptimize 这两个可执行文件。命令大致是:

git clone https://github.com/Tencent/ncnn.git cd ncnn mkdir build && cd build cmake -DNCNN_BUILD_TOOLS=ON .. make -j4

编译完成后,tools/onnx/onnx2ncnn 就是我们要用的转换器:

./onnx2ncnn det_ppocrv5.onnx det_ppocrv5.param det_ppocrv5.bin

转换成功的标志是屏幕上没有任何 warning,且生成的 bin 文件大小和原模型体积在同一量级。如果遇到不支持的算子,onnx2ncnn 会直接打印 “Unsupported operator” 并中断,这时候要么换模型结构,要么手动打补丁,具体按报错来。转换为 ncnn 之后,我强烈建议再跑一次模型优化器:

./ncnnoptimize det_ppocrv5.param det_ppocrv5.bin det_ppocrv5_opt.param det_ppocrv5_opt.bin 1

最后一个参数表示优化级别,设为 1 表示开启 fp16 存储,可以显著减小模型体积,在支持 fp16 的 ARM CPU 上还能提速。但注意,如果你后续要跑 Vulkan GPU,fp16 存储也必须保留;如果不确定自己的设备是否兼容,可以先用 0 跑一遍,确认功能正确后再开 1。

3.4 转换一路上最容易翻车的几个点

第一,动态 shape 问题。PaddleOCR 的模型在导出时如果输入 shape 是动态的,转换出来的 ONNX 可能带有dynamic axes,onnx2ncnn 对动态 shape 支持有限,容易卡在 reshape 或 gather 节点。我的经验是:导出 ONNX 时把输入尺寸固定下来,比如 det 固定为 640x640,rec 固定为 320x48,这样转换成功率最高,推理时再按输出尺寸做 letterbox 适配。

第二,注意力机制算子兼容。PP-OCRv5 的识别模型用了类似 SVTR 的结构,里面有大量的 matmul、reshape、transpose 操作,ONNX 中转出来的节点和 ncnn 的算子实现未必一一对应。如果 onnx2ncnn 报了某个算子不支持,先看看 ncnn 的“算子支持列表”,很多情况下你只需要把 ONNX 里的某个融合节点拆开,或者在导出前在 PaddleOCR 配置里关闭某些融合选项。

第三,模型精度对比。转完 ncnn 格式后,我建议先在桌面端用 ncnn 的 C++ 例程或 python 接口跑一张图,看输出是否跟 ONNX Runtime 下的一致。我踩过一次坑:转出来的 ncnn 模型单独跑一张图完全正常,但到 Android 上识别结果里多了很多乱序文字,最后发现是 rec 模型的max_seq_len参数在导出时被裁短了,导致输出长度不够,把识别结果截断了。排查这类问题,最好的办法是打印每一层的输出维度,和 ONNX 的输出做对比。

4. Android 端核心实现:JNI 与识别管线

模型转换完毕,接下来就是把项目跑起来。这一节我们把项目里的核心代码逐段拆开看,理解每个部分在干什么,这样出了问题你才知道往哪个方向查。

4.1 OCRPipeline 整体结构

项目的核心是一个 OCRPipeline 类,它串联了三个模型。初始化阶段,它把 det、cls、rec 三个模型的 param 和 bin 文件加载进 ncnn::Net,并为每个模型配置好线程数、打开 fp16 优化,然后分别创建三个模型的包装对象。识别阶段,流程是:

  1. 对输入图片做缩放预处理,生成网络输入 blob
  2. 先走 det,输出文本候选框位置
  3. 对每个候选框,先按输出矩形裁图,送入 cls 判断方向,如果旋转角度过大,就翻转图像
  4. 再把方向修正后的图像送入 rec,得到字符串和置信度

这里最关键的一点是:三个模型是串行调用,前者的输出直接决定后者的输入区域。如果 det 漏检了文本框,后面分类和识别就不会执行;如果 cls 矫正得不对,rec 拿到一张倒着的图,识别结果基本就是乱的。所以项目里把 det 的阈值调高一点,往往整体识别准确率反而会上升。

4.2 文本检测(det)到底做了什么

文本检测模型的作用是找到图片里所有像文字的区域。它不负责认识字,只负责画框。PP-OCRv5 的检测模型用了可微分二值化(Differentiable Binarization)的思路,输出一张和输入相同尺寸的概率图,再通过后处理得到文本框坐标。

项目里把这个过程封装在 ocr_det.cpp 里。核心逻辑是:模型输入一张 NCHW 格式的图,经过前向推理得到概率图,再用连通域分析(connected components)找到文字区域的轮廓,最后用 OpenCV 的minAreaRect或者项目自己实现的最小外接矩形算法,输出每个文本框的四个角点坐标。

这一段我看过很多次,最大的感触是:投喂给 det 的图像尺寸很影响结果。项目默认把长边缩放到 960 以上,如果一张图非常小,文字区域占比又大,检测效果会下降。反之,如果一张图特别大,直接缩放到底再检测,小字会被抹掉。所以实际使用时,我会根据图片的长宽比做一个二次缩放策略,保证文字高度至少占输入尺寸的 4% 以上。

4.3 方向分类(cls)的必要性

可能有人会问:检测模型都画好框了,直接把框里的图送去识别不就行了?为什么还要多一步分类?因为现实中的图片是旋转的。手机拍照时,人会倾斜拿手机,扫描文档时纸张也可能横放。如果不做方向修正,识别模型拿到的可能是一张倒着的字图,结果自然是乱码。

这里的 cls 模型本质上是一个轻量级分类器,只做一件事:判断当前文字图像是正立还是旋转了 180 度。它输出一个置信度,如果判断倒置,代码就把图像翻转 180 度再送识别。方向分类模型很小,推理速度极快,对整体性能影响微乎其微,但它能显著改善实拍场景的识别体验。

我在实际使用中遇到过一个边界情况:当文本框里的内容本来就是对称文字,比如“一”“中”这种字,翻转后 cls 的置信度会模棱两可。这时候代码如果强行翻转,反而会把原本正立的图倒过来,导致识别失败。所以我后来给项目加了一个置信度阈值:只有 cls 的“倒置概率”超过 0.6 时才执行翻转,否则保持原样。

4.4 文本识别(rec)核心逻辑

识别模型是整条管线里最深的一块。它接收一个高度固定、宽度可变的图像,输出一串字符序列和对应的置信度。PP-OCRv5 的识别模型是基于 CTC 的,所以它输出的是一个矩阵,再通过 CTC 解码得到最终文本字符串。

在代码层面,rec 的包装类主要做这几件事:

// 将检测框图像缩放为模型输入尺寸 ncnn::Mat in = ncnn::Mat::from_pixels_resize(rgb, ncnn::Mat::PIXEL_RGB, w, h, target_w, target_h); // 归一化 in.substract_mean_normalize(mean_vals, norm_vals); // 前向推理 ncnn::Extractor ex = net.create_extractor(); ex.input("x", in); ex.extract("softmax_0.tmp_0", out); // CTC 解码 std::string text = ctc_decode(out);

这段代码看起来简单,但里面有一个容易被忽略的细节:from_pixels_resize用的是双线性插值还是最近邻,对于文字识别来说影响极大。双线性插值能保留更多笔画边缘信息,但如果目标尺寸被压缩得过小,笔画会粘连,反而影响识别。我的经验是:rec 的输入宽度不要小于 80,高度不要小于 32。如果检测框本身很窄,宁可让图像轻微拉伸,也不要压缩。

4.5 从图片到 ncnn::Mat:预处理细节

从 Bitmap 到 ncnn 可用的输入,中间要经历好几次变换。第一是颜色空间转换:Android 的 Bitmap 默认是 ARGB_8888 格式,而模型期望的是 BGR 或 RGB 三通道数据,需要先做一次通道转换。项目里用的是ncnn::Mat::from_bitmapfrom_pixels的组合,或者手动读 bitmap 的 pixel 数组再转 RGB。

第二是尺寸变换。det 模型通常要求输入尺寸是 32 的倍数,rec 模型则是高度固定宽度可变。如果直接用普通 resize 会破坏文字长宽比。项目里的做法是 letterbox:保持长宽比的前提下缩放图片,然后用灰条填充剩余区域。这样做的好处是模型输入分布和训练时更接近,检测识别准确率都更稳定。

第三是归一化。PaddleOCR 模型的预处理参数是固定的 mean 值和 norm 值,不同版本可能有差异。如果你换用了别的模型,这段参数必须同步修改,否则整个模型输出都会偏离分布,识别结果完全不可控。

5. 图片读取与 Android 文件访问适配

这个项目的输入是图片,但“图片从哪里来”这个最简单的问题,在 Android 上反而成了最容易报错的地方。尤其是现在很多手机都在 Android 11 以上,分区存储、文件提供者这些概念把开发者坑得不轻。

5.1 content:// URI 与不同 App 的 FileProvider

调用系统相册选图时,应用拿到的往往不是一个真实的文件路径,而是一个 content:// URI。这类 URI 是由 SystemUI 或相册应用通过 FileProvider 生成的,格式类似:

content://com.tencent.wework.fileprovider/external_path/android/data/com.tencent.wework/...

这个 path 本身会暴露资源所在的位置前缀,有时候还带有另一个 App 的包名。比如有的用户从企业微信里保存图片,然后去系统相册选择,拿到 URI 就可能长这样。问题在于,我们自己的应用没有权限直接访问另一个应用的 FileProvider 路径,必须通过 ContentResolver 打开输入流,再解码成 Bitmap。

所以代码里不要依赖getPath(),而是统一走:

val inputStream = contentResolver.openInputStream(uri) val bitmap = BitmapFactory.decodeStream(inputStream)

这个方式能通吃系统相册、文件管理器、第三方 App 分享出来的图片 URI。我自己遇到的最典型报错就是FileNotFoundException,大多因为用了 file:// URI 或者直接拼路径导致的。

5.2 Android 11 分区存储下的路径处理

Android 11 开始,系统强制开启了分区存储(Scoped Storage),应用再也不能随便读写外部存储的任意目录了。你去读/storage/emulated/0/Android/data/xxx/files/这样的路径,会被直接拒绝,即使你声明了存储权限也一样。

那从文件管理器选文件怎么处理?最稳妥的还是走 SAF(Storage Access Framework),通过ACTION_OPEN_DOCUMENT让用户主动授权文件,你用contentResolver.takePersistableUriPermission持久化权限之后,可以反复读写这个文件。项目里如果你需要支持“从文件目录选择图片”,我建议用这个方式,而不是去申请什么MANAGE_EXTERNAL_STORAGE特殊权限,那东西上架审核是很麻烦的。

5.3 权限声明与实际使用建议

如果你只是从相册选图,理论上在 Android 11 以上不需要申请任何存储权限。相册 App 本身负责读取,你的 App 只是通过 URI 接收结果。但如果你既要拍照又要选图,还要保存识别结果,那就得动态申请相机权限,并把保存操作限定在 MediaStore 允许的范围里。

我在实际项目里是这么处理的:

  • 选图:只用ACTION_OPEN_DOCUMENT,加takePersistableUriPermission
  • 拍照:申请CAMERA权限,拍完直接拿到 Bitmap,不走存储
  • 保存结果:优先写入 app 私有目录,再通过 MediaStore 迁移到公共目录

这套组合既能满足大部分业务场景,又不会被 Android 的权限机制卡住,上架审核也干净。

6. 编译、运行与问题排查实录

最后这部分,我把实际运行中遇到的高频问题挨个列出来,每个都附上我自己最后的解决方案。这里面有的是环境问题,有的是模型问题,有的是代码逻辑问题,但都真实发生过。

6.1 编译报错列表(含 tag number over 30)

我整理了这份速查表,你可以直接对照:

报错内容触发原因解决方案
tag number over 30 is not supported项目资源总数超过 Android 构建系统的单 dex 限制或资源 ID 上限开启 multiDex,清理冗余资源,减少 drawable 数量,必要时将部分资源转为文件加载
unable to find suitable Visual Studio toolchainFlutter 或 NDK 编译在 Windows 下找不到合适工具链安装对应的 Visual Studio Build Tools,或改用 Android Studio 内置的 NDK + CMake 组合
onnx2ncnn: Unsupported operator模型里有 ncnn 尚未实现的算子检查 ncnn 版本并更新到最新,或在 PaddleOCR 导出时关闭对应融合选项
E/mace: no text detected输入图片中 det 模型没有找到任何文本框降低检测阈值,或提高输入图像分辨率,检查图片是否过暗过模糊
could not create a primitivencnn 在部分 GPU 驱动上创建 Vulkan primitive 失败关闭 Vulkan 加速,回退到 CPU,或者升级 GPU 驱动

这里重点说下tag number over 30,这个报错看着很吓人,其实是 Android Gradle Plugin 对单 dex 内注册的资源 ID 数量有限制。项目如果集成了很多大库,很容易触发。解决方案是开 multiDex,并检查是否有重复的大资源。如果只是为了跑 OCR,我建议把项目里和 OCR 无关的模块先移除,资源少了自然就不报错了。

6.2 识别失败:could not create a primitive / no text detected

这两个报错放在一起说,因为它们出在同一段推理阶段。

could not create a primitive多半出现在 Vulkan 分支。ncnn 在初始化 pso cache 时会调用图形驱动创建计算管线,如果设备的 Vulkan 驱动不完整或内存不足,就报这个错。解决方法是加载模型时把net.opt.use_vulkan_compute设为 false,强制走 CPU。CPU 推理虽然慢一些,但兼容性最好,识别准确率不打折扣。

no text detected就比较耐人寻味了。它不是说模型崩溃了,而是 det 模型输出的概率图里,所有区域的置信度都低于阈值,所以连一个候选框都没生成。最常见的两个原因是:图片本身没有文字,或者预处理把图片压得太小。我在这里吃过亏:用一张 2000x1500 的合同照片,直接缩放到 320x320 后,文字全成噪点了。后来我把长边缩放到 960,检测率一下就上来了。

6.3 精度与性能调优实战

识别精度和性能是两个方向,有时候甚至是对立的,你需要根据场景做取舍。

精度优先时,我建议这样做:

  • 检测阈值从默认的 0.3 调整到 0.4 到 0.5 之间,减少文本噪声框的干扰
  • 在 rec 前对图像做一次锐化,增强笔画边缘
  • 对置信度低于 0.7 的识别结果做二次识别,输入更宽尺寸的图

性能优先时,我的做法是:

  • 把 det 输入分辨率降低到 640 边长,识别耗时能下降 40% 以上
  • rec 的输入宽度限制在 160 以内,过长的文本框只取中间区域
  • 线程数设为和 CPU 核心数一致的 4 线程,开太多反而因为锁导致性能下降
  • 打开 Vulkan 加速,但前提是设备能稳定跑通

我实测过一台骁龙 778G 的手机,默认模型参数下,全流程识别一张普通合同照片大约 800ms。把 det 输入缩小到 640、openmp 线程设为 4 之后,耗时能压到 450ms 左右。这个速度在办公扫码、文档录入场景下完全够用。

6.4 效果评估:用 IIIT5K 这类数据集心里有数

最后聊一下如何客观评估 OCR 效果。如果你只拿几张图自己看一眼“好像识别对了”,那是不够的。业内比较公认的评估数据集之一是 IIIT5K,它包含 5000 张自然场景文字图像,专门用来评测文字识别模型在“复杂背景、不同字体、不同语言混杂”下的表现。

本地评估时可以这么做:把 IIIT5K 的图片按批送入识别管线,跑完后和标注文件里的真实文本逐字比对,计算准确率。我自己用 PP-OCRv5 的 rec 模型在 IIIT5K 上跑过一轮,识别准确率能到 85% 左右,比 Tesseract 的 60% 出头强不少。这个数据不绝对,但能让你在选型时心里有底,不会信口开河说“模型准确率 99%”,保证测试集一样才算数。

如果你不追求严格的学术评估,只想验证业务场景,我建议准备一个覆盖自己领域内 50 张图片的小测试集,分别记录不同光线、不同拍摄角度、不同字号下的识别率。这样出来的结果,比数据集上的数字更能指导上线决策。

写在最后的一点私货

项目在我这边最终落地成了一个小工具,给客户拍照上传的合同和票据批量提取关键字段。从一开始在 Ubuntu 上编译 onnx2ncnn 折腾了一整天,到后来在 Android 上反复调参数,再到把 cls 置信度阈值改掉之后识别结果肉眼可见变准,这套组合用到现在,稳定性和效果都在我的预期之上。

如果你也要拿它做二次开发,我最想提醒的其实就一句话:不要在拿到自带模型能跑通之后就停止思考,模型转换链路、预处理参数、后处理阈值这三样东西,是你未来所有业务定制化的着力点。把这条管线里的每一个细节都吃透,比你多找十个开源项目都有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 13:52:40

电子元器件AI检测:YOLO与大模型协同识别范式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 13:51:57

LSTM短期光伏功率预测实战:从数据清洗到模型部署

简介:一份基于Python与LSTM的短期光伏预测算法实战资源,适合计算机、人工智能、电气及自动化方向的学生用于毕设、课设或项目初探。项目源自个人毕业设计,代码经完整测试,可结合园区数据完成光伏与负荷预测建模,覆盖单…

作者头像 李华
网站建设 2026/9/12 13:49:14

Raspberry Pi Pico入门:MicroPython控制GPIO与PWM实战

1. 项目概述:为什么从 Pico 和 MicroPython 开始学硬件开发?如果你最近翻过电子爱好者论坛、刷过 B 站硬件区,或者在淘宝搜过“入门单片机”,大概率会撞见 Raspberry Pi Pico 这块巴掌大的小板子——它不靠性能堆料,不…

作者头像 李华
网站建设 2026/9/12 13:48:11

LLM推理优化实战:从KV缓存到FlashAttention的硬核调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华