
简介这份资源面向需要在C或Python环境中落地OCR能力的深度学习开发者与工程人员提供基于ONNXRuntime推理框架部署PaddleOCR-v3的完整方案解决从PaddlePaddle模型到跨框架推理的迁移问题适用于文字检测与识别场景的工程化实践。压缩包共22个文件约23.57MB包含6个onnx模型文件、4个Python脚本、4个C源文件及3个头文件另有词表、说明文档与测试图片覆盖模型、双语言源码与配套资料。已有679人学习下载说明其在部署实践中具备一定参考价值。读者可据此掌握模型转换、C与Python两套推理接口的调用方式理解检测、方向分类与识别三个模块的协同流程并借助说明文档与示例图片快速验证效果减少环境搭建与接口调试的试错成本。1. ONNXRuntime 部署 PaddleOCR-v3为什么这套组合值得你花一个下午跑通如果你手上有一批扫描件、票据或者现场拍回来的照片需要把上面的文字抠出来又不想把数据传到别人的服务器上那 PaddleOCR-v3 加 ONNXRuntime 这条路线大概率是你绕不开的一个选项。PaddleOCR-v3 指的是 PaddleOCR 里那套 PP-OCRv3 检测加识别模型检测用的是 DBNet 的改进版识别用的是轻量 CRNN 结构中文场景下的精度和速度平衡做得相当扎实。而 ONNXRuntime 是一个跨平台的推理引擎它把训练框架和部署环境解耦了——你可以在 PyTorch 或者 Paddle 里训好模型导出成 ONNX 格式然后拿 ONNXRuntime 在 Windows、Linux 甚至鲲鹏 920 这类 ARM 服务器上跑推理不需要装完整的训练框架。这套组合解决的核心问题是推理环境要轻、要跨平台、要能嵌到 C 工程里。很多做工业质检或者文档数字化的团队最终交付的是一个 C 桌面程序或者一个后台服务Python 只用来做模型导出和验证。ONNXRuntime 同时提供 Python 和 C API两边都能调同一份 ONNX 模型这就意味着你可以用 Python 快速验证效果再用 C 做最终交付模型文件不用换。适合谁呢适合那些需要本地化部署 OCR、对数据隐私有要求、或者要把 OCR 嵌到已有 C 系统里的开发者。如果你只是偶尔识别几张图直接调云 API 更省事但如果你要批量处理、要离线、要集成那往下看。2. 把 PaddleOCR-v3 拆开看检测、识别、方向分类三件事2.1 PP-OCRv3 的三个模型各管什么PaddleOCR-v3 不是单个模型而是一条流水线通常包含三个 ONNX 文件。第一个是检测模型输入是一张任意尺寸的图输出是一堆文本框的坐标它负责回答“文字在哪”。第二个是方向分类模型输入是裁出来的单个文本框图像输出是 0 或 180 度的判断它负责回答“这行字是不是倒的”。第三个是识别模型输入是摆正后的文本行图像输出是字符序列它负责回答“这行字念什么”。三个模型串起来才是一条完整的 OCR 链路。为什么强调这个拆分因为部署的时候很多人只导出了检测和识别两个模型忘了方向分类结果遇到倒置的扫描件就翻车。PaddleOCR 的默认配置里方向分类是可选的但实际文档里倒置页并不少见尤其是复印再扫描的合同。我一般会建议把三个都导出方向分类模型很小多出来的推理开销几乎可以忽略。2.2 从 Paddle 格式到 ONNX导出时最容易忽略的输入输出名导出这一步是整条链路的起点也是最容易埋雷的地方。PaddleOCR 官方提供了导出 ONNX 的脚本但不同版本参数名有差异。下面这段 Python 代码是我常用的导出方式基于 PaddleOCR 的tools/export_model.py思路改写核心是确认动态输入轴和输出节点名。import paddle from paddleocr import PaddleOCR import paddle2onnx # 先加载 PP-OCRv3 的中文模型让 PaddleOCR 自动下载或指定本地路径 ocr PaddleOCR(use_angle_clsTrue, langch, ocr_versionPP-OCRv3) # 导出检测模型输入是 [1,3,H,W] 的动态尺寸 # 注意 opset_version 建议 11 以上否则某些算子不支持 paddle2onnx.export( model_dir./inference/ch_PP-OCRv3_det_infer, save_file./onnx/ch_PP-OCRv3_det.onnx, opset_version11, input_shape_dict{x: [1, 3, -1, -1]}, # -1 表示动态维度 enable_onnx_checkerTrue ) # 导出识别模型输入高度固定 48宽度动态 paddle2onnx.export( model_dir./inference/ch_PP-OCRv3_rec_infer, save_file./onnx/ch_PP-OCRv3_rec.onnx, opset_version11, input_shape_dict{x: [1, 3, 48, -1]}, enable_onnx_checkerTrue ) # 导出方向分类模型输入固定 48x192 paddle2onnx.export( model_dir./inference/ch_ppocr_mobile_v2.0_cls_infer, save_file./onnx/ch_ppocr_cls.onnx, opset_version11, input_shape_dict{x: [1, 3, 48, 192]}, enable_onnx_checkerTrue )这段代码的关键参数是input_shape_dict。检测模型的高度和宽度都设成 -1因为实际图片尺寸不固定识别模型高度锁死 48这是 PP-OCRv3 识别网络的固定输入高度宽度设 -1 以适配不同长度的文本行方向分类模型输入固定因为它的输入是已经裁好并 resize 到 48x192 的图。opset_version选 11 是因为 ONNXRuntime 对 11 的支持最稳再高有些算子会挑版本。导出后一定要用onnx.checker或者 ONNXRuntime 的InferenceSession加载一次确认没有报错再往下走。2.3 用 Python 先跑通最小推理闭环导出完先别急着写 C用 Python 把三个模型串起来跑一张图确认链路通。下面是最小闭环代码不依赖 PaddleOCR 的推理封装纯 ONNXRuntime。import onnxruntime as ort import cv2 import numpy as np # 创建三个 sessionproviders 按优先级排CPU 上就用 CPUExecutionProvider det_sess ort.InferenceSession(./onnx/ch_PP-OCRv3_det.onnx, providers[CPUExecutionProvider]) rec_sess ort.InferenceSession(./onnx/ch_PP-OCRv3_rec.onnx, providers[CPUExecutionProvider]) cls_sess ort.InferenceSession(./onnx/ch_ppocr_cls.onnx, providers[CPUExecutionProvider]) def preprocess_det(img, limit960): # 检测预处理resize 到 32 的倍数归一化 h, w img.shape[:2] ratio min(limit / max(h, w), 1.0) nh, nw int(h * ratio), int(w * ratio) nh max(32, (nh // 32) * 32) nw max(32, (nw // 32) * 32) resized cv2.resize(img, (nw, nh)) blob resized.astype(np.float32) / 255.0 blob (blob - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] blob blob.transpose(2, 0, 1)[None, ...] return blob.astype(np.float32), (h, w), (nh, nw) img cv2.imread(test.jpg) blob, orig_shape, resize_shape preprocess_det(img) det_out det_sess.run(None, {x: blob})[0] # 输出概率图 print(det output shape:, det_out.shape)这里providers参数决定了用哪个后端。纯 CPU 环境写CPUExecutionProvider如果有 NVIDIA 显卡并且装了 onnxruntime-gpu可以写CUDAExecutionProvider但要注意 CUDA 版本和 onnxruntime-gpu 的对应关系版本错配是常见翻车点。检测输出是一张概率图后面还需要做二值化和轮廓提取才能得到文本框这部分逻辑和 PaddleOCR 后处理一致可以参照官方DBPostProcess实现。识别模型输入需要把每个文本框裁出来、摆正、resize 到高度 48再送进rec_sess。方向分类在识别前对每个框做一次如果分数超过 0.9 就旋转 180 度。3. C 侧接入 ONNXRuntime从环境配置到第一个 session3.1 Windows 和 Linux 下 ONNXRuntime 动态库的获取与链接C 用 ONNXRuntime 的第一步是拿到动态库。官方在 GitHub Releases 里提供预编译包Windows 是onnxruntime-win-x64-1.x.x.zipLinux 是onnxruntime-linux-x64-1.x.x.tgz。解压后include放头文件lib放.so或.dll加.lib。Windows 下用 Visual Studio 的话项目属性里把include加到附加包含目录lib加到附加库目录链接器输入里加onnxruntime.lib。运行时把onnxruntime.dll放到 exe 同目录或者系统 PATH 里。Linux 下更简单编译时-I/path/to/include -L/path/to/lib -lonnxruntime运行时export LD_LIBRARY_PATH/path/to/lib:$LD_LIBRARY_PATH。如果是鲲鹏 920 这类 ARM 服务器要确认下载的是 aarch64 版本的包x64 的包装不上。我遇到过有人在鲲鹏上硬装 x64 包报cannot open shared object file查了半天才发现架构不对。另外如果目标机器没有装 Microsoft Visual C RedistributableWindows 下会提示缺msvcp140.dll这个依赖要提前装好或者静态链接。3.2 用 C 加载 ONNX 模型并做一次检测推理下面这段 C 代码演示了加载检测模型、构造输入张量、执行推理的完整过程。用的是 ONNXRuntime 的 C API核心类是Ort::Session和Ort::Env。#include onnxruntime_cxx_api.h #include opencv2/opencv.hpp #include vector #include iostream int main() { // 1. 创建环境和 session 选项 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, ocr_det); Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); // 控制线程数CPU 上别设太大 session_options.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); // 2. 加载模型Windows 下用宽字符路径 const wchar_t* model_path L./onnx/ch_PP-OCRv3_det.onnx; Ort::Session session(env, model_path, session_options); // 3. 查询输入输出信息 Ort::AllocatorWithDefaultOptions allocator; auto input_name session.GetInputNameAllocated(0, allocator); auto output_name session.GetOutputNameAllocated(0, allocator); std::cout input: input_name.get() output: output_name.get() std::endl; // 4. 准备输入数据假设已经预处理成 1x3x640x640 的 float 数组 std::vectorint64_t input_shape {1, 3, 640, 640}; std::vectorfloat input_data(1 * 3 * 640 * 640, 0.0f); // 实际使用时这里填入归一化后的图像数据 auto memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); // 5. 执行推理 const char* input_names[] {input_name.get()}; const char* output_names[] {output_name.get()}; auto outputs session.Run(Ort::RunOptions{nullptr}, input_names, input_tensor, 1, output_names, 1); // 6. 取输出 auto output_tensor outputs[0]; auto output_shape output_tensor.GetTensorTypeAndShapeInfo().GetShape(); float* output_ptr output_tensor.GetTensorMutableDatafloat(); std::cout output dim: output_shape.size() std::endl; return 0; }几个关键点。SetIntraOpNumThreads控制单次推理内部的并行线程数CPU 上一般设成物理核心数设太大反而因为线程切换变慢。GetInputNameAllocated返回的是智能指针不同版本 API 略有差异1.12 以后用这个老版本用GetInputName返回裸指针。输入张量的内存必须保持有效直到Run结束所以input_data不能是局部临时对象。输出张量的形状和数据类型要按模型实际输出解析检测模型输出通常是[1, 1, H, W]的概率图后处理需要自己实现。3.3 识别模型在 C 里的动态宽度处理识别模型的输入宽度是动态的这是 C 侧比 Python 麻烦的地方。Python 里可以直接传不同 shape 的 numpy 数组C 里每次遇到不同宽度的文本行要么重新创建Ort::Value要么把宽度 padding 到固定值。我一般用 padding 策略把所有文本行 resize 到高度 48宽度按比例缩放后 padding 到 320 或者 640记录实际宽度用于后处理时截断。这样输入 shape 固定session 只需要创建一次。// 假设文本行图像已 resize 到高度 48实际宽度为 real_w int real_w 200; int pad_w 320; // 固定宽度 std::vectorfloat rec_input(1 * 3 * 48 * pad_w, 0.0f); // 把图像数据填入前 real_w 列其余保持 0 // ... 填充逻辑省略 ... std::vectorint64_t rec_shape {1, 3, 48, pad_w}; Ort::Value rec_tensor Ort::Value::CreateTensorfloat( memory_info, rec_input.data(), rec_input.size(), rec_shape.data(), rec_shape.size()); auto rec_outputs rec_sess.Run(Ort::RunOptions{nullptr}, rec_input_names, rec_tensor, 1, rec_output_names, 1);padding 的副作用是识别结果可能在尾部多出一些空白字符后处理时按real_w / pad_w的比例截断输出序列即可。另一个方案是用 ONNXRuntime 的动态 shape 支持每次Run前重新创建输入张量但频繁创建销毁有开销批量处理时 padding 更划算。4. 避坑与排查部署 ONNXRuntime 时我踩过的五个坑4.1 模型加载报错 “Invalid GraphProto” 或算子不支持现象Python 里InferenceSession加载正常C 里加载同一个 ONNX 文件报Invalid GraphProto或者NOT_IMPLEMENTED。原因导出 ONNX 时用的 opset 版本高于 ONNXRuntime 支持的上限或者 C 用的 ONNXRuntime 版本比 Python 低。解决统一 ONNXRuntime 版本导出时opset_version不要超过目标运行时支持的最大值。查版本对应关系看官方文档的算子支持表实在不行降到 opset 11。4.2 检测框坐标偏移或者框全错现象Python 跑出来框是对的C 跑出来框整体偏移或者形状完全不对。原因预处理不一致。Python 里用的归一化均值方差是 ImageNet 的[0.485,0.456,0.406]和[0.229,0.224,0.225]C 里如果忘了减均值除方差或者 BGR 和 RGB 通道顺序搞反输出概率图就会错。解决把 Python 预处理代码逐行对照 C 实现确认 resize 插值方式、归一化参数、通道顺序完全一致。PaddleOCR 用的是 BGR 输入不是 RGB。4.3 识别结果全是乱码或者重复字符现象识别模型输出的字符序列看起来像随机字符或者同一个字重复很多遍。原因识别模型的字典文件没有正确加载或者 CTC 解码逻辑写错了。PP-OCRv3 识别输出是 CTC 格式需要先按时间步取 argmax再去重、去 blank。解决确认字典文件ppocr_keys_v1.txt的路径和编码C 里读字典注意 UTF-8 和宽字符转换。CTC 解码时 blank 的索引通常是 0去重时只合并相邻相同字符不相邻的相同字符要保留。4.4 鲲鹏 920 上推理速度远低于预期现象同样的模型在 x64 服务器上跑 50ms在鲲鹏 920 上跑 500ms。原因ONNXRuntime 的 ARM 版本默认没有启用针对鲲鹏的优化或者用了 x64 的包通过转译运行。解决确认下载的是 aarch64 原生包编译时开启--arm64相关选项。另外检查线程数设置ARM 核心数和 x64 不同SetIntraOpNumThreads要按实际核心数调整。如果还慢考虑用 ONNXRuntime 的 OpenVINO 或者 ACL 后端。4.5 C 程序退出时崩溃在 ONNXRuntime 析构现象程序功能正常但退出时偶尔崩溃堆栈指向 ONNXRuntime 内部。原因Ort::Env和Ort::Session的析构顺序问题或者多线程环境下 session 被多个线程同时析构。解决确保Ort::Env的生命周期比所有Ort::Session长把 Env 放在全局或者 main 函数最外层。多线程场景下每个线程用自己的 session或者用互斥锁保护 session 的 Run 调用。ONNXRuntime 的 session 不是线程安全的这点和某些推理框架不一样。5. 进阶技巧用 Python 做批量验证用 C 做交付跑通单张图之后下一步是批量验证和性能调优。我习惯先用 Python 写一个批量测试脚本把整个测试集跑一遍统计检测召回率和识别准确率确认模型效果达标。Python 侧可以用onnxruntime的SessionOptions开启 profiling看每个算子的耗时找出瓶颈在检测还是识别。如果检测耗时占比高可以尝试降低输入分辨率PP-OCRv3 检测模型在 640 宽度下通常够用960 是精度优先的选择。C 侧交付时重点在内存复用和批处理。ONNXRuntime 支持同一个 session 多次 Run输入输出张量可以预先分配好内存反复使用避免每次推理都 malloc。对于识别模型可以把多个文本行拼成一个 batch 送进去batch size 设成 8 或者 16吞吐量能提升明显。但要注意 padding 到相同宽度否则 batch 内 shape 不一致会报错。验证方法上我一般会准备三类测试图清晰扫描件、手机拍摄的倾斜文档、低光照条件下的票据。三类各跑 100 张人工核对结果记录漏检和错识的 case。如果倾斜文档漏检多检查检测模型的输入 resize 策略PaddleOCR 默认的limit_side_len是 960对于长边超过 960 的图会压缩小字可能丢。可以适当调大到 1280代价是推理变慢。最后说一个我自己的习惯每次导出 ONNX 模型后先用onnxsim做一次图简化去掉冗余算子通常能减少 10% 到 20% 的推理时间。简化后再用 Python 和 C 各跑一遍确认结果一致再交付。这个步骤花不了几分钟但能省掉后面很多“为什么两边结果不一样”的排查时间。希望帮到你。本文还有配套的精品资源点击获取