ARTICLE DETAIL

资讯详情

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

PP-OCRv4模型转换部署实战:从ONNX到RK3588 NPU加速

PP-OCRv4模型转换部署实战:从ONNX到RK3588 NPU加速 上个月帮客户做一批基于RK3588的边缘识别终端OCR模块要识别设备铭牌上的型号序列号。最开始在服务器上用PaddleOCR的PP-OCRv4跑得很顺一到嵌入式平台就卡壳Paddle Inference在ARM板的部署依赖太多算子支持得逐个验证交叉编译一堆未知符号等着处理。折腾了两天后我换了个思路把PP-OCRv4的三个子模型全部导出成ONNX再分别接到RK3588的CPU和NPU上流程一下子清晰了很多。这篇东西就是把这条“PP-OCRv4 → ONNX → RK3588/RKNN”的路线完整记录下来适合正在做OCR边缘设备、想把PaddleOCR移植到ARM盒子或RK3588平台的工程师参考。1. 为什么是ONNXPP-OCRv4在嵌入式部署中的绕路方案1.1 PP-OCRv4本身并不难跑难的是离开服务器PP-OCRv4是PaddleOCR里比较成熟的版本检测端用的是可微二值化DBNet框架识别端是SVTR系列演进出的HGNet骨干网络整条pipeline按“文本检测 → 方向分类 → 文本识别”三段式组织。在x86服务器上直接用PaddlePaddle的Python接口跑demo非常轻松下载官方权重加载模型一张图三五十毫秒就出结果。但部署到嵌入式平台就是另一回事。Paddle Inference和Paddle Lite对ARM架构有支持可实际用起来有两个绕不开的问题。第一Paddle生态和自家推理库绑定得比较紧模型格式很难直接吃进其他推理引擎第二RK3588这种平台的算力核心是NPU而NPU能识别的模型格式是RKNNPaddle模型必须绕一大圈才能转过去中间只要有一个算子不支持就白忙活。我在第一块板子上尝试直接用Paddle Lite加载成模型结果光编译环境就折腾了一天后面又遇到几个ARM上未注册算子的报错果断放弃了这条路线。1.2 ONNX为什么能做“翻译官”ONNX是一个开放的模型交换格式作用很像中间翻译官。训练框架用Paddle、PyTorch或者TensorFlow导出成ONNX后下游推理引擎只要能读ONNX就能跑不需要关心模型原来是从哪个框架来的。PP-OCRv4在PaddleOCR框架里虽然是Paddle原生格式但官方提供了paddle2onnx转换工具三步就能把检测、分类、识别三个子模型全部导成ONNX。更关键的是ONNX能同时接入RK3588的两条推理通道。一条是ONNX Runtime作为通用推理引擎直接跑在ARM的CPU上部署最快另一条是用RKNN-Toolkit2把ONNX转成RKNN格式喂给NPU算力跑。也就是说导出ONNX这一步做完后面无论是快速验证、CPU兜底还是NPU加速路径全打通了。1.3 三条路线放在一起看我整理了一张路线对比能直观看出为什么最终选择ONNX作为中间层部署路线模型格式开发成本性能表现适合场景Paddle Inference/LitePaddle原生格式高依赖库多嵌入式编译麻烦CPU上中等NPU基本用不上纯Linux x86服务端或对框架生态依赖很强的项目ONNX RuntimeONNX低导出后直接加载纯CPU推理性能取决于板子原型验证、跨平台PoC、业务跑通阶段ONNX → RKNNRKNN中需做转换和量化NPU加速性能最优RK3588量产设备、视频流实时OCR我最终选择的是“先导ONNX再按需求选ORT或RKNN”这个策略。这样做的好处是中间产物只有一份换平台时不用重新导出模型只需要在目标平台上换推理后端。2. 导出前的准备环境版本、模型权重与输入规范2.1 版本组合先固定下来别用最新很多人一上来就装最新版PaddlePaddle和最新版paddle2onnx结果导出报错后完全不知道是谁的锅。PaddleOCR的版本迭代非常快而paddle2onnx对新导出的模型结构不一定完全兼容这一块我建议直接用经过验证的稳定组合组件推荐版本说明PaddlePaddle2.5.x 或 2.6.x训练和导出用同一套避免算子差异paddle2onnx1.0.5 或 1.0.91.x系列比较稳2.x换了参数形式习惯用1.xPaddleOCRrelease/2.7 或 release/2.8这两个分支包含PP-OCRv4的完整权重和配置onnxruntime1.13开发机做校验用版本旧一点没关系RKNN-Toolkit21.6.x 或 2.x与RK3588固件配套选对应版本有一点要提醒paddle2onnx在1.0.x和2.x的CLI参数差异很大网上很多教程混用导致命令不生效。我在写转换脚本时统一用的是1.0.x的参数风格如果你手头是2.x版本记得先执行paddle2onnx --help看下实际参数名。2.2 权重来源与目录结构PP-OCRv4的官方推理模型可以从PaddleOCR仓库的release文档里下载需要准备三份文本检测模型ch_PP-OCRv4_det_infer.tar方向分类模型ch_ppocr_mobile_v2.0_cls_infer.tar文本识别模型ch_PP-OCRv4_rec_infer.tar解压后每个目录里有两个核心文件ch_PP-OCRv4_det_infer/ ├── inference.pdmodel # 模型结构 ├── inference.pdiparams # 模型参数 └── inference.pdiparams.info有人会问为什么方向分类还是v2.0的因为PP-OCRv4在方向分类这个环节上沿用MobileNetV3小模型已经足够官方没有单独出v4版分类模型这条信息在PaddleOCR文档里明确写过部署时不要去找不存在的ch_PP-OCRv4_cls_infer。下载完权重我建议先在开发机上用PaddleOCR的预测接口把官方demo跑通一次记录一张测试图的识别结果。这张结果图后面有大用所有导出、转换、量化后的模型都要以它作为基准去做对比确保每一步没有引入精度损失。2.3 三个子模型的输入规范PP-OCRv4的检测、分类、识别是三个独立模型输入输出各不相同。导出前先把各自的输入规范搞清楚后面写预处理和后处理时才不会乱。模型输入形状说明检测det[1, 3, H, W]H/W可变通常按长边960缩放宽高最好保持原图比例方向分类cls[1, 3, 48, 192]固定输入宽高不能乱动识别rec[1, 3, 48, W]W可变高固定48宽度根据文本行宽度缩放建议最大不超过320预处理必须和PaddleOCR源代码保持一致。以检测模型为例PaddleOCR的NormalizeImage是在/255之后按ImageNet的均值方差做的标准化具体是mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]。而且PaddleOCR读取图像后统一转成RGB顺序如果用OpenCV的imread读图默认是BGR顺序必须加一步cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。这个细节看起来不起眼但我在实际调试中遇到过一次全图框位置完全错乱的问题最后查下来就是颜色通道顺序反了。对于OCR这种对像素值极其敏感的模型来说RGB/BGR的顺序差异是致命的。3. 检测、方向分类、识别三个子模型的ONNX导出与校验3.1 导出命令与参数选择三个模型的导出方式完全一样只是model_dir和save_file不同。以检测模型为例paddle2onnx \ --model_dir ./ch_PP-OCRv4_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./onnx_models/ch_PP-OCRv4_det.onnx \ --opset_version 11 \ --enable_onnx_checker True这里有几个参数值得解释。opset_version我建议选11这是RKNN-Toolkit2和ONNX Runtime兼容性最稳妥的版本调成13或更高的话某些算子会被拆得更碎反而增加RKNN转模型时的工作量。enable_onnx_checker True会在导出后自动校验ONNX结构合法性等于做了一遍初步体检。方向分类模型paddle2onnx \ --model_dir ./ch_ppocr_mobile_v2.0_cls_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./onnx_models/ch_ppocr_mobile_v2.0_cls.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 ./onnx_models/ch_PP-OCRv4_rec.onnx \ --opset_version 11 \ --enable_onnx_checker True3.2 导出后的立即校验导出完成后先别急着转RKNN在开发机上用ONNX Runtime做一轮输入输出核对确认模型没有“导出成功但结构损坏”的问题。import onnxruntime as ort import numpy as np sess ort.InferenceSession(./onnx_models/ch_PP-OCRv4_det.onnx) for inp in sess.get_inputs(): print(input:, inp.name, inp.shape, inp.type) for out in sess.get_outputs(): print(output:, out.name, out.shape, out.type)正常的话det模型的输入名一般是x输出是一个四维的Tensor后面接Sigmoid概率图rec模型输出是[1, 133, ?]133代表字典字符数加空格等符号cls模型输出是[1, 2]或类似两个类别的概率。如果输出维度明显不对比如rec模型输出维度变成了[1, 100, ?]那说明模型版本和字典不匹配必须回头检查权重来源。校验完结构再喂一张真实图片做一次推理。这一步不求后处理只求输出数值在合理范围内。比如检测模型的概率图输出应该在0到1之间如果出现大量负值或NaN大概率是预处理没对齐。3.3 onnxsim化简的必要性导出的ONNX通常会带很多训练框架留下的冗余结构比如多余的Identity节点、固定shape的Shape节点、常量操作等。这些节点在ONNX Runtime上跑没问题但喂给RKNN-Toolkit2转换时很容易变成“不支持算子”。我的经验是先用onnxsim做一次图优化。安装很简单pip install onnxsim然后对三个模型分别执行python3 -m onnxsim \ ./onnx_models/ch_PP-OCRv4_det.onnx \ ./onnx_models/ch_PP-OCRv4_det_sim.onnxonnxsim会把常数折叠掉、删除无用节点最后输出的模型图更干净。转RKNN时sim后的模型成功率比原始ONNX高出不少。如果你的onnxsim遇到某些特殊算子处理不了还有一个备用方案是用onnxruntime.tools的onnx_model_editor手动删节点但操作成本高优先用onnxsim。化简后的模型建议再用Netron打开看一眼确认三个输入节点和输出节点都正常。Netron是网页工具把onnx文件拖进去就能看到图结构这一步对后续定位算子问题非常有帮助。4. RK3588上的两条部署路线ONNX Runtime直跑与RKNN NPU加速4.1 先说结论什么时候选哪条RK3588是一颗8核处理器4个A76大核加4个A55小核NPU算力标称6 TOPS同时带RGA硬件加速和强大的多媒体能力做边缘OCR设备相当合适。但“算力6 TOPS”指的是INT8模式下的峰值如果程序只跑CPU根本发挥不出这块板子的价值。我在项目里的实际分工是这样的原型验证阶段板端直接用ONNX Runtime加载sim后的ONNX模型把整个OCR业务流跑通包括拍照、预处理、推理、后处理、结果返回。这阶段可能一天就完成目的是确认整条链路逻辑没问题。性能达标阶段把三个模型全部转成RKNN格式拆分到NPU上跑同时处理好CPU和NPU之间的数据拷贝追求端到端时延降到最低。如果应用场景对时延不敏感比如每秒只处理一两张图CPU的ONNX Runtime方案其实完全够用还能省掉RKNN转换的工作量。反过来如果要做视频流连续识别或并发处理多路图像就必须上NPU。4.2 路线AONNX Runtime直接推理在RK3588的Ubuntu系统上安装ONNX Runtime非常方便pip install onnxruntimeRK3588的Ubuntu系统是aarch64架构只需要下载arm64变体即可。如果板端是精简系统没有Python环境也可以下载官方发布的C库版本通过C/C接口调用但开发速度会慢一些。用ONNX Runtime加载之前导出的sim模型import onnxruntime as ort import cv2 import numpy as np sess ort.InferenceSession(ch_PP-OCRv4_det_sim.onnx, providers[CPUExecutionProvider]) img cv2.imread(test.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized cv2.resize(img_rgb, (640, 640)) img_norm img_resized.astype(np.float32) / 255.0 img_norm (img_norm - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) img_nchw np.transpose(img_norm, (2, 0, 1))[np.newaxis, ...].astype(np.float32) outputs sess.run(None, {x: img_nchw})实测下来在RK3588上纯用CPU跑PP-OCRv4的检测模型640×640输入大概在90到150毫秒这个量级具体和电源策略、CPU调度都有关系。单个识别模型在48×320输入下大概在20到40毫秒。整条流水线下来一般要200毫秒以上。对于原型验证来说够了但离实时识别还差得远。4.3 路线BONNX转RKNN要发挥NPU算力必须在开发机上用RKNN-Toolkit2把ONNX转成RKNN格式。RKNN-Toolkit2是运行在x86开发机上的工具转换完成后将.rknn文件拷贝到板端用RKNNLite加载推理。开发机上安装RKNN-Toolkit2我建议直接按官方文档用Docker镜像能省掉不少环境依赖的麻烦。转换脚本核心部分from rknn.api import RKNN rknn RKNN(verboseTrue) # 设置目标平台和量化级别 rknn.config(target_platformrk3588, optimization_level3) # 加载ONNX模型 ret rknn.load_onnx( model./onnx_models/ch_PP-OCRv4_det_sim.onnx, input_size_list[[1, 3, 640, 640]] ) if ret ! 0: raise RuntimeError(load onnx failed) # 先不量化跑一次全精度转换 ret rknn.build(do_quantizationFalse, dataset./dataset.txt) if ret ! 0: raise RuntimeError(build failed) # 导出RKNN模型 rknn.export_rknn(./rknn_models/ch_PP-OCRv4_det.rknn)这里必须强调input_size_list的用途。PP-OCRv4的检测模型在Paddle框架里是动态shape但RKNN转换时如果ONNX模型里还有动态维度转换器会自动fallback到CPU算子NPU完全用不上。因此转RKNN之前要么在ONNX图上把shape固定死要么通过input_size_list指定固定尺寸。我最终固定的检测输入是1×3×640×640识别输入是1×3×48×320。这样虽然会损失一部分大图检测的动态灵活性但换来的是NPU上的流畅加速值得。转换完成后把rknn文件拷贝到板端用RKNNLite推理from rknnlite.api import RKNNLite rknn_lite RKNNLite() rknn_lite.load_rknn(./rknn_models/ch_PP-OCRv4_det.rknn) rknn_lite.init_runtime(core_maskRKNNLite.NPU_CORE_0_1) outputs rknn_lite.inference(inputs[img_nchw])4.4 两条路线的结果对齐同一张测试图用ONNX Runtime和RKNN分别推理输出结果可能会有细微差异特别是在某些激活函数或归一化算子上NPU的定点计算和CPU浮点计算不完全一致。我的习惯是先把两边的输出都存成npy然后逐元素比较误差确保误差在可接受范围内。如果差异过大优先检查两点一是预处理是否完全一致二是是否存在算子被映射成了低精度模式。尤其注意RKNN模型输入数据的布局是NCHW和ONNX Runtime保持一致不要在板端想当然改成NHWC。这个错误我在早期调试时踩过一次输出全乱排查了好几个小时才定位到是数据摆布问题。5. int8量化把NPU算力真正吃到嘴里的关键一步5.1 为什么RK3588上的部署绕不开量化RK3588的NPU之所以标6 TOPS指的就是INT8算力。模型保持FP32精度转成RKNN虽然能跑但NPU会以较低效率执行内存占用也高。以检测模型为例FP32的ONNX模型转成RKNN后会明显变大推理时还有额外内存开销。而做完INT8量化后模型体积缩到约四分之一推理速度明显提升精度损失通常又在一个可接受范围内。特别是在OCR这种场景里检测模型和识别模型同时跑如果不量化NPU上同时承载两个大模型会比较吃力量化后就从容很多。5.2 校准数据集怎么准备RKNN量化并不是简单把权重转成INT8它需要一批真实图片做校准统计每层激活值的分布范围然后确定量化尺度。这一步非常关键直接影响量化后的精度。rknn.build里的dataset参数指向一个txt文件每行写一张图片的路径./calib_imgs/001.jpg ./calib_imgs/002.jpg ./calib_imgs/003.jpg ...我的建议是准备200张左右代表真实业务的图片覆盖不同光照、不同字体大小、不同背景复杂度。如果做的是设备铭牌识别就多拍一些金属反光面、贴纸产品标牌、塑料外壳上的丝印如果做的是文档扫描就多准备打印体和手写体混合的数据。千万不要随便拿ImageNet图片凑数那种图片和OCR场景的像素分布差异太大量化完很容易掉点。另外校准图片喂给模型前也要走相同的预处理包括缩放、归一化。RKNN-Toolkit2在量化的时候会自动读取图片但我不确定它的预处理是否和PaddleOCR一致稳妥做法是自己先把图片处理好再喂给dataset。5.3 量化后精度验收与精度补救手段量化后的模型必须在真实测试集上做一轮对比。我的验收方法是抽50到100张业务图片分别用FP32的RKNN模型和INT8的RKNN模型跑一遍OCR对比识别正确率和检测框的IoU。如果识别正确率下降超过1%到2%或者出现明显漏检就要想办法补救。补救手段有几个提高校准数据的质量加入更多与业务场景接近的样本重新统计激活分布。混合精度RKNN-Toolkit2提供了部分算子保持FP16/FP32的能力对精度损失大的敏感层做保留。调整后处理阈值量化后某些低置信度检测框会消失可以把检测的后处理阈值适当调低找回一部分召回率。下表是我在项目里的典型模型大小对比以检测模型为例模型版本大小说明FP32 ONNX约100%基准原始导出大小FP32 RKNN略小于ONNX转换后格式紧凑INT8 RKNN约25%大小量化后显存和带宽压力都小实际数据会因模型结构有浮动但大致是这个比例。量化后识别模型的输出概率分布通常会比原始模型更“尖锐”后处理时要注意不要设置太高阈值否则容易把置信度0.6左右的正确结果滤掉。6. 移植调试中真正值得记录的坑6.1 预处理对不上输出全是无效框这是很多人第一次移植时最容易踩的坑。导出ONNX后在开发机上用Paddle原版推理结果正常但用自己写的ONNX Runtime推理脚本一跑框全乱了或者干脆没有有效框。根本原因就是预处理没对齐。PaddleOCR的预处理链路包含Resize → Normalize → Transpose三个环节而Normalize用的不是简单的除255是ImageNet的mean/std标准化。如果只做了缩放到0到1就喂给模型输出特征分布和训练时完全不同自然出不来框。我的排查经验是先用最简单的方式打印模型输入前的像素值和PaddleOCR源码中同一张图预处理后的像素值做逐像素对比。只要两个值一致模型输出基本就不会差。6.2 字典错位识别结果一页乱码识别模型输出的是每个字符的类别索引要把索引映射回实际字符需要字典文件ppocr_keys_v1.txt。如果字典文件和模型版本不匹配识别结果就会是一串乱码或错误字符。这类问题隐蔽在导出后的模型里很难看出来。我遇到过一种情况识别模型输出维度看起来正常字符索引范围也对但实际印出的结果和图上文字完全对不上。后来发现是PaddleOCR不同版本之间调整过字典顺序我用了一个旧版字典。解决方案很简单每个推理模型目录或权重包发布时官方会附带对应版本的字典必须使用和模型同一版本号的字典文件不要跨版本混用。另外如果启用了use_space_char选项字典末尾会多一个空格符号索引匹配时也要把这个偏移考虑进去。6.3 ONNX转RKNN不支持的算子RKNN-Toolkit2的算子支持虽然在持续扩充但遇到比较新或比较特殊的算子还是会有报错。我在转PP-OCRv4识别模型时就撞到过一次Unsupported op的报错定位到的节点是动态shape场景下生成的Gather和Resize相关算子。排查思路是从报错日志中找到具体算子名再回到ONNX图里看这个算子能不能化简。大部分情况下先用onnxsim简化模型就能干掉一大半不支持的节点。如果简化后还报错就考虑把动态维度固定下来比如把无限制的输入高度从-1改成48宽度从-1改成固定值。还有一招是升级RKNN-Toolkit2版本新版本对ONNX的兼容性会好一些。注意升级后要回归验证一次模型精度因为新版本的算子融合策略可能会变输出结果有微小差异是正常的。6.4 固定shape导致的坐标回映射错误RKNN模型固定为640×640输入后检测出的框坐标是在缩放后的图像坐标系里的。把框画回原图时必须根据原图和固定输入之间的缩放比例做回映射。这个比例不是简单的640 / original_width因为PaddleOCR在检测预处理时用的是保持长宽比的resize如果原图是1920×1080缩放后图像并不会充满640×640的四周剩余空间通常用0来填充。这意味着坐标回映射还要考虑padding的偏移量。我的做法是记录三个信息原图尺寸、缩放后尺寸、padding偏移然后按线性映射把检测框坐标换算回原图坐标。调试时直接在图上画框验证框和文字边界贴合就算正确。6.5 NPU内存对齐与并发调用RKNNLite在板端如果长时间运行偶尔会出现内存分配失败或malloc failed的报错。这类问题通常和输入数据的对齐要求有关。NPU处理时对输入tensor的宽高有一定对齐要求我实际遇到的是宽度必须是16的整数倍最保险的写法是在预处理阶段直接把图像resize到16的倍数再喂给NPU。另外如果应用里同时跑多个OCR任务不要频繁初始化RKNNLite实例最好启动时加载一次后续复用同一个实例做推理。我在一个并发项目里遇到内存持续上涨排查到最后发现是每次请求都重新load_rknn又没释放改成全局单例之后内存稳定了下来。6.6 版本锁定的建议最后一个建议不是具体代码而是工程习惯。RKNN-Toolkit2、PaddleOCR、paddle2onnx、ONNX Runtime这几个工具的版本一旦验证通过就必须在项目文档里固化下来。我在做第二个设备时升级了一次RKNN-Toolkit2结果转换出的模型在旧固件上无法加载回退版本才解决。建议把所用的版本号、转换脚本、校准图片集、三个sim后的ONNX模型、三个rknn模型全部归档到一个独立目录作为项目的基线文件保存。后续接新平台或者换新需求都从这个基线出发避免每次重新摸索组合兼容性。以上这些坑都是我在RK3588平台上真正踩过的。如果重新做一次移植我会把“先对齐预处理再验证模型输出最后做量化”这个顺序执行得更彻底因为很多看似诡异的模型输出问题根子上都是前处理或后处理环节的参数偏差。把PP-OCRv4转成ONNX再上RK3588这件事本质是拿一个开放中间格式换来了整个工具链的选择权这一步选对了后面板端适配的路就顺了。
返回列表