Jetson Orin Nano 部署 PaddleOCR 终极方案:ONNX + OpenCV DNN,6倍提速!
设备: NVIDIA Jetson Orin Nano Super (8GB)
效果: 比 C++ Paddle Inference 快6 倍,单图 ~1 秒
核心: PP-OCRv5 Mobile → ONNX → OpenCV DNN CUDA
一、前言
在 Jetson Orin Nano 上部署 PaddleOCR 做文字识别,网上能找到的教程基本都指向 C++ Paddle Inference 方案。但实际跑下来,速度只有5.5 秒/图,完全没法用。
PaddlePaddle 官方至今没有发布 JetPack 6 (L4T 36.x) 的 GPU 预编译包。没有 GPU 加速,OCR 在嵌入设备上就是慢动作。
经过一整天的折腾,我找到了一条可行的路:将 PP-OCRv5 模型导出为 ONNX,用 OpenCV DNN + CUDA 做推理。最终速度提升了6 倍,从 5.5s 降到 1s 以内。
这篇文章记录了完整过程,包括 8 个大坑和解决方案。
二、环境说明
| 项目 | 参数 |
|---|---|
| 设备 | NVIDIA Jetson Orin Nano Super (P3767-0005) |
| CPU | 6核 ARM Cortex-A78AE |
| GPU | Ampere GA10B, 1024 CUDA cores, SM 8.7 |
| 内存 | 8GB LPDDR5 (CPU/GPU 统一) |
| 系统 | Ubuntu 22.04, L4T R36.5.0, JetPack 6.0 |
| CUDA | 12.6.68 |
| cuDNN | 9.3.0 |
| OpenCV | 4.10.0 (自编译, CUDA 加速) |
| Python | 3.10.12 |
三、为什么不用 PaddlePaddle GPU?
一句话:PaddlePaddle 没有 JetPack 6 的 GPU 预编译包。
| 平台 | PaddlePaddle GPU 预编译 |
|---|---|
| x86_64 Linux CUDA 12 | ✅ |
| Jetson JetPack 5.x (CUDA 11.4) | ✅ |
| Jetson JetPack 6.x (CUDA 12.6) | ❌ |
有人用 JetPack 5.x 的预编译包在 JetPack 6 上跑(利用 CUDA 向后兼容),但这种方式:
- TensorRT 不兼容(预编译 TRT 8.5 vs 系统 TRT 10.3)
- FP16 反而更慢(Orin Nano 无专用 FP16 Tensor Core)
- CPU 模式 Segfault(ARM 没有 Intel MKL)
- 速度 5.5s/图,太慢
从源码编译 PaddlePaddle 理论上可行,但耗时 4+ 小时且需要 8GB+ 内存,失败率高。
四、核心思路
PP-OCRv5 Mobile 模型 (PIR格式) │ ▼ paddle.onnx.export() ← 导出 ONNX │ ▼ onnxsim simplify() ← 简化模型 │ ▼ OpenCV DNN + CUDA ← GPU 推理 │ ▼ PaddleOCR 原生后处理 ← 复用 DTPostProcess + CTCLabelDecode │ ▼ 输出: JSON 文本 + 可视化图片为什么选 OpenCV DNN?
- Jetson 上 OpenCV 已编译 CUDA 加速版(含 DNN CUDA 后端)
- 无需额外依赖,无需 PaddlePaddle 运行时
- 推理速度实测 67ms/检测 + 25ms/识别框
为什么选 onnxsim?
OpenCV DNN 的 ONNX 解析器比较挑剔,部分算子不支持。原始 ONNX 加载直接报Reshape错误,onnxsim简化后可以正常加载。
五、Step by Step 实战
Step 1: 创建虚拟环境
cd/home/ysdhanji/ocr python3-mvenv venv_paddleocrsourcevenv_paddleocr/bin/activate pipinstall--upgradepipStep 2: 安装依赖
# PaddlePaddle CPU 版 (GPU 不可用)pipinstallpaddlepaddle-fhttps://www.paddlepaddle.org.cn/whl/linux/aarch64/cpu/stable.html# PaddleOCRpipinstallpaddleocr# paddle2onnx (--no-deps 跳过 onnxoptimizer 编译)pipinstallpaddle2onnx --no-deps# ONNX 工具链pipinstallonnx onnxsim onnxruntimeStep 3: 准备 PIR 模型
PP-OCRv5 模型可以从 PaddleOCR 官方下载:
wgethttps://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/PP-OCRv5_mobile_det_infer.tarwgethttps://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/PP-OCRv5_mobile_rec_infer.tartarxf PP-OCRv5_mobile_det_infer.tartarxf PP-OCRv5_mobile_rec_infer.tar模型是 PIR 格式(PaddlePaddle 3.x 新格式),包含inference.json+inference.pdiparams。
Step 4: PIR → ONNX 导出
importpaddleimportos os.makedirs('models',exist_ok=True)# === 导出检测模型 ===det_model=paddle.jit.load('PP-OCRv5_mobile_det_infer/inference')# 注意: 文件前缀, 不是目录!det_model.eval()paddle.onnx.export(det_model,'models/PP-OCRv5_mobile_det',# 不带.onnx后缀!input_spec=[paddle.static.InputSpec(shape=[1,3,-1,-1],dtype='float32',name='x')],opset_version=14,)# === 导出识别模型 ===rec_model=paddle.jit.load('PP-OCRv5_mobile_rec_infer/inference')rec_model.eval()paddle.onnx.export(rec_model,'models/PP-OCRv5_mobile_rec',input_spec=[paddle.static.InputSpec(shape=[1,3,-1,-1],dtype='float32',name='x')],opset_version=14,)⚠️坑 1:
paddle.jit.load()参数是文件前缀inference,不是目录!传目录会报KeyError: 'forward'。
⚠️坑 2:export()的第二个参数不要带.onnx后缀,否则文件会变成.onnx.onnx。
Step 5: ONNX 模型简化
importonnxfromonnxsimimportsimplifyfornamein['PP-OCRv5_mobile_det','PP-OCRv5_mobile_rec']:model=onnx.load(f'models/{name}.onnx')model_simp,check=simplify(model)onnx.save(model_simp,f'models/{name}_sim.onnx')print(f'{name}: simplified, check={check}')简化后 OpenCV DNN 才能加载。
Step 6: 准备字符字典
从识别模型的inference.yml提取:
importyamlwithopen('PP-OCRv5_mobile_rec_infer/inference.yml')asf:config=yaml.safe_load(f)# 只保存 18383 个字符, blank 和 space 由 CTCLabelDecode 自动添加chars=config['PostProcess']['character_dict']withopen('models/ppocr_keys_v1.txt','w',encoding='utf-8')asf:forcinchars:f.write(c+'\n')⚠️坑 3: 字典文件只需 18383 字符。
CTCLabelDecode(use_space_char=True)会自动添加 blank + space → 18385 维,匹配模型输出。
Step 7: 编写推理脚本
完整的推理脚本包含以下关键部分:
7.1 系统 OpenCV 导入
pip 版opencv-python没有 CUDA!必须用系统自编译版:
importsys sys.path.insert(0,'/usr/local/lib/python3.10/dist-packages')importcv2⚠️坑 4: 如果之前 pip install 过 opencv-python,会导致 NumPy 版本冲突。需要卸载 pip 版,降级 NumPy 到 1.x。
7.2 检测预处理
defdet_preprocess(img,target_size=960):"""PP-OCRv5 检测预处理: resize_long=960, NormalizeImage, padding到32倍数"""h,w=img.shape[:2]ratio=target_size/max(h,w)new_h,new_w=int(h*ratio),int(w*ratio)# Padding 到 32 的倍数 (DBNet 下采样 1/32)pad_h=(32-new_h%32)%32pad_w=(32-new_w%32)%32img=cv2.resize(img,(new_w,new_h))img=cv2.copyMakeBorder(img,0,pad_h,0,pad_w,cv2.BORDER_CONSTANT,value=(114,114,114))# 归一化: mean=[0.485,0.456,0.406] std=[0.229,0.224,0.225]img=img.astype(np.float32)/255.0mean=np.array([0.485,0.456,0.406],dtype=np.float32)std=np.array([0.229,0.224,0.225],dtype=np.float32)img=(img-mean)/std# HWC → CHW → BCHWimg=img.transpose(2,0,1)img=np.expand_dims(img,axis=0).astype(np.float32)returnimg,(new_h,new_w),(h,w),(pad_h,pad_w)7.3 识别预处理
defrec_preprocess(img,target_shape=(48,320)):"""PP-OCRv5 识别预处理: RecResizeImg 内置归一化"""h,w=img.shape[:2]target_h,target_w=target_shape# 保持宽高比, 高度缩放到 48ratio=target_h/h new_w=int(w*ratio)ifnew_w>3200:# ⚠️ 不是 320! C++ 默认 max_imgW=3200new_w=3200img=cv2.resize(img,(new_w,target_h))# CHW + RecResizeImg 内置归一化: (x/255 - 0.5) / 0.5 → [-1, 1]img=img.astype(np.float32).transpose(2,0,1)img=img/255.0img=(img-0.5)/0.5# 宽度填充 (仅当 < target_w)ifnew_w<target_w:pad=np.zeros((3,target_h,target_w-new_w),dtype=np.float32)img=np.concatenate([img,pad],axis=2)img=np.expand_dims(img,axis=0).astype(np.float32)returnimg⚠️坑 5: 识别模型
max_imgW默认3200,不是 320!设置为 320 会导致长文本(如登机牌底部小字)被严重压缩,识别为空。
⚠️坑 6: 识别预处理中没有独立的NormalizeImage,但RecResizeImg内置了(x/255 - 0.5) / 0.5的归一化操作。不要漏掉,也不要多做。
7.4 模型加载与 CUDA 后端
det_net=cv2.dnn.readNetFromONNX('models/PP-OCRv5_mobile_det_sim.onnx')rec_net=cv2.dnn.readNetFromONNX('models/PP-OCRv5_mobile_rec_sim.onnx')# 设置 CUDA 后端det_net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)det_net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)rec_net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)rec_net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)7.5 后处理(复用 PaddleOCR 原生类)
不用自己写!直接引用 PaddleOCR 源码中的类:
importimportlib.util# 绕过 __init__.py 避免 skimage 等可选依赖spec=importlib.util.spec_from_file_location('db_pp','PaddleOCR-main/ppocr/postprocess/db_postprocess.py')mod=importlib.util.module_from_spec(spec)spec.loader.exec_module(mod)DBPostProcess=mod.DBPostProcess# 同样加载 CTCLabelDecodespec2=importlib.util.spec_from_file_location('rec_pp','PaddleOCR-main/ppocr/postprocess/rec_postprocess.py')mod2=importlib.util.module_from_spec(spec2)spec2.loader.exec_module(mod2)CTCLabelDecode=mod2.CTCLabelDecode7.6 中文可视化
⚠️坑 7:
cv2.putText()的 Hershey 字体不支持中文,中文字会空白!
fromPILimportImage,ImageDraw,ImageFont# 系统自带 Noto Serif CJK 中文字体font=ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc',16)# OpenCV BGR → PIL RGB → 绘制中文 → 转回 OpenCV BGRvis_rgb=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)vis_pil=Image.fromarray(vis_rgb)draw=ImageDraw.Draw(vis_pil)draw.text((x,y),text,font=font,fill=(0,255,0))vis=cv2.cvtColor(np.array(vis_pil),cv2.COLOR_RGB2BGR)Step 8: 解决 ONNX 检测框碎片化
⚠️坑 8: ONNX 输出的概率图比 PaddlePaddle 原生稍碎片化,导致多出 ~8 个检测框。
添加相邻框合并算法:
def_merge_adjacent(boxes,scores):"""并查集合并水平紧邻框"""n=len(boxes)parent=list(range(n))deffind(x):whileparent[x]!=x:parent[x]=parent[parent[x]]x=parent[x]returnxdefunion(a,b):pa,pb=find(a),find(b)ifpa!=pb:parent[pb]=paforiinrange(n):bi=np.array(boxes[i])yi_min,yi_max=bi[:,1].min(),bi[:,1].max()xi_max=bi[:,0].max()hi=yi_max-yi_minforjinrange(i+1,n):bj=np.array(boxes[j])yj_min,yj_max=bj[:,1].min(),bj[:,1].max()xj_min=bj[:,0].min()hj=yj_max-yj_min# 垂直重叠 > 50%y_overlap=min(yi_max,yj_max)-max(yi_min,yj_min)ify_overlap<min(hi,hj)*0.5:continue# 水平间距 < 平均高度 × 2x_gap=xj_min-xi_max avg_h=(hi+hj)/2if0<x_gap<avg_h*2:union(i,j)# ... 按组合并 ...效果:38 框 → 30 框(对齐 C++ 基准)。
六、性能测试
测试环境
- 图片: 896×528, 30 个文本区域
- 预热 2 次后取 20 次平均
结果
| 方案 | 检测 | 识别 | 总耗时 | 提速 |
|---|---|---|---|---|
| C++ Paddle Inference | ~2s | ~3s | ~5.5s | 基准 |
| OpenCV DNN CUDA (本项目) | 100ms | 900ms | ~1.0s | 6× |
每框耗时分解
裁剪 (numpy): 0.1ms ( 0%) 预处理 (resize): 0.8ms ( 1%) setInput (拷贝): 0.1ms ( 0%) 模型推理 (GPU): 79.0ms (97%) ← 瓶颈 CTC解码 (numpy): 1.8ms ( 2%) ───────────────────────── 每框合计: 81.8ms97% 的时间花在 GPU 推理上,CPU 处理几乎不占时间。
为什么不用 TensorRT?
试过了,在 Jetson Orin Nano 8GB 上:
optimization_level=2: 构建 2 分钟,但输出全零(计算错误)optimization_level=3: 构建 3+ 分钟未完成- 识别模型(SVTR Transformer)预计 10+ 分钟
结论: TensorRT 在此设备上不实用。编译太慢,调试周期不可接受。
七、效果展示
识别结果(登机牌测试图)
[0.996] 登机牌 [0.981] BOARDING PASS [0.997] 票价FARE [0.997] 张祺伟 [0.989] ZHANGQIWEI [0.989] 姓名NAME [0.999] 福州 [0.995] FUZHOU [0.996] 航班FLIGHT [0.998] 登机口 [0.999] 日期DATE [1.000] 座位号 [0.959] 登机口于起飞前10分钟关闭 GATESCLOSE10MINUTESBEFORE DEPARTURE TIME性能
$ python ocr_opencv_dnn.py-itest.png--benchmark性能测试(896x528,20次): 检测(det_size=960): 100ms(±8ms)识别(单框):25.5ms 识别(30框): 765ms 总耗时: ~900ms八、完整代码
完整推理脚本(200+ 行)可在项目中找到:
gitclone<repo_url>cdocrsourcevenv_paddleocr/bin/activate python ocr_opencv_dnn.py-iyour_image.jpg关键文件:
ocr_opencv_dnn.py— 主推理脚本export_to_onnx.py— ONNX 导出脚本models/— ONNX 模型 + 字典PP-OCRv5_部署完整记录.md— 完整技术文档
九、踩坑速查表
| # | 现象 | 原因 | 解决 |
|---|---|---|---|
| 1 | paddle.jit.load(dir)报KeyError: 'forward' | PIR 格式需文件前缀 | load(f'{dir}/inference') |
| 2 | ONNX 文件名变xxx.onnx.onnx | export自动加后缀 | 传入不带后缀的路径 |
| 3 | cv2.dnn.readNetFromONNX报Reshape错误 | OpenCV DNN 不支持某些算子 | onnxsim 简化 |
| 4 | 系统import cv2报_ARRAY_API | NumPy 2.x vs 1.x 冲突 | pip install 'numpy<2' |
| 5 | pip opencv-python 覆盖 CUDA 版 | pip 依赖自动安装 | pip uninstall opencv-python -y |
| 6 | 识别全部乱码 | 字典维度不匹配 | CTCLabelDecode(use_space_char=True) |
| 7 | 图片上中文空白 | cv2.putText 不支持中文 | PIL + 中文字体 |
| 8 | 长文本识别为空 | max_imgW 设为 320 | 改为 3200 |
| 9 | 检测框比基准多 8 个 | ONNX 概率图碎片化 | 相邻框合并算法 |
| 10 | TensorRT 引擎输出全零 | optimization_level=2bug | 放弃 TRT, 用 OpenCV DNN |
十、总结
在 Jetson Orin Nano (JetPack 6) 上部署 PaddleOCR,ONNX + OpenCV DNN是目前最实用的方案:
✅速度快: 比 C++ Paddle Inference 快 6 倍
✅精度好: 对齐原生 PaddlePaddle 输出
✅依赖少: 不需要 PaddlePaddle 运行时
✅纯 Python: 不需要编译 C++ 代码
✅可维护: 代码清晰, 后处理复用 PaddleOCR 原生类
❌ TensorRT 在此设备上不实用(编译太慢)
❌ 批处理不支持(模型架构限制)
完整文档和代码已开源,欢迎 Star ⭐
作者: CedarQ
日期: 2026-06-09
设备: NVIDIA Jetson Orin Nano Super
项目地址: [GitHub]