ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PaddleOCR转ONNX跨语言部署实战:Java/C++集成与性能优化

PaddleOCR转ONNX跨语言部署实战:Java/C++集成与性能优化 1. 为什么要把 PaddleOCR 转成 ONNX1.1 一个真实的需求场景上个月帮朋友处理一个车牌识别的项目他那边是 Java 技术栈服务器上跑的是 Spring Boot整个团队没有一个人碰过 Python 深度学习框架。最初的想法是直接调 PaddleOCR 的 Python 服务用 HTTP 接口通信但实测下来延迟太高一张图从请求到返回要 300ms 以上而且多了一个服务进程要维护运维同事直接摇头。后来换了个思路把 PaddleOCR 的模型转成 ONNX 格式用 ONNX Runtime 的 Java API 直接加载推理。改完之后单张图片推理时间降到 80ms 左右而且整个推理逻辑就封装在一个工具类里不需要额外部署 Python 环境。这个方案后来在好几个项目里复用包括一个需要做人物抠图的场景也是同样的思路——模型转 ONNX然后用对应语言的 Runtime 加载。PaddleOCR 本身是个非常优秀的开源 OCR 工具中文识别效果在开源方案里属于第一梯队。但它原生是 Python 生态对于 Java、C#、C 这些技术栈的团队来说直接集成并不方便。转成 ONNX 之后模型就变成了一个通用的中间格式任何支持 ONNX Runtime 的语言都能加载这就解决了跨语言部署的核心痛点。1.2 ONNX 到底解决了什么问题ONNX 的全称是 Open Neural Network Exchange翻译过来就是开放神经网络交换格式。你可以把它理解成深度学习模型界的 PDF——不管你是用什么框架训练的模型PyTorch 也好PaddlePaddle 也好TensorFlow 也好都可以导出成 ONNX 格式。导出之后这个模型就不再依赖原来的训练框架了任何支持 ONNX 的推理引擎都能跑。这个特性带来的好处非常直接。第一是部署环境简化原来要装 PaddlePaddle 那一整套东西现在只需要一个 ONNX Runtime 的库体积小很多。第二是跨语言支持ONNX Runtime 官方提供了 Python、Java、C、C#、JavaScript 等多个语言的 API你用什么语言开发就用什么语言的接口。第三是推理性能优化ONNX Runtime 内部做了大量的图优化和算子融合在 CPU 上的推理速度往往比原生框架还要快一些。对于中文 OCR 这个场景来说PaddleOCR 的 PP-OCRv 系列模型转 ONNX 之后在 ONNX Runtime 上的推理效果和原生 Paddle 推理基本一致精度损失可以忽略不计。这也是为什么现在很多生产环境都选择这条路线。1.3 适合哪些人参考这篇文章主要面向几类读者。第一类是 Java 或 C# 后端开发需要在服务端集成 OCR 能力但不想引入 Python 依赖的。第二类是 C 开发需要在桌面端或嵌入式设备上跑 OCR 的。第三类是运维或架构师在评估 OCR 部署方案时想了解 ONNX 路线的可行性。第四类是对模型转换和推理优化感兴趣的算法工程师。即使你之前没接触过 ONNX只要会用 Python 装包、能看懂基本的命令行操作跟着走一遍就能跑通。我会把每一步的命令和参数都写清楚包括踩过的坑和对应的解决办法。2. 环境准备与模型转换全流程2.1 安装 PaddleOCR 和 Paddle2ONNX第一步是准备 Python 环境。我建议用 conda 创建一个独立的环境避免和系统里的其他包冲突。Python 版本选 3.8 到 3.10 之间比较稳太新的版本有些依赖包还没跟上。conda create -n paddle2onnx python3.9 conda activate paddle2onnx接下来安装 PaddlePaddle。这里要注意如果你有 GPU 并且想用 GPU 做转换其实转换过程用 CPU 就够了可以装 GPU 版本。但纯粹为了转模型的话CPU 版本完全够用而且安装简单很多。# CPU 版本转换模型够用了 pip install paddlepaddle2.5.2 # 如果需要 GPU 版本 # pip install paddlepaddle-gpu2.5.2然后安装 PaddleOCR 和 Paddle2ONNXpip install paddleocr2.7.0.3 pip install paddle2onnx1.0.5这里有个版本兼容性的坑要提醒一下。PaddleOCR 2.7 和 Paddle2ONNX 1.0.5 这个组合是我实测下来最稳的。如果你装最新版有时候会遇到算子不支持的问题转换过程中报错。如果遇到转换失败优先考虑降版本。2.2 下载 PP-OCRv4 推理模型PaddleOCR 提供了多个版本的模型目前中文场景下效果最好的是 PP-OCRv4。它包含三个部分检测模型det、方向分类模型cls、识别模型rec。检测模型负责找出图片里文字的位置方向分类模型判断文字有没有旋转180度识别模型把文字内容读出来。官方提供了推理模型的直接下载地址不需要自己训练。我用的是以下三个# 下载检测模型 wget https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_det_infer.tar tar -xf ch_PP-OCRv4_det_infer.tar # 下载识别模型 wget https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_rec_infer.tar tar -xf ch_PP-OCRv4_rec_infer.tar # 下载方向分类模型 wget https://paddleocr.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar tar -xf ch_ppocr_mobile_v2.0_cls_infer.tar下载解压后每个目录里会有三个文件inference.pdmodel模型结构、inference.pdiparams模型权重、inference.pdiparams.info参数信息。转 ONNX 只需要前两个。注意如果你只需要识别横向文字方向分类模型可以不用。但实际项目里手机拍的照片经常有旋转加上这个模型能显著提升识别率建议还是转上。2.3 用 Paddle2ONNX 命令行转换转换命令本身不复杂关键是参数要对。Paddle2ONNX 的命令行工具用法如下# 转换检测模型 paddle2onnx --model_dir ch_PP-OCRv4_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file det_model.onnx \ --opset_version 11 \ --enable_onnx_checker True # 转换识别模型 paddle2onnx --model_dir ch_PP-OCRv4_rec_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file rec_model.onnx \ --opset_version 11 \ --enable_onnx_checker True # 转换方向分类模型 paddle2onnx --model_dir ch_ppocr_mobile_v2.0_cls_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file cls_model.onnx \ --opset_version 11 \ --enable_onnx_checker True几个参数解释一下。--opset_version 11是 ONNX 的算子集版本选 11 是因为兼容性好ONNX Runtime 1.8 以上都支持。如果你用的 Runtime 版本很新可以选 12 或 13但没必要。--enable_onnx_checker True会在转换后自动校验模型合法性建议开着能提前发现问题。转换成功后你会得到三个 .onnx 文件。检测模型大概 4.7MB识别模型大概 10MB方向分类模型大概 1.4MB。加起来不到 20MB比原来的 Paddle 模型小一些。2.4 转换后的模型校验转完不能直接用得先验证模型是不是好的。ONNX Runtime 提供了 Python API可以快速跑一下import onnxruntime as ort import numpy as np # 加载检测模型 sess ort.InferenceSession(det_model.onnx) input_name sess.get_inputs()[0].name print(输入名称:, input_name) print(输入形状:, sess.get_inputs()[0].shape) print(输出名称:, [o.name for o in sess.get_outputs()]) print(输出形状:, [o.shape for o in sess.get_outputs()]) # 构造一个假输入测试 dummy_input np.random.randn(1, 3, 640, 640).astype(np.float32) outputs sess.run(None, {input_name: dummy_input}) print(输出数量:, len(outputs)) for i, out in enumerate(outputs): print(f输出{i}形状:, out.shape)如果这一步能正常打印出输入输出信息说明模型转换没问题。检测模型的输入是[1, 3, H, W]H 和 W 是动态的可以是 32 的倍数。识别模型的输入是[1, 3, 48, W]高度固定 48宽度动态。方向分类模型的输入是[1, 3, 48, 192]都是固定的。实操心得如果转换时报 Unsupported operator 之类的错误大概率是 Paddle2ONNX 版本和 PaddleOCR 版本不匹配。我遇到过 PP-OCRv4 的某个算子在新版 Paddle2ONNX 里被改了名字降回 1.0.5 就好了。另外如果模型里有自定义算子转换会失败这种情况只能改模型结构或者用 Paddle Inference 原生推理。3. ONNX Runtime 推理部署实战3.1 Python 端完整推理代码先用 Python 把整个流程跑通理解每一步在做什么后面移植到 Java 或 C 就清楚了。完整的推理流程分四步预处理、检测、后处理、识别。import cv2 import numpy as np import onnxruntime as ort from shapely.geometry import Polygon import pyclipper class PaddleOCROnnx: def __init__(self, det_path, rec_path, cls_pathNone): self.det_sess ort.InferenceSession(det_path, providers[CPUExecutionProvider]) self.rec_sess ort.InferenceSession(rec_path, providers[CPUExecutionProvider]) self.cls_sess ort.InferenceSession(cls_path, providers[CPUExecutionProvider]) if cls_path else None # 识别模型的字符字典需要从 PaddleOCR 的 ppocr_keys_v1.txt 加载 with open(ppocr_keys_v1.txt, r, encodingutf-8) as f: self.characters [blank] [line.strip() for line in f.readlines()] def preprocess_det(self, img, limit_side_len960): 检测模型预处理等比缩放 归一化 h, w img.shape[:2] ratio 1.0 if max(h, w) limit_side_len: ratio limit_side_len / max(h, w) resize_h int(h * ratio / 32) * 32 resize_w int(w * ratio / 32) * 32 resized cv2.resize(img, (resize_w, resize_h)) # 归一化mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] img_f resized.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) img_f (img_f - mean) / std # HWC - CHW - NCHW img_f img_f.transpose(2, 0, 1)[np.newaxis, ...] return img_f.astype(np.float32), (h, w), (resize_h, resize_w) def postprocess_det(self, output, orig_shape, resize_shape, thresh0.3): 检测后处理从概率图提取文本框 pred output[0][0, 0] # [H, W] h, w orig_shape rh, rw resize_shape # 二值化 bitmap (pred thresh).astype(np.uint8) # 这里省略了 DB 后处理的完整实现膨胀、轮廓提取、框筛选 # 完整代码见文末仓库 boxes self._extract_boxes(bitmap, rh, rw, h, w) return boxes def preprocess_rec(self, img): 识别模型预处理高度固定48宽度等比缩放 h, w img.shape[:2] ratio 48.0 / h new_w int(w * ratio) resized cv2.resize(img, (new_w, 48)) img_f resized.astype(np.float32) / 255.0 img_f (img_f - 0.5) / 0.5 img_f img_f.transpose(2, 0, 1)[np.newaxis, ...] return img_f.astype(np.float32) def decode_rec(self, output): CTC 解码 preds output[0] # [1, T, num_classes] preds_idx preds.argmax(axis2)[0] result [] prev -1 for idx in preds_idx: if idx ! prev and idx ! 0: result.append(self.characters[idx]) prev idx return .join(result) def __call__(self, img_path): img cv2.imread(img_path) # 检测 det_input, orig_shape, resize_shape self.preprocess_det(img) det_out self.det_sess.run(None, {self.det_sess.get_inputs()[0].name: det_input}) boxes self.postprocess_det(det_out, orig_shape, resize_shape) # 识别 results [] for box in boxes: crop self._crop_rect(img, box) rec_input self.preprocess_rec(crop) rec_out self.rec_sess.run(None, {self.rec_sess.get_inputs()[0].name: rec_input}) text self.decode_rec(rec_out) results.append((box, text)) return results这段代码里最复杂的是检测后处理。DB 算法的后处理包括概率图二值化、膨胀、轮廓提取、最小外接矩形计算、框筛选按面积和长宽比过滤。完整实现大概 100 行核心逻辑是先用cv2.findContours找轮廓然后用cv2.minAreaRect得到旋转矩形最后用pyclipper做多边形偏移。注意识别模型的字符字典文件ppocr_keys_v1.txt在 PaddleOCR 的 GitHub 仓库里可以找到大概 6600 多个字符。这个文件必须和模型匹配用错了字典识别出来就是乱码。如果你遇到 paddleocr文字识别乱码 的问题九成是字典文件不对或者编码没处理好。3.2 Java 端集成方案Java 端用 ONNX Runtime 的 Java APIMaven 依赖如下dependency groupIdcom.microsoft.onnxruntime/groupId artifactIdonnxruntime/artifactId version1.16.3/version /dependencyJava 端的推理逻辑和 Python 完全一致只是 API 调用方式不同。核心代码结构import ai.onnxruntime.*; import org.opencv.core.*; import org.opencv.imgproc.Imgproc; public class OcrOnnxEngine { private OrtEnvironment env; private OrtSession detSession; private OrtSession recSession; private ListString characters; public OcrOnnxEngine(String detPath, String recPath, String dictPath) throws OrtException, IOException { env OrtEnvironment.getEnvironment(); OrtSession.SessionOptions opts new OrtSession.SessionOptions(); opts.setIntraOpNumThreads(4); opts.setOptimizationLevel( OrtSession.SessionOptions.OptLevel.ALL_OPT); detSession env.createSession(detPath, opts); recSession env.createSession(recPath, opts); characters new ArrayList(); characters.add(blank); try (BufferedReader br new BufferedReader( new InputStreamReader(new FileInputStream(dictPath), StandardCharsets.UTF_8))) { String line; while ((line br.readLine()) ! null) { characters.add(line.trim()); } } } public float[] preprocessDet(Mat img, int limitSideLen) { int h img.rows(), w img.cols(); float ratio 1.0f; if (Math.max(h, w) limitSideLen) { ratio (float) limitSideLen / Math.max(h, w); } int resizeH (int)(h * ratio / 32) * 32; int resizeW (int)(w * ratio / 32) * 32; Mat resized new Mat(); Imgproc.resize(img, resized, new Size(resizeW, resizeH)); resized.convertTo(resized, CvType.CV_32FC3, 1.0/255.0); // 归一化并转 CHW float[] mean {0.485f, 0.456f, 0.406f}; float[] std {0.229f, 0.224f, 0.225f}; float[] data new float[3 * resizeH * resizeW]; // ... 逐像素处理省略具体循环 return data; } public ListTextBox detect(Mat img) throws OrtException { float[] input preprocessDet(img, 960); long[] shape {1, 3, -1, -1}; OnnxTensor tensor OnnxTensor.createTensor(env, FloatBuffer.wrap(input), shape); MapString, OnnxTensor inputs Collections.singletonMap( detSession.getInputNames().iterator().next(), tensor); OrtSession.Result result detSession.run(inputs); // 后处理提取文本框 return postprocessDet(result, img); } }Java 端有几个地方要注意。第一是OrtSession.SessionOptions的线程数设置setIntraOpNumThreads控制算子内部并行度一般设成 CPU 核心数就行。第二是setOptimizationLevel建议开ALL_OPTONNX Runtime 会自动做算子融合和常量折叠。第三是输入张量的形状检测模型的高度和宽度是动态的Java 里用 -1 表示动态维度。实操心得Java 端处理图像建议用 OpenCV 的 Java 绑定比用 ImageIO 方便很多。OpenCV 的 Mat 转 float 数组时要注意通道顺序OpenCV 默认是 BGR而模型训练时用的是 RGB需要做一次通道交换。这个坑我踩过不换通道识别率会明显下降。3.3 性能优化与量化如果对推理速度有更高要求可以考虑 INT8 量化。ONNX Runtime 提供了动态量化的工具from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_inputrec_model.onnx, model_outputrec_model_int8.onnx, weight_typeQuantType.QInt8 )动态量化会把模型权重从 FP32 转成 INT8模型体积缩小到原来的四分之一左右推理速度在 CPU 上能提升 30% 到 50%。但精度会有一定损失实测识别准确率大概下降 1 到 2 个百分点。对于大部分场景够用了如果对精度要求极高建议用静态量化需要提供校准数据集。另一个优化点是输入尺寸。检测模型的limit_side_len默认是 960如果图片本身不大可以调小到 640 甚至 480速度会快很多。识别模型的宽度是动态的短文本的推理速度比长文本快。优化手段速度提升精度影响适用场景INT8 动态量化30%-50%下降 1-2%对速度敏感、精度要求不极端减小检测输入尺寸20%-40%小字可能漏检图片分辨率不高多线程推理接近线性无批量处理GPU 推理5-10 倍无有 GPU 且批量大3.4 批量推理与并发处理生产环境往往是批量处理图片这时候单张推理的效率不够。ONNX Runtime 支持批处理检测模型可以把多张图拼成一个 batch但要求尺寸一致。实际做法是先把图片 resize 到相同尺寸再拼 batch。识别模型更适合批处理因为高度固定 48只需要把宽度 padding 到最大值就行。我实测过batch size 设为 8 的时候吞吐量比单张推理提升 3 倍左右。并发方面ONNX Runtime 的 Session 是线程安全的多个线程可以共享同一个 Session 做推理。但要注意如果开了setIntraOpNumThreads多个线程同时推理会争抢 CPU 资源反而变慢。建议的做法是要么单线程推理但开多线程算子要么多线程推理但每个线程用独立的 Session 且算子线程设为 1。// 多线程推理配置 OrtSession.SessionOptions opts new OrtSession.SessionOptions(); opts.setIntraOpNumThreads(1); // 每个 Session 单线程 opts.setInterOpNumThreads(1); // 每个线程创建独立的 Session4. 常见问题排查与避坑指南4.1 模型转换阶段的典型报错转换阶段最常见的问题是算子不支持。PaddlePaddle 有一些自定义算子ONNX 标准里没有对应的实现转换时就会报错。比如 PP-OCRv4 的检测模型里有个deformable_conv算子老版本的 Paddle2ONNX 不支持需要升级到 1.0.5 以上。另一个常见问题是版本不匹配。PaddleOCR 2.6 和 Paddle2ONNX 1.0.0 组合会有问题转出来的模型在 ONNX Runtime 里加载报错。我的建议是锁定版本组合PaddleOCR 2.7.0.3 Paddle2ONNX 1.0.5 ONNX Runtime 1.16.x这个组合我用了大半年没出过问题。如果转换后的模型推理结果和 Paddle 原生推理不一致先检查预处理。PaddleOCR 的预处理有特定的归一化参数检测模型是 mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]识别模型是 mean0.5、std0.5。用错了归一化参数输出会完全不对。4.2 推理阶段的精度问题识别结果乱码是最常见的问题。原因通常有三个字典文件不对、字典编码不对、CTC 解码逻辑有误。字典文件必须和模型训练时用的一致PP-OCRv4 用的是ppocr_keys_v1.txt。编码必须是 UTF-8如果用 GBK 读会乱码。CTC 解码要注意去重和去 blank连续相同的字符只保留一个blank索引 0要跳过。检测框漏检或误检也经常遇到。漏检通常是因为thresh设得太高默认 0.3 可以调到 0.2。误检是因为框筛选条件太松可以加长宽比过滤比如宽高比大于 10 的框直接丢弃。另外检测模型对竖排文字的检测效果一般如果场景里有竖排文字需要额外处理。方向分类模型判断错误的情况比较少但如果遇到可以调低阈值。方向分类的输出是两个分数分别对应 0 度和 180 度取较大值。如果两个分数接近说明模型不确定这时候可以结合业务逻辑判断比如车牌号不会倒着写。4.3 部署环境的兼容性问题ONNX Runtime 在不同平台上的表现有差异。Windows 上 CPU 推理没问题但 GPU 推理需要 CUDA 和 cuDNN 版本匹配。Linux 上相对省心Docker 镜像里装好依赖就行。ARM 平台比如树莓派需要用 ONNX Runtime 的 ARM 版本性能比 x86 差不少但跑 PP-OCRv4 mobile 模型还是能用的。Java 端有个坑是本地库加载。ONNX Runtime 的 Java 包依赖本地动态库Maven 会自动下载对应平台的库但如果部署环境是 Alpine Linux 这种用 musl libc 的系统需要手动指定。解决办法是用onnxruntime的-linux-x64分类器或者换成 glibc 的基础镜像。内存占用方面三个模型加载后大概占 200MB 左右内存推理时峰值会到 500MB。如果并发量高建议限制并发数或者用模型池。ONNX Runtime 的 Session 创建比较耗时建议在应用启动时创建好复用 Session 而不是每次请求都新建。4.4 常见问题速查表问题现象可能原因排查方法解决方案转换时报 Unsupported operator算子不支持看报错里的算子名升级 Paddle2ONNX 或换模型版本模型加载失败版本不匹配检查 ONNX Runtime 版本锁定版本组合识别结果乱码字典不对对比字典文件用官方 ppocr_keys_v1.txt检测框漏检阈值太高调低 thresh 测试thresh 从 0.3 降到 0.2推理速度慢输入尺寸太大打印输入形状减小 limit_side_lenJava 端报 UnsatisfiedLinkError本地库缺失检查系统架构换对应平台的依赖内存持续增长Session 未释放检查 Session 生命周期复用 Session避免重复创建GPU 推理报错CUDA 版本不匹配检查 CUDA 和 cuDNN 版本按官方文档匹配版本4.5 几个实用的调试技巧第一个技巧是可视化中间结果。检测模型的输出是一张概率图可以把它保存成图片看正常的话文字区域应该是高亮白色背景是黑色。如果概率图全黑或者全白说明预处理有问题。第二个技巧是单步对比。用同一张图片分别跑 Paddle 原生推理和 ONNX 推理对比检测框坐标和识别文本。如果检测框差很多问题在检测模型如果检测框一致但文本不同问题在识别模型或解码逻辑。第三个技巧是构造简单测试用例。用一张纯白背景、黑色大字的图片测试这种图片检测和识别都应该很容易。如果这种图都识别不对说明流程有根本性问题。然后再逐步增加难度比如加背景、加旋转、加模糊。实操心得调试 OCR 的时候我习惯先把检测和识别分开测。检测单独跑把框画在图上保存下来看。识别单独跑用裁剪好的文字图片测。这样能快速定位问题出在哪一环。另外ONNX Runtime 的日志级别可以调设成 VERBOSE 能看到算子执行情况排查性能问题很有用。5. 从车牌识别到通用 OCR 的扩展思路5.1 车牌识别场景的特殊处理车牌识别和通用 OCR 有几个不同点。第一是车牌文字排列规整检测框基本是水平矩形不需要复杂的后处理。第二是车牌字符集小只有汉字、字母和数字可以针对性地优化识别模型。第三是车牌有固定格式可以用规则做后校验。实际项目中我用 PP-OCRv4 的检测模型定位车牌区域然后用专门训练的车牌识别模型做识别。识别模型可以基于 PP-OCRv4 的识别网络微调把字符集缩小到 70 个左右这样模型更小、速度更快、精度更高。微调需要准备车牌数据集大概几千张就能有不错的效果。如果不想训练模型直接用通用识别模型也能用但要注意后处理。车牌的第一个字符是汉字后面是字母数字可以用正则表达式做校验和纠错。比如识别出 京A12345如果第一个字不是省份简称可以按相似度替换。5.2 人物抠图场景的 ONNX 实践人物抠图背景移除和 OCR 虽然任务不同但部署思路完全一样。RMBG-2.0 是个效果很好的人物抠图模型同样可以转 ONNX 然后用 ONNX Runtime 推理。Java 端集成方式和 OCR 一模一样只是预处理和后处理不同。抠图模型的输入是[1, 3, 1024, 1024]输出是[1, 1, 1024, 1024]的 alpha 掩码。预处理是 resize 加归一化后处理是把掩码 resize 回原图尺寸然后和原图做 alpha 混合。整个流程比 OCR 简单因为没有检测和后处理解码的复杂度。这个思路可以推广到很多视觉任务只要模型能转 ONNX就能用 ONNX Runtime 在任意语言里推理。分类、检测、分割、超分套路都一样。关键是把预处理和后处理用目标语言实现一遍中间推理部分交给 ONNX Runtime。5.3 模型版本升级的注意事项PaddleOCR 从 2.x 升级到 3.x 的时候模型结构有变化转换脚本也要跟着改。PP-OCRv6 是最新版本识别效果比 v4 更好但模型也更大。如果是从 v4 升级到 v6需要注意几点输入尺寸可能变了字典文件可能更新了后处理逻辑可能有调整。升级前建议先做 A/B 测试用同一批测试图片对比新旧版本的识别率和速度。如果新版本提升不明显但速度下降很多就没必要升级。另外ONNX 模型的版本管理要做好不同版本的模型文件命名要区分开避免部署时搞混。实操心得模型文件建议用 Git LFS 或者对象存储管理不要直接提交到代码仓库。ONNX 文件虽然不大但二进制文件在 Git 里 diff 很不友好。我一般会在文件名里带上版本号和日期比如rec_model_v4_20240115.onnx这样回滚的时候很清楚。5.4 端侧部署的可行性分析ONNX Runtime 也支持移动端和嵌入式端。Android 和 iOS 都有对应的包模型可以打包进 App 里离线推理。不过端侧算力有限PP-OCRv4 的 mobile 模型在手机上跑一张图大概 200 到 500ms比服务端慢不少但离线场景够用了。如果端侧性能不够可以考虑模型裁剪。检测模型可以换成更小的版本识别模型可以减小宽度。另外端侧可以用 NCNN 或 MNN 这类专门为移动端优化的推理引擎它们对 ONNX 模型的支持也不错性能比 ONNX Runtime 在 ARM 上更好。选择推理引擎的时候要考虑生态。ONNX Runtime 的优势是跨平台一致性好同一套代码在服务端和端侧都能跑。NCNN 和 MNN 性能更好但生态相对小遇到问题查资料没那么方便。如果团队没有特别的性能要求ONNX Runtime 是更稳妥的选择。5.5 完整代码仓库结构最后说一下代码组织。我一般会把项目分成几个模块模型转换脚本、Python 推理参考实现、Java 推理实现、测试用例。模型转换脚本单独放因为只需要跑一次。Python 实现用来做基准测试和调试。Java 实现是生产用的。测试用例包含各种场景的图片和预期结果。目录结构大概是这样paddleocr-onnx/ ├── convert/ │ ├── download_models.sh │ └── convert_to_onnx.sh ├── models/ │ ├── det_model.onnx │ ├── rec_model.onnx │ ├── cls_model.onnx │ └── ppocr_keys_v1.txt ├── python/ │ ├── ocr_onnx.py │ └── test_ocr.py ├── java/ │ ├── pom.xml │ └── src/main/java/com/example/ocr/ │ ├── OcrOnnxEngine.java │ ├── DetPostProcess.java │ └── RecDecoder.java └── test_images/ ├── sample1.jpg └── sample2.jpg这套结构我在三个项目里用过维护起来很清晰。模型文件和代码分离换模型不用改代码。Python 和 Java 实现并存方便对比验证。测试图片固定每次改代码跑一遍就知道有没有回归。整个方案从转换到部署熟练的话半天就能搞定。核心难点在检测后处理的实现那部分代码量最大。但只要照着 PaddleOCR 的 Python 源码翻译一遍逻辑是清楚的。我建议先把 Python 版本跑通确认模型转换没问题再移植到目标语言。这样出问题的时候容易定位是模型的问题还是代码的问题。
返回列表