
1. 项目背景与整体方案选型1.1 为什么选 RDK X5 跑 YOLOv11地平线 RDK X5 是面向边缘计算场景的开发套件板载 BPUBrain Processing Unit专门为神经网络推理做了优化。跟纯 CPU 或者 GPU 方案相比它的优势在于功耗和算力比非常突出尤其适合做机器人、智能相机、工业视觉这类需要把模型部署到现场的设备。那为什么用 YOLOv11这代模型在检测精度和推理速度之间拿捏得比较好而且相比之前几个版本C3k2 模块和训练策略都有改进小目标的召回率也有提升。实际测试下来在同等输入分辨率下YOLOv11 的 mAP 明显好于 YOLOv8而模型体积没有明显变大。再加上 Ultralytics 框架的生态已经相当成熟训练、导出、评估一条龙都很顺手社区资料也足够多遇到问题基本都能查到解决方案。不过有个现实问题RDK X5 的 BPU 不认识 PyTorch 的权重格式甚至不认识标准 ONNX 里的很多算子。所以完整的部署链路其实是这样的——先在 PC 上用 GPU 训练 YOLOv11导出 ONNX再用地平线工具链把 ONNX 转换成 BPU 能高效执行的 bin 模型最后在板子上用推理库加载 bin 做推理。整个过程踩坑不少本文就把每一步的关键细节和参数选择讲清楚。1.2 整体技术链路拆解很多第一次接触边缘部署的朋友会以为“在板子上装个 PyTorch 然后 load 权重就行”这是最大的误区。RDK X5 上虽然也能装 PyTorch但 CPU 推理 YOLOv11 的速度大概只有 2-3 FPS完全没法实际使用。正确的路径是训练PC/GPU - 导出 ONNX - 模型转换工具链hb_mapper - bin 模型 - 板端推理hobot_dnn其中 ONNX 是一个开放的模型交换格式地平线的工具链以 ONNX 作为输入自己做算子映射、图优化、量化最终生成 BPU 指令集。所以你要做的核心事情有两件一是导出工具链能识别的 ONNX二是准备好转换时需要的校准数据。在进入具体步骤之前先说说我选择这套方案的几个考量用 Docker 而不是直接在 PC 上安装工具链因为地平线的工具链依赖特定版本的 Python、protobuf、numpy 等库直接装在系统里容易跟现有环境冲突。Docker 容器隔离得干干净净用完就扔。训练阶段用官方 COCO 预训练权重做微调而不是从零开始训练。YOLOv11 的特征提取层已经学得很好了微调只需要很少的数据量和时间。转换时优先保证输入尺寸统一比如 640x640不要在 ONNX 里留动态维度BPU 对静态 shape 的支持最成熟性能也最好。2. 开发环境与 Docker 镜像准备2.1 安装 Docker 并配置国内镜像源Docker 是整套流程的基础。如果你的 PC 是 Linux 系统Ubuntu 20.04/22.04 都行安装命令很简单sudo apt update sudo apt install docker.io sudo systemctl enable --now docker sudo usermod -aG docker $USER装完之后记得退出重新登录让 docker 组权限生效。如果系统是 Windows 或者 macOS直接去 Docker 官网下载 Docker Desktop 安装即可。然后是最让人头疼的镜像下载问题。地平线的工具链镜像在 Docker Hub 上国内直连下载速度经常只有几十 KB/s拉一个几个 GB 的镜像能等一晚上。这里推荐两种加速方式我都试过效果不错第一种是配置镜像加速器。编辑/etc/docker/daemon.json{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com, https://docker.nju.edu.cn ] }改完重启 Dockersudo systemctl restart docker第二种是用镜像加速工具。有一些开源项目可以把 Docker Hub 的镜像同步到国内节点拉取速度能到几十 MB/s。不过这类工具时效性变化比较快建议优先用第一种方式实在不行再搜一下当前可用的镜像加速方案。注意配置完镜像源后用docker info查看 Registry Mirrors 一栏是否已经生效。如果拉取还是慢可以试试直接设置 HTTP 代理或者找一台海外服务器中转。2.2 拉取地平线工具链镜像地平线的开发文档里会明确给出工具链 Docker 镜像的地址和 tag。以我这次用的版本为例docker pull hub.hobot.cc/ai_toolchain/horizon_x5_toolchain:latest这里要特别说明如果你在 Docker Hub 上搜不到这个镜像别急地平线的镜像是放在自家或者指定的私有仓库里的需要按照官方文档的指引去拉取。有些版本还会要求先注册开发者账号获取下载权限。拉取完成后启动容器的时候有几个关键参数要设置好docker run -it \ --name x5_toolchain \ -v /home/yourname/yolo11_project:/workspace \ -v /home/yourname/calibration_data:/calibration_data \ --privileged \ hub.hobot.cc/ai_toolchain/horizon_x5_toolchain:latest \ /bin/bash参数说明-v把宿主机的工程目录挂载进容器这样模型文件、校准图片都可以在两边共享。这是非常重要的因为容器一旦删除里面的文件就全没了。--privileged给容器更高的权限某些版本的工具链在跑模型转换时需要访问设备的调试接口或者做内存映射不加这个参数可能会报权限错误。如果后续要连接板子做联调启动容器时还需要加--networkhost让容器直接使用宿主机网络方便 SSH 到板子。2.3 容器内环境自检进入容器后先做一次环境自检确保工具链可用python3 --version hb_mapper --help如果hb_mapper命令找不到检查一下环境变量export PATH/opt/ai_toolchain/usr/bin:$PATH source /opt/ai_toolchain/usr/setup_env.sh注意每次重新进入容器都要重新 source 环境除非你把它写进容器的.bashrc。我当时就是在.bashrc里加了一行省了很多麻烦。3. YOLOv11 模型训练与 ONNX 导出3.1 训练环节的关键配置训练 YOLOv11 用 Ultralytics 框架最省事。安装方式pip install ultralytics我这次的项目是检测输送带上的产品缺陷属于典型的小样本工业场景总共才标注了 800 多张图片包含 4 个类别。训练命令如下yolo detect train \ modelyolo11n.pt \ data/path/to/dataset.yaml \ epochs100 \ imgsz640 \ batch16 \ device0 \ workers4有几个参数值得展开说modelyolo11n.pt选择 nano 版本因为最终要跑到边缘设备上模型越小推理越快。如果你的精度不够再往上升级到 yolo11s 或者 yolo11m但要注意 RDK X5 的 BPU 算力是固定的模型越大帧率越低。imgsz640训练和推理的输入尺寸尽量保持一致。有人在训练时用 640导出 ONNX 时又改成 416这样做精度损失往往比想象中大因为模型已经适应了训练时的感受野分布。epochs100对小数据集来说 100 轮足够了配合早停机制patience20一般 60 轮左右就能收敛。训练完成后在runs/detect/train/weights/目录下会生成best.pt和last.pt我们只需要best.pt。3.2 导出 ONNX 时的参数选择ONNX 导出是整个部署链路中最容易出问题的一环。用 Ultralytics 自带的方法导出from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) model.export( formatonnx, opset11, imgsz640, dynamicFalse, simplifyTrue )每个参数背后的原因opset11不是越高越好。地平线工具链对 ONNX 算子有自己支持列表过高的 opset 可能会引入工具链不支持的新算子。实测下来 opset 11 是最稳的某些高版本导出后的模型反而会报算子不支持。dynamicFalse把输入 shape 固定死。BPU 推理要求静态 shape动态 shape 在转换时往往需要额外的配置而且性能会打折。simplifyTrue用 onnx-simplifier 对计算图做简化能合并一些冗余节点、去掉多余的 reshape 和 transpose。这个操作对后续转换非常有帮助能让工具链的算子匹配更顺畅。导出之后的 ONNX 文件建议先用下面这段代码做个快速检查import onnx model onnx.load(best.onnx) onnx.checker.check_model(model) print(onnx.helper.printable_graph(model.graph))检查有两个目的一是确认模型结构完整二是看输出节点的名字。YOLOv11 导出的 ONNX 输出节点通常是三个分别对应 80x80、40x40、20x20 的特征图名字形如/model.22/Concat_output_0这种。记下准确的输出节点名后面配置转换 yaml 时要用。3.3 一个容易忽略的问题将输出拆分为两个分支直接导出的 YOLOv11 ONNX每个输出节点的 shape 是[1, 4num_classes, 80, 80]这种格式也就是推理结果里同时包含了检测框信息和类别概率。但地平线的模型转换工具链对这类多任务融合输出的支持不够好最常见的做法是在 ONNX 层面把输出拆成两个分支分支一检测框回归结果shape 为[1, 4, 80, 80]分支二类别分类结果shape 为[1, num_classes, 80, 80]这个拆分可以通过修改 Ultralytics 的导出脚本或者在 ONNX 图里手动操作来实现。我当时用的方案是在导出脚本里加一个自定义 forward 方法直接输出两个分支。这里贴一个参考写法import torch from ultralytics.nn.tasks import DetectionModel class SplitHeadModel(DetectionModel): def forward(self, x): y self.model(x) box torch.cat([yi[..., :4] for yi in y], dim1) cls torch.cat([yi[..., 4:] for yi in y], dim1) return box, cls这样导出的 ONNX 就有两个干净的输出分支转换时不需要额外的预处理工具链也能比较轻松地完成量化。4. 从 ONNX 到地平线 bin 模型的转换4.1 转换流程总览拿到 ONNX 文件之后就要进入本项目的核心环节——模型转换。地平线的工具链提供了一个叫hb_mapper的命令行工具核心功能是把 ONNX 模型分析、优化、量化并编译成 BPU 可执行的指令文件。转换完成后会生成.bin文件这个才是板子能加载的模型。整个转换过程可以拆成三个步骤模型分析检查 ONNX 里哪些算子能落到 BPU 上执行哪些必须用 CPU 兜底。校准量化用一批有代表性的图片统计每一层激活值的分布区间把 float32 的权重和激活值量化成 int8/int16。编译生成 BPU 指令并打包成 bin 模型。4.2 准备校准图片校准这一步特别容易让人一头雾水但它直接决定模型在板子上的精度表现。为什么要校准因为 float32 转 int8 之后需要知道每一层输出的数值范围。这个范围是拿一堆真实图片喂进去统计出来的不是拍脑袋定的。校准图片的要求数量一般 50 到 200 张太多了浪费转换时间太少了统计不准。我当时用了 100 张来自验证集的图片。图片内容要有代表性尽量覆盖各种光照、角度、目标大小和类别分布。如果你校准图片里全是小目标那模型对中大型目标的数值范围就估计得不准。图片尺寸不需要统一工具链会自己在预处理阶段做 resize 和归一化。把图片放到一个目录里然后在 yaml 配置里指定这个目录的路径。4.3 转换 yaml 配置详解下面是实测过的一版配置关键字段都加了注释model_type: onnx input_model: /workspace/yolo11_best_split.onnx output_model_prefix: yolo11_defect # 输入相关配置 input_shape: [1, 3, 640, 640] # 预处理配置 norm_type: data_scale data_mean: [0, 0, 0] data_scale: 0.003921569 # 1/255 # 校准配置 calibration_type: max calibration_data_dir: /calibration_data calibration_batch_size: 1 # 编译配置 optimize_level: O3 max_batch_size: 1 # 输出相关 out_dtype: int8参数选择的逻辑norm_type: data_scale配合data_scale: 0.003921569意思是板端推理时直接把输入像素值乘以 1/255。因为训练时我们就是这么做的推理时也必须保持一致否则数值分布会偏精度直接崩掉。有些配置会用data_mean: [123.675, 116.28, 103.53]和data_std: [58.395, 57.12, 57.375]这是 ImageNet 的标准化参数。关键是要跟训练时的预处理对齐。YOLOv11 默认的预处理是简单的缩放到 0-1不需要减均值所以这里设为 0 最合适。calibration_type: max是一种量化校准方式按最大值做映射。还有percentile等选项区别在于对长尾分布的处理。工业场景下 prefer 用percentile能稍微减少极端值对量化区间的影响但max更稳定我最终用了max。4.4 执行转换并解读关键输出运行转换命令hb_mapper makertbin --config yolo11_defect.yaml转换过程的日志非常长重点关注几个地方第一个是算子映射表。日志会列出每个算子的归置情况——是在 BPU 上执行还是 fallback 到 CPU。如果 CPU 算子太多说明 ONNX 导出时有些操作没做干净建议回炉简化模型。第二个是量化前后的精度对比。工具链会跑一些量化层的余弦相似度cosine similarity一般 0.98 以上算正常低于 0.95 就要警惕了。如果遇到这种问题优先检查预处理参数和校准图片是否和训练时一致。转换完成后目录下会生成yolo11_defect.bin。在正式部署前推荐用工具链自带的模拟器先跑一遍推理验证模型逻辑是否正确hobot_model_tool --model-file yolo11_defect.bin --input test.jpg --output result.jpg模拟器的输出如果跟 PC 上 PyTorch 推理的结果基本一致就可以放心的拷到板子上了。5. 板端部署与推理实现5.1 将模型和代码部署到 RDK X5我是通过 SSH 连上板子的ssh root192.168.1.10接下来把 bin 模型拷过去可以用 scpscp yolo11_defect.bin root192.168.1.10:/root/models/板端推理代码最常用的库是hobot_dnn。下面这段是完整的推理代码框架from hobot_dnn import pyeasy_dnn as dnn import numpy as np import cv2 # 加载模型 model dnn.load(/root/models/yolo11_defect.bin) # 获取输入输出信息 print(model.inputs[0].properties) print(model.outputs[0].properties) # 读取图像并预处理 img cv2.imread(test.jpg) img_resized cv2.resize(img, (640, 640)) input_data img_resized.transpose((2, 0, 1))[None, ...].astype(np.float32) input_data * 0.003921569 # 推理 outputs model.run(input_data) # 解码后处理注意几个细节hobot_dnn的输入和输出都是numpy数组model.run()接收的是一个 list返回的也是 list。输入数据的 layout 要和转换时一致。如果转换时配的是 NCHW那这里也要用transpose((2, 0, 1))把 HWC 变成 CHW。前处理做了 float 转换和缩放因为 yaml 里配置了data_scale所以这一步必须由用户自己完成板端不会自动做。5.2 后处理解码、NMS 和坐标映射模型输出的原始结果不能直接画框要做后处理。这部分跟在 PC 上训练时的后处理逻辑一样只是需要自己写一遍实现。核心步骤解出所有候选框的坐标、置信度和类别概率。根据置信度阈值过滤低分框。对每个类别做 NMS非极大值抑制去掉重叠框。把坐标从 640x640 的网络输入尺寸映射回原图尺寸。坐标映射的公式比较简单x_scale orig_w / 640.0 y_scale orig_h / 640.0 x1 int(box[0] * x_scale) y1 int(box[1] * y_scale) x2 int(box[2] * x_scale) y2 int(box[3] * y_scale)这里容易出现的坑是输出坐标可能是归一化的0~1也可能是像素值取决于你导出 ONNX 时的 head 结构。如果是归一化的要先乘 640 再映射回原图。NMS 的实现可以直接用cv2.dnn.NMSBoxes不过要注意 OpenCV 版本导致的返回值格式差异。我自己的经验是直接用numpy实现一个简单的 NMS 更可控代码量也不大def nms(boxes, scores, iou_threshold0.45): x1 boxes[:, 0] y1 boxes[:, 1] x2 boxes[:, 2] y2 boxes[:, 3] areas (x2 - x1) * (y2 - y1) order scores.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) inter w * h iou inter / (areas[i] areas[order[1:]] - inter 1e-6) order order[np.where(iou iou_threshold)[0] 1] return keep5.3 实际性能与优化方向我在 RDK X5 上实测YOLOv11n 输入 640x640纯 BPU 推理耗时约 35~40ms加上前后处理整体大约 50ms也就是 20 FPS 左右。如果对延迟有更高要求有两个优化方向第一是降低输入分辨率。把输入改到 480x480推理耗时能降到 25ms 左右但精度也会有相应的下降需要评估你的业务能不能接受。第二是减少输出层的计算量。可以只保留两个输出分支去掉大目标检测层或者改成单类检测后端解码的时间能大幅减少。注意修改后需要重新导出 ONNX 并重新转换模型。另外对 hobot_dnn 的调用方式也有讲究。如果你用 Python 的model.run()每次单帧推理帧率会受 Python 解释器开销影响。可以试试model.run_inference()批量推理模式或者把整个推理逻辑用 C 重写板子上性能还能再拉上去一截。6. 常见问题与实操踩坑记录6.1 问题速查表问题现象可能原因解决办法拉取 Docker 镜像超时网络问题Docker Hub 访问不稳定配置国内镜像加速器或代理hb_mapper 命令找不到环境变量未配置执行 setup_env.sh 或手动配置 PATH提示 ONNX 算子不支持导出的 ONNX 用了不支持的算子换 opset 11 并开启 simplify转换时提示 calibration 数据为空校准图片路径错误或格式不支持检查目录权限图片使用 jpg/png转换后的模型精度下降明显预处理参数不一致或校准图片代表性不足核对 data_scale/data_mean增加校准图片数量板端推理结果全为零输入 layout 不对或数据未做归一化检查 NCHW 顺序确认前处理缩放推理速度远低于预期CPU 算子过多重新导出 ONNX简化网络结构6.2 三个印象最深的坑第一个坑是 docker 容器内跑hb_mapper一直报权限错误。排查了很久才发现是容器启动时没加--privileged参数USB 设备访问权限不够。如果你在转换过程中要连接板子做在线编译一定要记得加这个参数。第二个坑是校准图片选得不好。最开始我图省事直接用训练集的图片做校准结果模型在板上的 mAP 比 GPU 上掉了将近 8 个点。后来换成验证集的 100 张图覆盖面更广精度损失降到 2 个点以内。校准集的选择真的不能马虎尽可能覆盖多种难度的样本。第三个坑是 ONNX 输出没有拆分支。第一次转换时输出层是三个特征图 concat 在一起的格式工具链在量化编译时 CPU 算子特别多推理速度只有 5 FPS。后来把输出拆成 box 和 cls 两个分支BPU 算子占用率大幅提升速度直接翻了几倍。6.3 提升迭代效率的几个小技巧整套流程跑顺之后效率提升的关键在于减少来回试错的时间。我实测下来比较有效的做法是写一个一键转换的脚本把模型分析、校准、编译、模拟验证全部串起来。这样每次改完模型或者配置跑一遍脚本就能看到结果不用手动敲一条条命令。#!/bin/bash set -e echo 1. Checking ONNX model... python3 check_onnx.py yolo11_best_split.onnx echo 2. Running hb_mapper... hb_mapper makertbin --config yolo11_defect.yaml echo 3. Validating with simulator... hobot_model_tool --model-file yolo11_defect.bin --input test.jpg --output result.jpg echo Done!另外建议把常见报错和解决办法整理成自己的速查笔记尤其是算子不支持的映射表后面遇到类似模型能省很多查文档的时间。在这个项目里我还有一个体会是流程里的每一步都要做验证不要等最后统一查问题。导出 ONNX 后先跑一下 onnx.checker转换完先跑一下模拟器连板子之前先跑一下 PC 端的模拟推理。每一步都确认无误整个流程会顺畅很多不然排错的成本会成倍增加。RDK X5 的地平线工具链这几年迭代力度不错算子覆盖率一直在提升YOLOv11 这种主流检测模型已经能转得很干净了剩下的就是多调配置、多积累经验的事。