RDK X5实战:YOLOv5模型从ONNX到地平线BPU部署全流程

发布时间:2026/10/7 5:31:23
RDK X5实战:YOLOv5模型从ONNX到地平线BPU部署全流程
RDK X5板子到手之后我第一件事就是把之前训练好的YOLOv5模型搬上去跑实时推理。整个过程走下来最花时间的不是训练也不是部署而是中间的模型转换环节。这套工具链的坑确实比想象中多但摸清楚了之后整个流程其实非常固定。这篇文章就把我从训练到部署的完整过程复盘一遍重点放在ONNX到地平线模型的转换细节上给正在折腾RDK X5的兄弟们一份可以直接照着抄的作业。内容涉及数据集准备、YOLOv5训练、ONNX导出修正、地平线OE工具链转换、板端推理部署以及我踩过的各种报错和排查思路。无论你之前有没有接触过地平线平台只要对YOLOv5的训练流程有一定了解这篇文章都能帮你少走不少弯路。1. 为什么选这套组合RDK X5的定位与YOLOv5的取舍1.1 旭日X5芯片和RDK X5板卡到底什么水平先聊硬件。RDK X5用的是地平线旭日X5芯片八核ARM Cortex-A55处理器主频1.5GHz左右板载BPU算力标称10 TOPS。光看CPU部分它和树莓派5没有本质差别真正的核心优势在BPU上——这玩意是专门跑神经网络推理的加速单元能效比远高于用CPU、GPU硬扛。实际使用中YOLOv5s模型输入640x640在BPU上单帧推理可以做到十几毫秒级别这个速度在树莓派上用CPU跑是想都不敢想的。RDK X5还支持双路MIPI CSI摄像头接入板载千兆网口、USB 3.0、HDMI输出接口配置比较齐全。做边缘端视觉检测项目比如安全帽识别、缺陷检测、人流统计这套板子的性价比是相当能打的。从整个部署链路来看芯片厂商给你的是完整的工具链。训练用什么框架无所谓PyTorch也好、TensorFlow也好最终都要导出成ONNX再通过地平线的OE工具链做模型优化、量化和编译生成BPU能直接执行的模型文件。RDK X5官方部署推理库是hobot-dnn提供Python和C接口调用方式比较友好。1.2 为什么用YOLOv5而不是更新版本我知道肯定有人要问现在YOLOv8、YOLOv11都出来了为什么还抱着YOLOv5不放。这里有个很现实的原因模型转换工具链对算子的兼容性是有滞后性的。YOLOv5的结构相对传统C3模块、SPPF、Detect头都是非常标准的卷积、BatchNorm、Concat、Resize组合这些算子在ONNX导出和地平线编译工具链里支持得最成熟。YOLOv8以上的版本引入了C2f模块、解耦头、DFLDistribution Focal Loss等新结构导出ONNX后会产生一些比较新的算子比如DFL的cumsum、softmax组合这些算子在地平线工具链的某些版本里要么不支持要么转换效率很低跑出来的模型性能会打折扣。当然如果你用的是较新版本的地平线OE包YOLOv8也能转换但很多情况下需要手动改写Detect层工程成本直接上升。实际上只要是把模型部署到嵌入式端不管是瑞芯微、地平线还是海思用YOLOv5s作为主力检测模型都是非常稳妥的选择。模型体积小、精度够用、算子类型简单转换工具链支持好部署后帧率也高。做工业级边缘检测项目稳定性远比追新版本重要。我个人的原则是训练侧可以用任何框架部署侧能不上新结构就不上新结构YOLOv5s永远是第一候选。2. 训练端的准备工作从数据集到ONNX模型2.1 环境搭建与数据集标注规范部署走不通大概率是训练端埋了雷。我这里把训练环节也完整过一遍因为最后的ONNX导出质量直接取决于训练时的配置。YOLOv5的训练环境一般用Python 3.8搭配PyTorch 1.10以上的组合。官方仓库的requirements.txt列了所有依赖pip install -r requirements.txt就能搞定。这里建议使用固定的版本组合不要用最新版CUDA、最新版PyTorch因为YOLOv5仓库已经很稳定了新版依赖反而可能出现CUDA算子兼容问题。数据集格式方面YOLOv5用的是最经典的文件夹结构dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml标注工具我用的是LabelImg输出YOLO格式的txt文件每一行对应一个目标框格式是class_id x_center y_center width height坐标值都是归一化到0到1之间的相对坐标。这里有个很容易忽略的细节坐标必须是相对于图片宽高的比例不是像素坐标导出的时候千万确认LabelImg的保存格式选的是YOLO而不是PascalVOC。data.yaml文件指定训练集、验证集路径和类别名。比如做安全帽检测train: ./dataset/images/train val: ./dataset/images/val nc: 2 names: [helmet, person]训练之前还要确认数据集中有没有空标注的图片、有没有损坏的图片。我一般会写个脚本扫一遍把无法加载的图片和对应标签剔除掉否则训练到一半会莫名其妙中断。另外一个建议是标注时尽量让目标框贴合目标边缘不要留太多背景YOLO训练对边界框回归的宽容度其实没那么高标得越准最终mAP越高。2.2 训练参数与超参数调整训练命令本身不复杂但参数选择很影响最终精度。我常用的启动命令长这样python train.py \ --weights yolov5s.pt \ --data dataset/data.yaml \ --epochs 200 \ --batch-size 32 \ --imgsz 640 \ --device 0这里有几个关键参数要说一下。--imgsz直接决定了输入分辨率如果后续部署到RDK X5上跑实时推理建议训练时就固定640。640分辨率在精度和BPU推理速度之间是最均衡的。--batch-size取决于你的显卡显存如果显存不够就调小但不要低于16否则BN层统计不稳定会拉低模型精度。--weights yolov5s.pt是从COCO预训练权重开始微调。很多人纠结要不要用预训练权重我的建议是除非你的数据集特别特殊比如医学图像否则都用预训练权重。迁移学习能极大加速收敛尤其是数据集规模较小的时候最终精度也更高。超参数文件hyp.scratch-low.yaml是YOLOv5的默认配置包含学习率、动量、权重衰减、数据增强参数等。如果数据集难度较大可以适当增大hsv_h、hsv_s这些颜色增强参数来提高泛化能力但增强过头会导致训练集精度上不去。我通常在默认参数基础上只调整lr0和lrf学习率初始值设为0.01最终学习率lrf设为0.01这个组合在大多数数据集上都比较稳定。训练过程中主要盯着两个指标mAP0.5和mAP0.5:0.95。前者0.9以上基本够用后者则是更严格的评估标准。一般训练150到200个epoch就能收敛如果到后期mAP还在明显增长可以适当增加epoch数但要注意过拟合风险。训练完成后runs/train/exp/weights/best.pt就是精度最好的权重文件。2.3 导出ONNX前必须处理的三个细节训练完成之后就是导出ONNX这一步是整个转换流程的第一个坑点。如果直接拿官方仓库的export.py导出得到的ONNX模型大概率在地平线工具链里会报错或者转出来精度不对。我在实际项目中总结了三个必须检查的细节。第一个细节是opset版本。导出的命令是python export.py \ --weights runs/train/exp/weights/best.pt \ --include onnx \ --opset 11 \ --imgsz 640 \ --batch-size 1opset必须用11不能用更高的版本。地平线工具链对ONNX opset 11支持最完整opset 12以上的某些算子比如Split的num_outputs变体、Resize的coordinate_transformation_mode处理方式在低版本工具链里兼容性不好转换时会直接报算子不支持。第二个细节是导出时--batch-size 1和固定输入尺寸。RDK X5的BPU要求模型输入形状是静态的动态shape在转换时要么报错要么推理时性能极差。所以导出时强制固定batch size为1、输入分辨率为640x640不要加--dynamic参数。这个和GPU上推理的思路完全不同GPU可以利用动态batch并发推理但BPU不需要这些它要求的是极致的静态优化。第三个细节是检查输出的检测头形状。YOLOv5导出ONNX后输出层默认会包含三个尺度的检测结果最终拼成一个(1, 25200, 85)的tensor。85的含义是4个边界框坐标回归量 1个目标置信度 80个类别概率。如果是自定义类别数c才把85替换为5c。板端部署做后处理时需要把这么大的tensor拆分、过滤、做NMS这个在后处理阶段会详细展开。但导出ONNX之后我建议用onnx库或Netron先看一下输出节点的名字和shape确认输出层没问题再进入转换环节import onnx model onnx.load(best.onnx) for node in model.graph.output: print(node.name) print(node.type)这里有个经常搞混的概念YOLOv5的导出模型中包含了NMS吗实际上官方export.py默认不带NMS输出就是原始的预测tensor。如果你用的版本带了--nms选项导出的ONNX会包含后处理模块这种模型在地平线工具链里几乎无法转换因为NMS操作是非线性且动态的BPU根本不支持。切记导出时不要打开NMS选项NMS留在板端部署用CPU做。3. 模型转换核心从ONNX到地平线量化模型3.1 工具链安装和版本选择ONNX准备好之后就是整个流程最核心的部分——用地平线的OE工具链做模型转换。这里的OE包指的是OpenExplorer工具链官方也叫地平线开发工具箱它本质上是一个Python库集合基于Conda环境运行。安装方式在官方文档里写得很清楚大致流程是去地平线开发者社区下载对应开发板的OE包解压后创建Conda环境并安装。RDK X5对应的是tbd_tools或horizon_model_convert_sample这一系列工具包具体版本号会随SDK版本变化。关键点在于工具链版本必须和板端系统镜像版本配套否则转换出来的模型在板端加载时会报版本不匹配的错。我踩过这个坑用新版本工具链转出来的hbm模型加载到旧版本hobot-dnn上直接报model version mismatch重新对版本之后问题才消失。创建环境并验证工具链可用conda create -n horizon python3.8 conda activate horizon pip install horizon_toolkit-*.whl hb_mapper --help官方文档里建议用Python 3.8不要用更高的版本因为工具链依赖的某些so库在3.9、3.10环境里会编译报错。如果你的机器上装了多个Python版本用Conda隔离是最省心的做法。3.2 模型转换配置文件怎么填地平线工具链的转换流程核心是写一个yaml配置文件通过hb_mapper makertbin命令执行。这个yaml文件定义了模型的输入输出信息、量化参数、校准数据集、优化策略等指令直接决定了转换出来的模型质量。一个典型的配置文件骨架是这样的model_parameters: onnx_model: ./yolov5s.onnx march: bayes layer_out_dump: False working_dir: ./model_output output_model_file_prefix: yolov5s input_parameters: input_type_rt: nv12 input_layout_rt: nhwc input_type_train: rgb input_layout_train: nchw norm_type: data_scale scale_value: 0.00392156862745098 calibration_parameters: cal_data_dir: ./calibration_data cal_data_type: float32 compiler_parameters: compile_mode: batch-1 optimize_level: O3 debug: False core_num: 2这里解释几个容易踩坑的字段。march字段必须填bayes这是旭日X5的BPU架构代号。填错了工具链会直接报架构不匹配或者生成无法加载的模型。input_type_rt和input_layout_rt表示板端推理时的输入数据格式。我建议填nv12和nhwc因为RDK X5的摄像头采集接口直接输出NV12图像数据这样板端代码里就不需要先转成RGB再做推理省掉一次耗时操作。但如果你板端用Python读图习惯性地输RGB数据那这里就填rgb和nchw两种方式在最终精度上差异不大主要看哪边处理流水线更顺。norm_type和scale_value这里是一个大坑。YOLOv5训练时通常把输入像素值直接除以255也就是归一化到0到1之间。所以scale_value填0.00392156862745098就是1/255。但这里要注意工具链的归一化计算方式是把输入像素乘以scale_value与训练时的预处理方式必须保持一致否则精度会掉得特别离谱。我见过有人配置成norm_type: mean_std结果推理出来的检测框位置全部偏移排查了半天才发现是归一化方式没对上。3.3 校准数据集的准备与转换命令量化是转换流程里最影响精度的环节。BPU推理是定点计算需要把FP32的权重和激活值转换为INT8而校准数据集的作用是统计激活值的分布范围从而确定最佳的量化参数。这里准备校准数据的方法在训练集的验证集里随机抽取100到200张图片把bmp或者jpg图像存到calibration_data目录下。不需要打标签只需要原始图像即可因为校准过程只要前向推理得到激活分布不需要计算损失。但这里有个关键细节校准图像的预处理必须和训练时一致。YOLOv5训练时会对图像做letterbox等比缩放并填充灰边使输入尺寸统一为640x640。校准图像同样要先做letterbox再存盘否则模型看到的输入分布和训练时不一致量化精度自然受影响。准备完校准数据后执行转换命令hb_mapper makertbin --config yolov5s_config.yaml转换过程会输出大量日志包括每一层的量化误差分析、模型总耗时预估、内存占用等。转换完成后会在working_dir下生成多个文件。关键的产物是yolov5s.bin最终上板推理的模型文件yolov5s.hbm带模型结构信息的打包文件调试时用model_info.yaml记录模型输入输出信息和性能评估结果训练输出目录和转换产物之间的关系这里梳理一下。很多人在这个环节分不清best.pt、best.onnx和yolov5s.bin几者的区别。简单来说best.pt是PyTorch的权重文件只能在PyTorch环境里加载best.onnx是跨平台的神经网络描述文件产业链上所有工具链的输入基本都是它yolov5s.bin是地平线BPU专用的可执行模型只有RDK X5这类地平线芯片能加载。三者的关系是递进转换中途任何一个环节出错后面的部署就无从谈起。4. 板端部署把模型跑在RDK X5上4.1 镜像烧录与基础环境配置模型转换完成下一个战场是RDK X5板卡本身。首先得给板卡烧写系统镜像。RDK X5支持从SD卡启动和从eMMC启动两种方式开发调试阶段我建议用SD卡启动方便随时换系统。烧录工具用balenaEtcher就行选择对应的系统镜像文件选择SD卡设备直接烧录。烧录镜像的版本要和你工具链版本对应这个在上面已经强调过。烧录完成后把SD卡插进RDK X5接上电源、网线、HDMI显示器板卡启动后会进入系统桌面。开发阶段我们最常用的连接方式是SSHssh sunrise192.168.1.10默认用户名和密码在官方文档里有通常是sunrise。首次登录后建议立即修改密码并且用sudo apt update sudo apt upgrade更新一下系统软件源但这步不要轻易做系统全量升级因为地平线相关的内核模块和驱动如果一起被系统升级替代掉很可能会导致BPU设备节点无法加载。板端验证BPU设备是否正常工作ls /dev/hb*如果能列出/dev/hb-vp等相关节点说明BPU驱动已经正常加载可以进行后续部署。4.2 推理代码框架hobot-dnn加载模型地平线为RDK系列开发板提供了Python版本的推理库hobot_dnn接口封装得比较友好。核心类就是hobot_dnn的DNNTensor相关接口加载模型文件后直接执行推理。一个最基本的推理代码如下from hobot_dnn import pyeasy_dnn as dnn import numpy as np import cv2 # 加载模型 model dnn.load(./yolov5s.bin) # 读取输入图片 img cv2.imread(test.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 模型输入shape以转换时配置的为准 input_shape (640, 640, 3) # 推理 outputs model.run(img_ready)这里有一个很重要的预处理问题。如果用hobot_dnn的Python接口直接输入numpy数组输入数据的内存布局最好和转换配置里的input_layout_rt一致。如果转换配置用nhwc你输入的numpy数组的shape就应该是(640, 640, 3)。输入数据还需要做letterbox和归一化归一化方式就是除以255这个必须与转换配置中的norm_type对应。实际项目中摄像头采集的图像通常是1920x1080或1280x720的分辨率直接喂给模型会尺寸不匹配。在板端我们也要和训练时一样做letterbox操作def letterbox(img, new_shape(640, 640)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value(114, 114, 114)) return img这个letterbox操作是YOLOv5预处理的核心训练、校准、板端推理三个环节必须保持一致坐标转换才会准确。4.3 摄像头实时推理与结果解析实时推理场景下我们要使用地平线提供的摄像头管理库hobot_codec采集MIPI摄像头画面再用hobot_dnn推理最后解析输出结果并做后处理。摄像头采集可以用官方示例里的libsrcampy库它提供了Python接口直接读取MIPI摄像头的NV12数据。这里不展开具体摄像头配置因为不同型号摄像头的主板上电时序和设备树配置不同需要参考你手上摄像头的驱动说明。核心后处理代码是解码模型输出。YOLOv5的输出tensor是(1, 25200, 85)我们首先需要解析出每个预测框。完整后处理不包括在模型里要在CPU侧做置信度过滤和NMSdef postprocess(outputs, conf_thres0.25, iou_thres0.45): predictions outputs[0].reshape((25200, 85)) class_conf predictions[:, 5:].max(axis1) class_pred predictions[:, 5:].argmax(axis1) conf_mask (predictions[:, 4] * class_conf) conf_thres detections predictions[conf_mask] class_ids class_pred[conf_mask] confs (predictions[:, 4] * class_conf)[conf_mask] boxes detections[:, :4] # 坐标从中心点格式转成xyxy boxes_xyxy np.zeros_like(boxes) boxes_xyxy[:, 0] boxes[:, 0] - boxes[:, 2] / 2 boxes_xyxy[:, 1] boxes[:, 1] - boxes[:, 3] / 2 boxes_xyxy[:, 2] boxes[:, 0] boxes[:, 2] / 2 boxes_xyxy[:, 3] boxes[:, 1] boxes[:, 3] / 2 # NMS按类别执行 keep [] for cls in np.unique(class_ids): cls_mask class_ids cls cls_boxes boxes_xyxy[cls_mask] cls_confs confs[cls_mask] idx nms(cls_boxes, cls_confs, iou_thres) keep.extend(np.where(cls_mask)[0][idx]) return boxes_xyxy[keep], confs[keep], class_ids[keep]这里输出的是归一化坐标需要映射回原始图像分辨率。映射方式是先把坐标乘以模型的640然后除以letterbox的缩放比例再减掉letterbox的padding偏移。这一步写不对画出来的检测框就会整体偏移但很多人一开始很难发现这个问题。实测下来RDK X5跑YOLOv5s 640x640输入在BPU上的推理耗时大约20毫秒上下加上摄像头采集、预处理、后处理整条流水线跑到30帧每秒以上是没问题的。如果只追求检测速度不追求画质把输入分辨率降到320x320帧率还能再翻倍。5. 踩坑记录与性能调优5.1 转换阶段最常见的报错和排查思路整个流程走下来我整理了一个高频问题速查表这些问题在我自己的项目以及好几个读者反馈中反复出现。问题现象可能原因解决方案转换时报unexpected opONNX包含工具链不支持的算子检查ndoes列表常见于opset版本过高或带有NMS导出转换时报shape mismatch输入尺寸有动态维度导出ONNX时确保固定batch和分辨率不要加dynamic校准阶段报数据加载失败校准图预处理与模型输入不符校准图必须做letterbox和归一化转换成功但板端加载报版本错工具链版本与板端hobot-dnn不匹配统一OE包和系统镜像版本板端推理精度大幅下降归一化配置不匹配或校准图太少检查scale_value校准图建议200张左右检测框位置偏移letterbox坐标映射不正确后处理映射需要还原padding和缩放unexpected op是我遇到频率最高的报错。处理方法是先定位到无法支持的算子在ONNX模型搜索算子的位置然后想办法用工具链支持的等价算子替换。常见问题集中在Resize的坐标转换模式、Upsample、以及某些Concat轴拼接方式。YOLOv5如果导出时用了过高opsetResize算子很容易变成ResizeCastRoIAlign组合这类组合在BPU上处理得很吃力。5.2 精度掉点与量化参数调优心得转成INT8量化模型后精度掉一点是正常的。我的经验是正常的数据集上量化后mAP0.5掉点一般控制在2%到3%以内。如果掉点超过5%说明量化配置有问题。第一个排查点永远是校准数据集。校准图的质量比数量更重要。有些人随便从网上抓了一堆图片做校准这些图片的亮度分布、目标尺度与真实场景差异太大导致激活值统计不准量化精度崩盘。校准图必须从验证集或测试集里随机抽样保证覆盖各种光照、角度、目标大小。第二个排查点是scale_value和预处理的一致性。这一点前面反复强调但值得再次提醒训练、校准、板端推理三处的数据预处理必须严格一致任何一处不一致都会导致精度下降。我建议在转换完模型后先在板端用单张图片做前向对比比如取同一样本分别跑原始PyTorch模型和量化模型对比输出tensor的分布差异。如果差异过大优先检查预处理链路。第三个思路是使用optimize_level和混合精度量化。配置里optimize_level: O3会优化推理速度但有可能对精度造成影响。如果精度掉得太狠可以降低优化等级或者使用层粒度混合精度量化把对量化敏感的关键层保持FP16或FP32。地平线工具链最近几个版本对混合量化的支持越来越好这个功能值得研究。5.3 性能优化的实战建议部署稳定之后就轮到性能调优。RDK X5的BPU算力就那么多如何把帧率压榨到极限有下面几个非常实战的建议。第一个建议是优先使用NV12输入格式。MIPI摄像头采集的原始数据就是NV12如果模型转换时配置了input_type_rt: nv12板端就可以直接把摄像头数据送进BPU省掉一次RGB转换。这个操作看似简单但实测能省出约一到两毫秒的CPU时间在整条流水线中占比不小。第二个建议是多线程流水线设计。把摄像头采集、模型推理、后处理分到三个不同线程里用队列做数据传递。这样不会因为后处理耗时导致采集线程阻塞实际帧率会明显提升。采集线程负责拿帧推理线程调用BPU后处理线程做NMS和逻辑判断。用Python写这套多线程逻辑时注意GIL问题可以借助multiprocessing把后处理放到独立进程但工程复杂度会上升看需求决定。第三个建议是绑核。RDK X5是8核A55多核调度下推理任务可能在不同核心间切换导致缓存命中率下降。使用taskset绑核把推理进程固定到某几个核心上性能会有一定提升。板端实测绑核前后推理耗时能差百分之十左右不是玄学是真的有效。最后一个建议是用C重写关键路径。如果Python版本的上限不满足性能要求可以把后处理逻辑用C实现通过pybind11绑定到Python层。也可以直接使用地平线官方提供的C示例代码hobot_dnn的C接口性能比Python版本好不少。考虑到YOLOv5的后处理NMS是纯CPU操作C重写后整体帧率的提升非常显著。现在我个人的流程已经固定下来了训练用YOLOv5s导出ONNX用固定shape和opset11校准图从验证集抽200张并保持与训练完全一致的预处理转换配置里用NV12输入板端用多线程流水线接摄像头。这套流程跑通了多个项目包括安全帽识别、工服检测和园区车辆计数每次部署到新的RDK X5板子上都能在半天内完成从烧录到出结果的整个过程。对于刚开始接触地平线平台的开发者我的建议是先严格按照默认配置走通一遍流程再考虑混合精度、多线程优化这些进阶项。工具链的可视化debug功能也建议用起来模型各层的输出都能dump出来作对比分析排查问题会快很多。