Jetson Orin Nano 部署 PaddleOCR 终极方案ONNX OpenCV DNN6倍提速设备: 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)CPU6核 ARM Cortex-A78AEGPUAmpere GA10B, 1024 CUDA cores, SM 8.7内存8GB LPDDR5 (CPU/GPU 统一)系统Ubuntu 22.04, L4T R36.5.0, JetPack 6.0CUDA12.6.68cuDNN9.3.0OpenCV4.10.0 (自编译, CUDA 加速)Python3.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.3FP16 反而更慢Orin Nano 无专用 FP16 Tensor CoreCPU 模式 SegfaultARM 没有 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 DNNJetson 上 OpenCV 已编译 CUDA 加速版含 DNN CUDA 后端无需额外依赖无需 PaddlePaddle 运行时推理速度实测 67ms/检测 25ms/识别框为什么选 onnxsimOpenCV 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.jsoninference.pdiparams。Step 4: PIR → ONNX 导出importpaddleimportos os.makedirs(models,exist_okTrue)# 导出检测模型 det_modelpaddle.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],dtypefloat32,namex)],opset_version14,)# 导出识别模型 rec_modelpaddle.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],dtypefloat32,namex)],opset_version14,)⚠️坑 1:paddle.jit.load()参数是文件前缀inference不是目录传目录会报KeyError: forward。⚠️坑 2:export()的第二个参数不要带.onnx后缀否则文件会变成.onnx.onnx。Step 5: ONNX 模型简化importonnxfromonnxsimimportsimplifyfornamein[PP-OCRv5_mobile_det,PP-OCRv5_mobile_rec]:modelonnx.load(fmodels/{name}.onnx)model_simp,checksimplify(model)onnx.save(model_simp,fmodels/{name}_sim.onnx)print(f{name}: simplified, check{check})简化后 OpenCV DNN 才能加载。Step 6: 准备字符字典从识别模型的inference.yml提取importyamlwithopen(PP-OCRv5_mobile_rec_infer/inference.yml)asf:configyaml.safe_load(f)# 只保存 18383 个字符, blank 和 space 由 CTCLabelDecode 自动添加charsconfig[PostProcess][character_dict]withopen(models/ppocr_keys_v1.txt,w,encodingutf-8)asf:forcinchars:f.write(c\n)⚠️坑 3: 字典文件只需 18383 字符。CTCLabelDecode(use_space_charTrue)会自动添加 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_size960):PP-OCRv5 检测预处理: resize_long960, NormalizeImage, padding到32倍数h,wimg.shape[:2]ratiotarget_size/max(h,w)new_h,new_wint(h*ratio),int(w*ratio)# Padding 到 32 的倍数 (DBNet 下采样 1/32)pad_h(32-new_h%32)%32pad_w(32-new_w%32)%32imgcv2.resize(img,(new_w,new_h))imgcv2.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]imgimg.astype(np.float32)/255.0meannp.array([0.485,0.456,0.406],dtypenp.float32)stdnp.array([0.229,0.224,0.225],dtypenp.float32)img(img-mean)/std# HWC → CHW → BCHWimgimg.transpose(2,0,1)imgnp.expand_dims(img,axis0).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,wimg.shape[:2]target_h,target_wtarget_shape# 保持宽高比, 高度缩放到 48ratiotarget_h/h new_wint(w*ratio)ifnew_w3200:# ⚠️ 不是 320! C 默认 max_imgW3200new_w3200imgcv2.resize(img,(new_w,target_h))# CHW RecResizeImg 内置归一化: (x/255 - 0.5) / 0.5 → [-1, 1]imgimg.astype(np.float32).transpose(2,0,1)imgimg/255.0img(img-0.5)/0.5# 宽度填充 (仅当 target_w)ifnew_wtarget_w:padnp.zeros((3,target_h,target_w-new_w),dtypenp.float32)imgnp.concatenate([img,pad],axis2)imgnp.expand_dims(img,axis0).astype(np.float32)returnimg⚠️坑 5: 识别模型max_imgW默认3200不是 320设置为 320 会导致长文本如登机牌底部小字被严重压缩识别为空。⚠️坑 6: 识别预处理中没有独立的NormalizeImage但RecResizeImg内置了(x/255 - 0.5) / 0.5的归一化操作。不要漏掉也不要多做。7.4 模型加载与 CUDA 后端det_netcv2.dnn.readNetFromONNX(models/PP-OCRv5_mobile_det_sim.onnx)rec_netcv2.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 等可选依赖specimportlib.util.spec_from_file_location(db_pp,PaddleOCR-main/ppocr/postprocess/db_postprocess.py)modimportlib.util.module_from_spec(spec)spec.loader.exec_module(mod)DBPostProcessmod.DBPostProcess# 同样加载 CTCLabelDecodespec2importlib.util.spec_from_file_location(rec_pp,PaddleOCR-main/ppocr/postprocess/rec_postprocess.py)mod2importlib.util.module_from_spec(spec2)spec2.loader.exec_module(mod2)CTCLabelDecodemod2.CTCLabelDecode7.6 中文可视化⚠️坑 7:cv2.putText()的 Hershey 字体不支持中文中文字会空白fromPILimportImage,ImageDraw,ImageFont# 系统自带 Noto Serif CJK 中文字体fontImageFont.truetype(/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc,16)# OpenCV BGR → PIL RGB → 绘制中文 → 转回 OpenCV BGRvis_rgbcv2.cvtColor(img,cv2.COLOR_BGR2RGB)vis_pilImage.fromarray(vis_rgb)drawImageDraw.Draw(vis_pil)draw.text((x,y),text,fontfont,fill(0,255,0))viscv2.cvtColor(np.array(vis_pil),cv2.COLOR_RGB2BGR)Step 8: 解决 ONNX 检测框碎片化⚠️坑 8: ONNX 输出的概率图比 PaddlePaddle 原生稍碎片化导致多出 ~8 个检测框。添加相邻框合并算法def_merge_adjacent(boxes,scores):并查集合并水平紧邻框nlen(boxes)parentlist(range(n))deffind(x):whileparent[x]!x:parent[x]parent[parent[x]]xparent[x]returnxdefunion(a,b):pa,pbfind(a),find(b)ifpa!pb:parent[pb]paforiinrange(n):binp.array(boxes[i])yi_min,yi_maxbi[:,1].min(),bi[:,1].max()xi_maxbi[:,0].max()hiyi_max-yi_minforjinrange(i1,n):bjnp.array(boxes[j])yj_min,yj_maxbj[:,1].min(),bj[:,1].max()xj_minbj[:,0].min()hjyj_max-yj_min# 垂直重叠 50%y_overlapmin(yi_max,yj_max)-max(yi_min,yj_min)ify_overlapmin(hi,hj)*0.5:continue# 水平间距 平均高度 × 2x_gapxj_min-xi_max avg_h(hihj)/2if0x_gapavg_h*2:union(i,j)# ... 按组合并 ...效果38 框 → 30 框对齐 C 基准。六、性能测试测试环境图片: 896×528, 30 个文本区域预热 2 次后取 20 次平均结果方案检测识别总耗时提速C Paddle Inference~2s~3s~5.5s基准OpenCV DNN CUDA (本项目)100ms900ms~1.0s6×每框耗时分解裁剪 (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_level2: 构建 2 分钟但输出全零计算错误optimization_level3: 构建 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_size960): 100ms(±8ms)识别(单框):25.5ms 识别(30框): 765ms 总耗时: ~900ms八、完整代码完整推理脚本200 行可在项目中找到gitclonerepo_urlcdocrsourcevenv_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— 完整技术文档九、踩坑速查表#现象原因解决1paddle.jit.load(dir)报KeyError: forwardPIR 格式需文件前缀load(f{dir}/inference)2ONNX 文件名变xxx.onnx.onnxexport自动加后缀传入不带后缀的路径3cv2.dnn.readNetFromONNX报Reshape错误OpenCV DNN 不支持某些算子onnxsim 简化4系统import cv2报_ARRAY_APINumPy 2.x vs 1.x 冲突pip install numpy25pip opencv-python 覆盖 CUDA 版pip 依赖自动安装pip uninstall opencv-python -y6识别全部乱码字典维度不匹配CTCLabelDecode(use_space_charTrue)7图片上中文空白cv2.putText 不支持中文PIL 中文字体8长文本识别为空max_imgW 设为 320改为 32009检测框比基准多 8 个ONNX 概率图碎片化相邻框合并算法10TensorRT 引擎输出全零optimization_level2bug放弃 TRT, 用 OpenCV DNN十、总结在 Jetson Orin Nano (JetPack 6) 上部署 PaddleOCRONNX OpenCV DNN是目前最实用的方案✅速度快: 比 C Paddle Inference 快 6 倍✅精度好: 对齐原生 PaddlePaddle 输出✅依赖少: 不需要 PaddlePaddle 运行时✅纯 Python: 不需要编译 C 代码✅可维护: 代码清晰, 后处理复用 PaddleOCR 原生类❌ TensorRT 在此设备上不实用编译太慢❌ 批处理不支持模型架构限制完整文档和代码已开源欢迎 Star ⭐作者: CedarQ日期: 2026-06-09设备: NVIDIA Jetson Orin Nano Super项目地址: [GitHub]
Jetson Orin Nano 部署 PaddleOCR 终极方案:ONNX + OpenCV DNN,6倍提速!
Jetson Orin Nano 部署 PaddleOCR 终极方案ONNX OpenCV DNN6倍提速设备: 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)CPU6核 ARM Cortex-A78AEGPUAmpere GA10B, 1024 CUDA cores, SM 8.7内存8GB LPDDR5 (CPU/GPU 统一)系统Ubuntu 22.04, L4T R36.5.0, JetPack 6.0CUDA12.6.68cuDNN9.3.0OpenCV4.10.0 (自编译, CUDA 加速)Python3.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.3FP16 反而更慢Orin Nano 无专用 FP16 Tensor CoreCPU 模式 SegfaultARM 没有 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 DNNJetson 上 OpenCV 已编译 CUDA 加速版含 DNN CUDA 后端无需额外依赖无需 PaddlePaddle 运行时推理速度实测 67ms/检测 25ms/识别框为什么选 onnxsimOpenCV 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.jsoninference.pdiparams。Step 4: PIR → ONNX 导出importpaddleimportos os.makedirs(models,exist_okTrue)# 导出检测模型 det_modelpaddle.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],dtypefloat32,namex)],opset_version14,)# 导出识别模型 rec_modelpaddle.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],dtypefloat32,namex)],opset_version14,)⚠️坑 1:paddle.jit.load()参数是文件前缀inference不是目录传目录会报KeyError: forward。⚠️坑 2:export()的第二个参数不要带.onnx后缀否则文件会变成.onnx.onnx。Step 5: ONNX 模型简化importonnxfromonnxsimimportsimplifyfornamein[PP-OCRv5_mobile_det,PP-OCRv5_mobile_rec]:modelonnx.load(fmodels/{name}.onnx)model_simp,checksimplify(model)onnx.save(model_simp,fmodels/{name}_sim.onnx)print(f{name}: simplified, check{check})简化后 OpenCV DNN 才能加载。Step 6: 准备字符字典从识别模型的inference.yml提取importyamlwithopen(PP-OCRv5_mobile_rec_infer/inference.yml)asf:configyaml.safe_load(f)# 只保存 18383 个字符, blank 和 space 由 CTCLabelDecode 自动添加charsconfig[PostProcess][character_dict]withopen(models/ppocr_keys_v1.txt,w,encodingutf-8)asf:forcinchars:f.write(c\n)⚠️坑 3: 字典文件只需 18383 字符。CTCLabelDecode(use_space_charTrue)会自动添加 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_size960):PP-OCRv5 检测预处理: resize_long960, NormalizeImage, padding到32倍数h,wimg.shape[:2]ratiotarget_size/max(h,w)new_h,new_wint(h*ratio),int(w*ratio)# Padding 到 32 的倍数 (DBNet 下采样 1/32)pad_h(32-new_h%32)%32pad_w(32-new_w%32)%32imgcv2.resize(img,(new_w,new_h))imgcv2.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]imgimg.astype(np.float32)/255.0meannp.array([0.485,0.456,0.406],dtypenp.float32)stdnp.array([0.229,0.224,0.225],dtypenp.float32)img(img-mean)/std# HWC → CHW → BCHWimgimg.transpose(2,0,1)imgnp.expand_dims(img,axis0).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,wimg.shape[:2]target_h,target_wtarget_shape# 保持宽高比, 高度缩放到 48ratiotarget_h/h new_wint(w*ratio)ifnew_w3200:# ⚠️ 不是 320! C 默认 max_imgW3200new_w3200imgcv2.resize(img,(new_w,target_h))# CHW RecResizeImg 内置归一化: (x/255 - 0.5) / 0.5 → [-1, 1]imgimg.astype(np.float32).transpose(2,0,1)imgimg/255.0img(img-0.5)/0.5# 宽度填充 (仅当 target_w)ifnew_wtarget_w:padnp.zeros((3,target_h,target_w-new_w),dtypenp.float32)imgnp.concatenate([img,pad],axis2)imgnp.expand_dims(img,axis0).astype(np.float32)returnimg⚠️坑 5: 识别模型max_imgW默认3200不是 320设置为 320 会导致长文本如登机牌底部小字被严重压缩识别为空。⚠️坑 6: 识别预处理中没有独立的NormalizeImage但RecResizeImg内置了(x/255 - 0.5) / 0.5的归一化操作。不要漏掉也不要多做。7.4 模型加载与 CUDA 后端det_netcv2.dnn.readNetFromONNX(models/PP-OCRv5_mobile_det_sim.onnx)rec_netcv2.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 等可选依赖specimportlib.util.spec_from_file_location(db_pp,PaddleOCR-main/ppocr/postprocess/db_postprocess.py)modimportlib.util.module_from_spec(spec)spec.loader.exec_module(mod)DBPostProcessmod.DBPostProcess# 同样加载 CTCLabelDecodespec2importlib.util.spec_from_file_location(rec_pp,PaddleOCR-main/ppocr/postprocess/rec_postprocess.py)mod2importlib.util.module_from_spec(spec2)spec2.loader.exec_module(mod2)CTCLabelDecodemod2.CTCLabelDecode7.6 中文可视化⚠️坑 7:cv2.putText()的 Hershey 字体不支持中文中文字会空白fromPILimportImage,ImageDraw,ImageFont# 系统自带 Noto Serif CJK 中文字体fontImageFont.truetype(/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc,16)# OpenCV BGR → PIL RGB → 绘制中文 → 转回 OpenCV BGRvis_rgbcv2.cvtColor(img,cv2.COLOR_BGR2RGB)vis_pilImage.fromarray(vis_rgb)drawImageDraw.Draw(vis_pil)draw.text((x,y),text,fontfont,fill(0,255,0))viscv2.cvtColor(np.array(vis_pil),cv2.COLOR_RGB2BGR)Step 8: 解决 ONNX 检测框碎片化⚠️坑 8: ONNX 输出的概率图比 PaddlePaddle 原生稍碎片化导致多出 ~8 个检测框。添加相邻框合并算法def_merge_adjacent(boxes,scores):并查集合并水平紧邻框nlen(boxes)parentlist(range(n))deffind(x):whileparent[x]!x:parent[x]parent[parent[x]]xparent[x]returnxdefunion(a,b):pa,pbfind(a),find(b)ifpa!pb:parent[pb]paforiinrange(n):binp.array(boxes[i])yi_min,yi_maxbi[:,1].min(),bi[:,1].max()xi_maxbi[:,0].max()hiyi_max-yi_minforjinrange(i1,n):bjnp.array(boxes[j])yj_min,yj_maxbj[:,1].min(),bj[:,1].max()xj_minbj[:,0].min()hjyj_max-yj_min# 垂直重叠 50%y_overlapmin(yi_max,yj_max)-max(yi_min,yj_min)ify_overlapmin(hi,hj)*0.5:continue# 水平间距 平均高度 × 2x_gapxj_min-xi_max avg_h(hihj)/2if0x_gapavg_h*2:union(i,j)# ... 按组合并 ...效果38 框 → 30 框对齐 C 基准。六、性能测试测试环境图片: 896×528, 30 个文本区域预热 2 次后取 20 次平均结果方案检测识别总耗时提速C Paddle Inference~2s~3s~5.5s基准OpenCV DNN CUDA (本项目)100ms900ms~1.0s6×每框耗时分解裁剪 (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_level2: 构建 2 分钟但输出全零计算错误optimization_level3: 构建 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_size960): 100ms(±8ms)识别(单框):25.5ms 识别(30框): 765ms 总耗时: ~900ms八、完整代码完整推理脚本200 行可在项目中找到gitclonerepo_urlcdocrsourcevenv_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— 完整技术文档九、踩坑速查表#现象原因解决1paddle.jit.load(dir)报KeyError: forwardPIR 格式需文件前缀load(f{dir}/inference)2ONNX 文件名变xxx.onnx.onnxexport自动加后缀传入不带后缀的路径3cv2.dnn.readNetFromONNX报Reshape错误OpenCV DNN 不支持某些算子onnxsim 简化4系统import cv2报_ARRAY_APINumPy 2.x vs 1.x 冲突pip install numpy25pip opencv-python 覆盖 CUDA 版pip 依赖自动安装pip uninstall opencv-python -y6识别全部乱码字典维度不匹配CTCLabelDecode(use_space_charTrue)7图片上中文空白cv2.putText 不支持中文PIL 中文字体8长文本识别为空max_imgW 设为 320改为 32009检测框比基准多 8 个ONNX 概率图碎片化相邻框合并算法10TensorRT 引擎输出全零optimization_level2bug放弃 TRT, 用 OpenCV DNN十、总结在 Jetson Orin Nano (JetPack 6) 上部署 PaddleOCRONNX OpenCV DNN是目前最实用的方案✅速度快: 比 C Paddle Inference 快 6 倍✅精度好: 对齐原生 PaddlePaddle 输出✅依赖少: 不需要 PaddlePaddle 运行时✅纯 Python: 不需要编译 C 代码✅可维护: 代码清晰, 后处理复用 PaddleOCR 原生类❌ TensorRT 在此设备上不实用编译太慢❌ 批处理不支持模型架构限制完整文档和代码已开源欢迎 Star ⭐作者: CedarQ日期: 2026-06-09设备: NVIDIA Jetson Orin Nano Super项目地址: [GitHub]