ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PaddleOCR转ONNX部署实战:从模型转换到推理优化

PaddleOCR转ONNX部署实战:从模型转换到推理优化 1. 为什么要把PaddleOCR转成ONNX做过OCR推理部署的人多半都经历过这样的场景算法同事丢过来一个PaddleOCR的训练模型拍着肩膀说“效果很好你部署一下”然后你打开服务器一看生产环境是Java技术栈或者是一台没有装PaddlePaddle的ARM边缘盒子甚至是一个只支持ONNX Runtime的推理服务框架。这时候把PaddleOCR转成ONNX就成了绕不开的一步。PaddleOCR本身推理能力很强PP-OCRv系列模型在中文场景下的识别精度和速度平衡做得相当不错尤其是PP-OCRv5、PP-OCRv6这几代检测和识别模型都做了大量结构优化。但它的推理依赖PaddlePaddle框架这在Python环境下问题不大一旦跨语言、跨平台就会遇到各种麻烦。ONNX作为开放的模型交换格式几乎成了推理部署领域的“通用货币”ONNX Runtime在Windows、Linux、ARM、x86上都有成熟的运行时支持Java、C、C#、Python都能调用。把PaddleOCR转成ONNX本质上是用一次转换的代价换取后续部署的极大自由度。这篇文章面向的是需要把中文OCR能力落地到实际项目里的工程师不管你是做车牌识别、票据识别、文档数字化还是工业质检里的字符检测只要涉及PaddleOCR的推理部署这套流程都能直接参考。我会从模型结构拆解开始讲到转换的具体操作、ONNX Runtime的推理代码、量化压缩再到实际部署中踩过的坑尽量把每个环节的“为什么”说清楚。2. PaddleOCR模型结构拆解与转换前的准备2.1 检测模型和识别模型是两套独立的东西很多人刚开始接触PaddleOCR时会有一个误解以为它是一个端到端的模型输入图片直接输出文字。实际上PaddleOCR的推理流程是两阶段先由检测模型Det找出图片中文字区域的位置把每个文本框裁剪出来再由识别模型Rec对每个文本框做文字识别。这两个模型是独立训练、独立推理的转换ONNX时也要分别转换。检测模型常用的有PP-OCRv5的检测网络骨干是MobileNetV3或者ResNet的变体输出是概率图表示每个像素属于文字区域的概率。识别模型则是CRNN加CTC的结构输入是裁剪后的文本行图片输出是字符序列。理解这个结构很重要因为转换时两个模型的输入输出形状、动态轴设置都不一样后面会详细说。2.2 环境准备PaddlePaddle和Paddle2ONNX转换工作需要在Python环境下完成核心依赖是两个包PaddlePaddle和Paddle2ONNX。PaddlePaddle用来加载推理模型Paddle2ONNX负责把模型图转成ONNX格式。安装命令很直接pip install paddlepaddle2.6.0 pip install paddle2onnx1.0.6 pip install onnx1.15.0 pip install onnxruntime1.17.0这里有几个版本选择的经验。PaddlePaddle的版本要和你的模型版本匹配如果你用的是PP-OCRv5的推理模型建议PaddlePaddle用2.5以上。Paddle2ONNX的版本也很关键1.0.x系列对PP-OCR系列的支持比较稳定太老的版本可能不支持某些算子。ONNX的版本影响opset一般用opset 11或12就够了ONNX Runtime 1.17对这两个opset支持都很好。如果你有GPU可以装paddlepaddle-gpu转换过程本身用CPU就够了但如果你要在转换后做精度验证GPU会快很多。不过要注意转换出来的ONNX模型是否能用GPU推理取决于你部署环境里的ONNX Runtime是不是GPU版本和转换过程无关。2.3 下载PaddleOCR推理模型PaddleOCR官方提供了训练好的推理模型直接下载就行。以PP-OCRv5中文模型为例# 检测模型 wget https://paddleocr.bj.bcebos.com/PP-OCRv5/chinese/ch_PP-OCRv5_det_infer.tar tar -xf ch_PP-OCRv5_det_infer.tar # 识别模型 wget https://paddleocr.bj.bcebos.com/PP-OCRv5/chinese/ch_PP-OCRv5_rec_infer.tar tar -xf ch_PP-OCRv5_rec_infer.tar解压后你会看到每个目录下有四个文件inference.pdmodel、inference.pdiparams、inference.pdiparams.info和inference.pdmodel。其中.pdmodel是模型结构.pdiparams是权重参数.info文件在转换时用不到。这两个模型加起来大概十几兆体积很小非常适合部署。注意下载模型时确认一下你用的是哪个版本的PP-OCRv5和v6的模型结构有差异转换时的参数设置也不同。如果你拿的是v6模型建议Paddle2ONNX用1.1以上的版本。3. 动手转换从Paddle模型到ONNX3.1 检测模型转换的完整命令与参数解读检测模型的转换命令如下paddle2onnx \ --model_dir ch_PP-OCRv5_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file det_model.onnx \ --opset_version 11 \ --enable_onnx_checker True \ --input_shape_dict {x: [1, 3, -1, -1]}逐条解释这些参数。--model_dir指向解压后的模型目录--model_filename和--params_filename分别指定结构和权重文件。--save_file是输出的ONNX文件名。--opset_version 11指定ONNX的算子集版本选11是因为它兼容性好ONNX Runtime和TensorRT都支持。最关键的是--input_shape_dict。检测模型的输入是[N, C, H, W]其中N是batch sizeC是通道数3H和W是图片高宽。这里写成[1, 3, -1, -1]意思是batch固定为1高宽动态。为什么高宽要动态因为实际推理时输入图片尺寸各不相同如果固定了尺寸每张图都要resize到同一大小会影响检测精度。设成动态后ONNX Runtime可以根据实际输入调整。但动态轴也有代价。某些推理后端对动态形状支持不好比如一些NPU或特定版本的TensorRT遇到动态轴可能需要额外配置。如果你确定部署环境输入尺寸固定比如车牌识别场景图片都是固定角度和距离拍摄的那可以把H和W也固定写成[1, 3, 640, 640]这样推理速度会更快。3.2 识别模型转换的特殊处理识别模型的转换命令类似但输入形状不同paddle2onnx \ --model_dir ch_PP-OCRv5_rec_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file rec_model.onnx \ --opset_version 11 \ --enable_onnx_checker True \ --input_shape_dict {x: [1, 3, 48, -1]}识别模型的输入高度固定为48这是PP-OCR系列的标准输入高度。宽度设为动态因为不同文本行的长度不一样。宽度动态是识别模型转换的关键如果宽度也固定长文本会被截断短文本会补零都会影响识别效果。转换完成后可以用Netron打开ONNX文件看看结构。Netron是一个模型可视化工具直接拖进去就能看到计算图。重点检查输入输出的名字和形状后面写推理代码时要用到。检测模型的输出通常叫sigmoid_0.tmp_0之类的名字识别模型的输出叫softmax_0.tmp_0不同版本可能略有差异以Netron里看到的为准。3.3 转换后的精度验证转换完不能直接就用一定要验证精度。方法是用同一张图片分别用PaddleOCR原模型和ONNX模型推理对比结果。import onnxruntime as ort import numpy as np import cv2 # 加载ONNX模型 sess ort.InferenceSession(det_model.onnx) input_name sess.get_inputs()[0].name # 准备输入 img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img img[:, :, ::-1].astype(np.float32) / 255.0 img np.transpose(img, (2, 0, 1))[np.newaxis, ...] # 推理 output sess.run(None, {input_name: img}) print(output[0].shape)把ONNX的输出和PaddleOCR原模型的输出做对比如果数值差异在1e-4以内说明转换没问题。如果差异很大通常是opset版本或者算子支持的问题可以尝试换opset版本重新转。实操心得转换检测模型时如果遇到HardSwish或HardSigmoid算子报错把opset_version调到11以上通常能解决。这两个算子在MobileNetV3里很常见opset 10以下不支持。4. ONNX Runtime推理部署实战4.1 Python端推理检测识别完整流程先看Python端的完整推理代码这是验证模型是否好用的最快方式。import cv2 import numpy as np import onnxruntime as ort class PaddleOCR_ONNX: def __init__(self, det_model_path, rec_model_path): self.det_sess ort.InferenceSession(det_model_path) self.rec_sess ort.InferenceSession(rec_model_path) self.det_input self.det_sess.get_inputs()[0].name self.rec_input self.rec_sess.get_inputs()[0].name def preprocess_det(self, img): h, w img.shape[:2] # 调整到32的倍数 new_h int(np.ceil(h / 32) * 32) new_w int(np.ceil(w / 32) * 32) resized cv2.resize(img, (new_w, new_h)) # 归一化 input_data resized[:, :, ::-1].astype(np.float32) / 255.0 input_data (input_data - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] input_data np.transpose(input_data, (2, 0, 1))[np.newaxis, ...] return input_data.astype(np.float32), (h, w), (new_h, new_w) def detect(self, img): input_data, orig_shape, new_shape self.preprocess_det(img) output self.det_sess.run(None, {self.det_input: input_data})[0] # 后处理阈值化轮廓提取 prob_map output[0, 0] binary (prob_map 0.3).astype(np.uint8) * 255 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if w 3 or h 3: continue # 映射回原图坐标 scale_x orig_shape[1] / new_shape[1] scale_y orig_shape[0] / new_shape[0] boxes.append([int(x*scale_x), int(y*scale_y), int((xw)*scale_x), int((yh)*scale_y)]) return boxes这段代码里检测的后处理做了简化实际PaddleOCR的后处理还包括DBDifferentiable Binarization的阈值处理和文本框扩张但核心逻辑就是这样。识别部分需要把检测到的每个框裁剪出来resize到高度48宽度按比例缩放。def preprocess_rec(self, img): h, w img.shape[:2] ratio 48.0 / h new_w int(w * ratio) resized cv2.resize(img, (new_w, 48)) input_data resized[:, :, ::-1].astype(np.float32) / 255.0 input_data (input_data - 0.5) / 0.5 input_data np.transpose(input_data, (2, 0, 1))[np.newaxis, ...] return input_data.astype(np.float32) def recognize(self, img, boxes): results [] for box in boxes: x1, y1, x2, y2 box crop img[y1:y2, x1:x2] if crop.size 0: continue input_data self.preprocess_rec(crop) output self.rec_sess.run(None, {self.rec_input: input_data})[0] # CTC解码 text self.ctc_decode(output) results.append((box, text)) return resultsCTC解码是识别模型后处理的核心。识别模型的输出形状是[1, T, num_classes]T是时间步数num_classes是字符集大小。解码时取每个时间步的最大概率索引去掉重复和blank通常是索引0再映射到字符。4.2 Java端部署ONNX Runtime Java API很多企业项目是Java技术栈ONNX Runtime提供了Java API可以直接调用。Maven依赖dependency groupIdcom.microsoft.onnxruntime/groupId artifactIdonnxruntime/artifactId version1.17.0/version /dependencyJava端的推理代码结构和Python类似核心是构造OrtSession和OnnxTensorOrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession.SessionOptions opts new OrtSession.SessionOptions(); OrtSession session env.createSession(det_model.onnx, opts); float[] inputData preprocess(image); long[] shape {1, 3, height, width}; OnnxTensor tensor OnnxTensor.createTensor(env, FloatBuffer.wrap(inputData), shape); OrtSession.Result result session.run(Collections.singletonMap(x, tensor)); float[][] output (float[][]) result.get(0).getValue();Java端做图像预处理比Python麻烦一些需要用BufferedImage读取图片手动做resize和归一化。建议把预处理逻辑封装成工具类避免每次推理都重复写。注意Java端ONNX Runtime的版本要和ONNX模型opset匹配。1.17版本支持opset 11到19如果你转换时用了更高的opsetJava端可能加载失败。4.3 性能优化线程数和内存配置ONNX Runtime默认的线程数可能不是最优的。在初始化Session时可以配置options ort.SessionOptions() options.intra_op_num_threads 4 options.inter_op_num_threads 2 options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess ort.InferenceSession(model.onnx, options)intra_op_num_threads控制单个算子内部的并行线程数inter_op_num_threads控制算子之间的并行度。对于OCR这种模型检测模型计算量大intra设成4到8比较合适识别模型相对轻量intra设成2到4就够了。设太多线程反而会因为线程切换开销导致性能下降。graph_optimization_level设成ORT_ENABLE_ALL会启用所有图优化包括算子融合、常量折叠等通常能提升10%到20%的性能。5. 模型量化让推理再快一倍5.1 动态量化与静态量化的选择ONNX模型量化主要有两种方式动态量化和静态量化。动态量化在推理时动态计算量化参数不需要校准数据使用简单静态量化需要一批校准数据来统计激活值的分布精度通常更好但流程复杂一些。对于OCR模型我一般推荐先试动态量化因为实现简单精度损失通常在可接受范围内。如果动态量化后精度下降太多再考虑静态量化。动态量化的代码from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_inputdet_model.onnx, model_outputdet_model_int8.onnx, weight_typeQuantType.QUInt8 )QuantType.QUInt8表示权重量化成8位无符号整数。量化后模型体积会缩小到原来的四分之一左右推理速度在支持INT8的硬件上能提升2到4倍。5.2 量化后的精度对比与调优量化后一定要做精度对比。用同一批测试图片分别用FP32模型和INT8模型推理统计识别准确率的差异。如果准确率下降超过2个百分点就需要调优。调优的方法有几个。一是只量化部分层比如只量化卷积层不量化全连接层。二是用静态量化提供校准数据集。三是调整量化时的per_channel参数设成True会对每个通道单独计算量化参数精度更好但速度稍慢。quantize_dynamic( model_inputrec_model.onnx, model_outputrec_model_int8.onnx, weight_typeQuantType.QUInt8, per_channelTrue, reduce_rangeTrue )reduce_rangeTrue会把量化范围从0-255缩到0-127避免某些硬件上INT8溢出但会略微降低精度。这个参数在ARM平台上比较有用。实操心得识别模型量化后精度损失通常比检测模型大因为识别模型对数值精度更敏感。如果识别模型量化后效果不好可以只量化检测模型识别模型保持FP32整体速度提升也很明显。6. 常见问题与排查技巧实录6.1 转换报错不支持的算子怎么办转换时最常见的报错是“Unsupported operator”。PaddlePaddle有一些自定义算子ONNX标准里没有对应实现。遇到这种情况先看是哪个算子然后查Paddle2ONNX的文档看是否支持。如果确实不支持有两个思路。一是升级Paddle2ONNX到最新版新版本通常会增加算子支持。二是修改模型结构把不支持的算子替换成等价的ONNX算子组合。比如PaddlePaddle的swish激活函数在ONNX里可以用Sigmoid加Mul组合实现。6.2 推理结果乱码字符集映射问题PaddleOCR识别模型输出的是字符索引需要映射到实际字符。这个映射表在PaddleOCR的代码里叫ppocr_keys_v1.txt是一个每行一个字符的文本文件。如果映射表用错了识别结果就会乱码。映射表要和模型版本对应。PP-OCRv5的中文模型用的是ppocr_keys_v1.txt包含6623个字符。如果你用的是其他语言的模型映射表文件不一样。这个文件在PaddleOCR的GitHub仓库里可以找到下载后按行读取构建索引到字符的字典。6.3 动态形状导致的推理失败前面说了检测模型输入高宽设成动态但有些推理后端对动态形状支持不好。如果遇到“Invalid shape”或“Dimension mismatch”的报错可以尝试把动态轴固定。方法是在转换时指定具体的H和W或者在推理前把输入resize到固定尺寸。另一个常见问题是batch size。如果转换时batch设成1推理时传了batch大于1的数据会报错。反过来如果转换时batch是动态的推理时传batch 1也可以但某些后端可能会慢。建议转换时就把batch固定成1因为OCR推理通常一次处理一张图。6.4 常见问题速查表问题现象可能原因解决方法转换时报Unsupported operator算子不被Paddle2ONNX支持升级Paddle2ONNX或替换算子推理结果全是blank输入预处理不对检查归一化参数和通道顺序识别结果乱码字符映射表错误使用对应版本的keys文件推理速度慢线程数配置不当调整intra/inter_op线程数量化后精度下降大量化范围过宽使用per_channel或静态量化Java端加载模型失败opset版本不匹配降低opset或升级ONNX Runtime7. 部署场景扩展与个人经验7.1 车牌识别场景的特殊处理车牌识别是OCR的典型应用但和通用OCR有些不同。车牌文字排列规整字符集小汉字字母数字总共几十个字符可以用更小的识别模型。实际部署时检测模型可以复用PP-OCR的检测模型识别模型可以自己训练一个轻量级的然后转ONNX。车牌识别的预处理也有讲究。车牌图片通常需要先做透视矫正把倾斜的车牌拉正再送进识别模型。这一步可以在ONNX推理之前用OpenCV完成不增加模型复杂度。7.2 边缘设备部署的注意事项在ARM边缘设备上部署ONNX模型有几个点要注意。一是ONNX Runtime要装ARM版本Python包名是onnxruntime但ARM上可能需要从源码编译。二是INT8量化在ARM上收益很大因为ARM的CPU通常有INT8加速指令。三是内存占用边缘设备内存有限模型量化后体积小加载时内存占用也小。如果边缘设备有NPU比如某些国产芯片ONNX模型可能需要进一步转成NPU支持的格式。这个转换通常由芯片厂商的工具链完成ONNX作为中间格式兼容性最好。7.3 我踩过的几个坑第一个坑是归一化参数。PaddleOCR检测模型的归一化用的是ImageNet的均值和方差即mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。识别模型用的是mean0.5std0.5。这两个不一样我一开始搞混了检测结果一直不对排查了半天才发现。第二个坑是通道顺序。OpenCV读进来是BGRPaddleOCR训练时用的是RGB所以预处理时要img[:, :, ::-1]做通道翻转。这个细节很容易忘忘了之后检测框位置会偏识别结果也会乱。第三个坑是动态宽度。识别模型转换时宽度设了动态但推理时如果宽度太小比如小于10个像素ONNX Runtime可能会报错。解决办法是在预处理时保证最小宽度比如new_w max(new_w, 16)。第四个坑是Java端的内存管理。ONNX Runtime Java API的OnnxTensor和OrtSession.Result需要手动关闭否则会内存泄漏。长时间运行的服务一定要用try-with-resources或者手动close。7.4 后续可以扩展的方向这套流程跑通之后还可以做很多扩展。比如把检测和识别模型合并成一个ONNX模型减少两次推理之间的数据拷贝。或者用ONNX Runtime的TensorRT后端在NVIDIA GPU上进一步加速。还可以把模型转成ONNX之后再用OpenVINO的工具链转成IR格式在Intel平台上获得更好的性能。另外PaddleOCR 3.x版本对模型结构做了不少调整转换时的参数可能和v5不一样。如果你用的是3.x的模型建议先看Paddle2ONNX的release note确认支持的版本范围。我个人在实际项目中的体会是PaddleOCR转ONNX这件事难点不在转换本身而在转换之后的精度验证和性能调优。转换命令就那几行但要让模型在实际业务中稳定跑起来需要把预处理、后处理、量化、线程配置这些环节都调对。尤其是预处理不同版本的模型归一化参数可能不同一定要以官方代码为准不要凭记忆写。
返回列表