ARTICLE DETAIL

资讯详情

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

ONNXRuntime 部署 PaddleOCR-v3:从模型导出到 C++ 集成全流程

ONNXRuntime 部署 PaddleOCR-v3:从模型导出到 C++ 集成全流程 简介这份资源面向需要在C或Python环境中落地OCR能力的开发者提供基于ONNXRuntime推理框架部署PaddleOCR-v3的完整方案解决从PaddlePaddle模型到跨框架推理的迁移问题适用于文字检测与识别场景的工程实践。压缩包共22个文件约23.57MB包含6个onnx模型文件、4个Python脚本、4个C源文件及3个头文件另有说明文档、字典文件与测试图片覆盖模型、双语言源码与运行素材。资源已吸引679人学习下载具备一定参考热度。读者可据此掌握paddle2onnx模型转换流程理解C接口下会话创建、输入准备、推理执行与结果获取的完整链路也能对照Python接口的简洁调用方式快速搭建文字检测、方向分类与文字识别模块并借助说明文档与字典文件完成调试与结果解析为跨框架模型迁移和推理性能优化提供可复用的工程范例。1. ONNXRuntime 部署 PaddleOCR-v3为什么这条推理链路值得你花一个下午跑通如果你手上有一份 PaddleOCR-v3 的模型却卡在「训练环境能跑、生产环境装不上 Paddle」这一步那 ONNXRuntime 部署就是最省心的解法。PaddleOCR-v3 的检测、识别、方向分类三个模型都能导出成 ONNX之后用 onnxruntime 的 C 或 Python API 加载推理彻底摆脱对 PaddlePaddle 框架的运行时依赖。这件事的价值在于部署包体积从几百兆的框架依赖压到几十兆的动态库鲲鹏 920 这类 ARM 服务器上也不用再折腾 Paddle 的编译适配C 端直接链 onnxruntime 动态库就能出结果。适合谁做 OCR 落地的后端工程师、需要在 C 服务里嵌 OCR 的客户端开发者以及想把 Python 原型快速搬到生产环境的人。下面按「模型怎么来 → Python 怎么跑通 → C 怎么集成 → 坑在哪」的顺序讲透。2. 模型导出与目录结构从 PaddleOCR-v3 到 ONNX 的完整链路2.1 三个模型各管什么导出时分别注意什么PaddleOCR-v3 的推理链路是「检测 → 方向分类 → 识别」三级串联。检测模型负责找出文字区域输出的是文本框坐标方向分类模型判断每个框里的文字是不是旋转了 180 度识别模型把矫正后的文本行转成字符序列。三个模型导出 ONNX 时输入输出签名不一样这是后面 C 端写预处理时最容易翻车的地方。检测模型常见是 ch_PP-OCRv3_det的输入是[1, 3, H, W]的 float32 张量输出是[1, 1, H, W]的概率图。注意 H、W 必须是 32 的倍数否则导出时动态轴推断会出问题。识别模型ch_PP-OCRv3_rec输入是[1, 3, 48, W]高度固定 48宽度动态输出是[1, T, C]T 是时间步C 是字符集大小。方向分类模型输入[1, 3, 48, 192]输出[1, 4]或[1, 2]取决于你用的版本。导出命令用 PaddleOCR 自带的tools/export_model.py先转成 inference 格式再用paddle2onnx转 ONNX。常见做法是# 第一步导出 Paddle 静态图推理模型 python tools/export_model.py \ -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_cml.yml \ -o Global.pretrained_model./pretrain_models/ch_PP-OCRv3_det_distill_train/best_accuracy \ Global.save_inference_dir./inference/ch_PP-OCRv3_det # 第二步转 ONNX指定 opset 11开动态 batch paddle2onnx --model_dir ./inference/ch_PP-OCRv3_det \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./onnx/ch_PP-OCRv3_det.onnx \ --opset_version 11 \ --enable_onnx_checker True \ --input_shape_dict {x: [1, 3, -1, -1]}--input_shape_dict里的-1表示动态维度检测模型宽高都设动态识别模型高度写死 48、宽度设-1。--opset_version建议 11低于 11 时某些插值算子会退化成不支持的版本高于 13 部分老版本 onnxruntime 又跑不了。导出后务必用onnxruntime的 Python API 做一次 shape 校验别等到 C 端才发现在报维度不匹配。2.2 目录怎么摆C 和 Python 才能共用同一份模型一份能同时给 C 和 Python 用的目录核心原则是「模型文件、字典文件、配置参数三者分离且路径可配」。我一般会这样组织ocr_deploy/ ├── models/ │ ├── det.onnx │ ├── rec.onnx │ ├── cls.onnx │ └── ppocr_keys_v1.txt ├── python/ │ ├── ocr_engine.py │ └── test_ocr.py ├── cpp/ │ ├── include/ │ ├── src/ │ └── CMakeLists.txt └── config/ └── ocr_config.yamlppocr_keys_v1.txt是识别模型的字符字典每行一个字符行号就是模型输出的类别索引。这个文件必须和训练时用的完全一致差一行整个识别结果全错位。配置里把det_model_path、rec_model_path、cls_model_path、keys_path、use_gpu、det_limit_side_len这些参数抽出来Python 和 C 各读各的但值来自同一份 yaml。注意ONNX 模型文件本身不包含字典信息字典是后处理阶段用的。很多人导出完模型忘了拷字典Python 端跑出来全是乱码排查半天才发现是 keys 文件路径写错。2.3 用 Python 先验证 ONNX 模型能不能出正确结果在写 C 之前一定先用 Python 把整条链路跑通。这一步是「后悔药」——如果 ONNX 模型本身有问题C 端调试成本是 Python 的十倍。下面是最小验证脚本import onnxruntime as ort import numpy as np import cv2 # 加载检测模型CPU 执行 sess ort.InferenceSession(models/det.onnx, providers[CPUExecutionProvider]) input_name sess.get_inputs()[0].name img cv2.imread(test.jpg) # 检测模型预处理归一化 保持 32 倍数 h, w img.shape[:2] scale 960 / max(h, w) new_h, new_w int(h * scale), int(w * scale) new_h max(32, (new_h // 32) * 32) new_w max(32, (new_w // 32) * 32) resized cv2.resize(img, (new_w, new_h)) blob resized[:, :, ::-1].astype(np.float32) / 255.0 blob (blob - np.array([0.485, 0.456, 0.406])) / np.array([0.229, 0.224, 0.225]) blob blob.transpose(2, 0, 1)[None, ...].astype(np.float32) # 推理 prob_map sess.run(None, {input_name: blob})[0] print(det output shape:, prob_map.shape) # 应为 [1, 1, new_h, new_w]这段代码的关键点providers指定CPUExecutionProvider如果要用 GPU 就换成CUDAExecutionProvider并确认 onnxruntime-gpu 版本和 CUDA 版本匹配。预处理里的均值方差是 ImageNet 标准值PaddleOCR-v3 训练时用的就是这个。scale 960 / max(h, w)是限制长边det_limit_side_len参数控制这个值设太小小文字检测不到设太大显存爆。输出 shape 打印出来确认和输入尺寸一致如果对不上说明导出时动态轴没设对。识别模型验证类似但输入高度固定 48宽度按比例缩放。跑通后把检测框裁剪出来送识别看输出文字对不对。这一步过了C 端就只是「翻译」工作。3. C 端集成 onnxruntime从 CMake 配置到推理封装3.1 动态库怎么链CMakeLists 里必须写对的三处C 端用 onnxruntime 有两种方式链动态库推荐或静态库。动态库方式下你需要 onnxruntime 的头文件目录和.soLinux或.dllWindows文件。CMakeLists 里三处必须写对cmake_minimum_required(VERSION 3.10) project(ocr_deploy) set(CMAKE_CXX_STANDARD 14) # 第一处头文件路径指向 onnxruntime 解压后的 include 目录 include_directories(${CMAKE_SOURCE_DIR}/third_party/onnxruntime/include) # 第二处库文件路径Linux 下是 libonnxruntime.so link_directories(${CMAKE_SOURCE_DIR}/third_party/onnxruntime/lib) add_executable(ocr_test src/main.cpp src/ocr_engine.cpp) # 第三处链接 onnxruntime注意名字是 onnxruntime 不是 libonnxruntime target_link_libraries(ocr_test onnxruntime opencv_core opencv_imgproc opencv_imgcodecs)include_directories和link_directories的路径按你实际解压位置改。target_link_libraries里写onnxruntimeCMake 会自动找libonnxruntime.so。如果编译时报undefined reference to Ort::Session九成是链接顺序问题——把onnxruntime放在 opencv 后面试试。运行时如果报error while loading shared libraries: libonnxruntime.so说明动态库没在LD_LIBRARY_PATH里export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/path/to/onnxruntime/lib即可。注意Windows 下链接的是onnxruntime.lib导入库运行时需要onnxruntime.dll在同目录。Visual C Redistributable 版本太老也会导致加载失败装最新的 VC 运行库能省很多事。3.2 用 Ort::Session 封装一个可复用的推理类C 端不要每次推理都新建 SessionSession 初始化开销很大。封装一个类构造时加载模型推理时复用#include onnxruntime_cxx_api.h #include opencv2/opencv.hpp #include vector #include string class OcrEngine { public: OcrEngine(const std::string det_path, const std::string rec_path, const std::string keys_path, bool use_gpu false) : env_(ORT_LOGGING_LEVEL_WARNING, ocr) { // 配置 Session 选项 Ort::SessionOptions opts; opts.SetIntraOpNumThreads(4); // 线程数按 CPU 核数调 opts.SetGraphOptimizationLevel( GraphOptimizationLevel::ORT_ENABLE_ALL); if (use_gpu) { OrtCUDAProviderOptions cuda_opts; cuda_opts.device_id 0; opts.AppendExecutionProvider_CUDA(cuda_opts); } // 加载检测和识别模型 det_sess_ std::make_uniqueOrt::Session(env_, det_path.c_str(), opts); rec_sess_ std::make_uniqueOrt::Session(env_, rec_path.c_str(), opts); LoadKeys(keys_path); } // 检测输入 BGR 图输出文本框坐标 std::vectorcv::Rect Detect(const cv::Mat img); // 识别输入裁剪图输出文字 std::string Recognize(const cv::Mat crop); private: Ort::Env env_; std::unique_ptrOrt::Session det_sess_; std::unique_ptrOrt::Session rec_sess_; std::vectorstd::string keys_; void LoadKeys(const std::string path); };Ort::Env全局一个就够多个 Session 共享。SetIntraOpNumThreads设成 CPU 物理核数设太大反而因为线程切换变慢。GraphOptimizationLevel::ORT_ENABLE_ALL开启所有图优化首次推理会慢一点做优化之后稳定。GPU 模式下AppendExecutionProvider_CUDA必须在 Session 创建之前调用顺序反了不生效。3.3 预处理和后处理C 端最容易和 Python 结果对不上的地方C 端和 Python 端结果不一致九成出在预处理。Python 里cv2.resize默认双线性插值C 里cv::resize默认也是双线性但如果你在 Python 用了INTER_LINEAR而 C 用了INTER_NEAREST像素值就有差异。统一用cv::INTER_LINEAR。归一化那步Python 的blob (blob - mean) / std是逐通道减C 里要手动写循环或拆通道cv::Mat blob; cv::dnn::blobFromImage(resized, blob, 1.0 / 255.0, cv::Size(), cv::Scalar(), true, false); // blob 现在是 NCHW但没做 mean/std 归一化手动补 float* data blob.ptrfloat(); int channel_size new_h * new_w; float mean[3] {0.485f, 0.456f, 0.406f}; float std_[3] {0.229f, 0.224f, 0.225f}; for (int c 0; c 3; c) { for (int i 0; i channel_size; i) { data[c * channel_size i] (data[c * channel_size i] - mean[c]) / std_[c]; } }cv::dnn::blobFromImage的swapRBtrue把 BGR 转 RGB和 Python 里[:, :, ::-1]等价。后处理检测输出时概率图大于阈值一般 0.3的像素算文字区域然后做连通域分析或轮廓提取得到文本框。识别后处理是 CTC 解码对每个时间步取 argmax去掉重复和 blank再查字典。注意CTC 解码时 blank 的索引通常是 0字典从索引 1 开始对应。如果解码出来第一个字符总是错的检查一下是不是把 blank 当成了有效字符。4. 避坑与排查部署 ONNXRuntime PaddleOCR-v3 的五个血泪教训4.1 坑一onnxruntime 版本和模型 opset 不匹配现象Python 端加载模型报Unsupported model IR version或No Op registered for Resize。原因导出模型时 opset 版本高于当前 onnxruntime 支持的上限或者模型 IR 版本太新。解决先pip show onnxruntime看版本再python -c import onnx; m onnx.load(det.onnx); print(m.opset_import)看模型 opset。onnxruntime 1.12 支持到 opset 151.8 只到 opset 13。版本不匹配就降 opset 重新导出或者升级 onnxruntime。C 端同理头文件和动态库版本必须一致混用不同版本的 include 和 lib 会出各种诡异链接错误。4.2 坑二动态维度设了但推理时 shape 对不上现象检测模型输入 640x640 的图正常换 1280x720 就报Got invalid dimensions for input。原因导出时只设了 batch 动态H、W 写死了。解决导出命令里--input_shape_dict {x: [1, 3, -1, -1]}把 H、W 都设成-1。已经导出的模型可以用onnxsim或polygraphy改但最干净的办法是重新导出。识别模型高度必须固定 48宽度可以动态写成[1, 3, 48, -1]。4.3 坑三C 端推理结果和 Python 差几个像素导致框偏移现象Python 检测框位置准确C 端框整体偏移或大小不对。原因resize 时缩放比例计算方式不同或者 padding 策略不一致。解决统一用「保持长边不超过 limit_side_len短边按比例缩放然后 pad 到 32 的倍数」策略。Python 和 C 各写一个ResizeAndPad函数输入输出尺寸打印出来对比确保完全一致。别一边用 letterbox 一边用直接 resize。4.4 坑四鲲鹏 920 上 onnxruntime 跑不起来或性能极差现象x86 上正常的模型移到鲲鹏 920 报Illegal instruction或推理慢十倍。原因用了 x86 编译的 onnxruntime 动态库或者没开 ARM 的 NEON 优化。解决从源码编译 onnxruntime 的 ARM64 版本编译时开--arm64和--enable_neon。如果只是跑 CPU 推理用CPUExecutionProvider并设SetIntraOpNumThreads为物理核数。鲲鹏 920 是 48 核或 64 核线程数设 8 到 16 之间比较平衡设满反而因为内存带宽瓶颈变慢。4.5 坑五识别结果乱码或漏字现象检测框位置对但识别出来的文字缺笔画或完全不对。原因字典文件不匹配、CTC 解码逻辑错、或者输入图像归一化参数不对。解决先确认ppocr_keys_v1.txt行数和模型输出类别数一致常见是 6623 行。然后检查 CTC 解码连续相同字符只保留一个blank 跳过。最后确认识别模型输入是灰度还是 RGB——PaddleOCR-v3 识别模型输入是 RGB 三通道如果你送了灰度图结果会差很多。5. 进阶技巧用滑动窗口和动态 batch 把长图识别吞吐拉满长图识别是 OCR 落地里最容易被低估的场景。一张 2000x20000 的截图直接 resize 到长边 960 会让文字小到无法识别不 resize 又爆显存。我一般用滑动窗口切分按高度切成 960 像素高的条带相邻条带重叠 128 像素避免文字被切断每条单独检测识别最后按 y 坐标合并结果去重。def sliding_window_ocr(engine, img, window_h960, overlap128): h, w img.shape[:2] results [] y 0 while y h: y2 min(y window_h, h) strip img[y:y2, :] boxes engine.detect(strip) for box in boxes: text engine.recognize(strip[box[1]:box[3], box[0]:box[2]]) results.append((box[0], box[1] y, text)) if y2 h: break y y2 - overlap # 按 y 坐标排序重叠区域去重 results.sort(keylambda r: r[1]) deduped [] for r in results: if not deduped or abs(r[1] - deduped[-1][1]) 10: deduped.append(r) return dedupedwindow_h设 960 是因为检测模型在这个尺寸下对小文字最敏感overlap设 128 是经验值太小会切断文字太大浪费算力。去重阈值 10 像素是按常见行高估的行高小的文档可以调到 5。动态 batch 是另一个吞吐优化点。识别模型支持 batch 推理把多个文本行拼成一个 batch 送进去比逐行推理快 3 到 5 倍。C 端构造输入时把[1, 3, 48, W]扩成[N, 3, 48, W]注意所有样本的 W 要 pad 到同一尺寸。Python 端用np.stack拼 batchonnxruntime 会自动处理。# 动态 batch 识别把 N 个文本行拼成一个 batch def recognize_batch(engine, crops): max_w max(c.shape[1] for c in crops) batch np.zeros((len(crops), 3, 48, max_w), dtypenp.float32) for i, c in enumerate(crops): resized cv2.resize(c, (max_w, 48)) blob resized[:, :, ::-1].astype(np.float32) / 255.0 blob (blob - 0.5) / 0.5 batch[i] blob.transpose(2, 0, 1) outputs engine.rec_sess.run(None, {engine.rec_input: batch}) return [ctc_decode(o, engine.keys) for o in outputs[0]]batch 大小别超过 16再大显存收益递减且延迟增加。CPU 推理时 batch 4 到 8 比较合适。验证方法很简单拿一张长图分别用逐行推理和 batch 推理跑对比总耗时和识别结果是否一致。如果结果有差异检查 pad 的像素值——pad 用 0 还是 255 会影响识别PaddleOCR-v3 训练时 pad 用的是 0所以推理也 pad 0。我自己踩过最深的坑是滑动窗口的 overlap 设太小一张合同截图里「甲方」两个字被切成了「甲」和「方」分别识别合并时又没去重结果输出两遍。后来把 overlap 调到 128 并加了 y 坐标去重才解决。做 OCR 部署预处理和后处理的细节比模型本身更决定成败多花半小时对齐 Python 和 C 的每一步能省掉后面几小时的排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表