RDK-X5落地YOLOv5与GPS定位:模型转换与板端部署全流程
发布时间:2026/9/28 17:56:58来源:尧图网络
落地边缘端跑YOLOv5最怕的不是模型训不出来而是训好的模型死活搬不上设备。地平线RDK-X5我前后踩了两周坑从镜像刷写、模型转换到GPS模块集成每一步都有不少文档里没写明白的细节。这篇东西我按完整流程整理出来适合手里正好有RDK-X5、想在板子上跑YOLOv5并叠加定位信息的朋友新手照着做能少走弯路老手也能查漏补缺。1. 为什么选RDK-X5跑YOLOv5板卡选型与整体方案设计先交代一下背景。我手上的项目需要在移动小车上做实时目标检测同时把检测结果和地理位置绑定说白了就是“看到什么、在哪看到的”都得记录下来。之前在树莓派5上试过YOLOv5CPU推理一帧要几百毫秒完全跟不上小车移动的速度Jetson Nano虽然能跑CUDA加速但功耗和采购难度都不太友好。地平线RDK-X5是征程6平台的机器人开发者套件内置BPUBrain Processing Unit神经网络加速单元专门干卷积推理这活YOLOv5s量化后跑起来非常轻松整板功耗也就十几瓦配个小电池就能带得动。这套方案的核心链路是RDK-X5跑YOLOv5目标检测GPS模块通过串口输出经纬度检测结果和坐标在板端融合后以结构化数据或者叠加画面上报。相比PC端部署优势在于整机体积小、功耗低、启动快开机就能干活相比树莓派纯CPU方案BPU对卷积算子的加速效果非常明显不服不行。先看一张我自己整理的对比表方便你判断自己该不该选RDK-X5设备推理方式YOLOv5s帧率int8整机功耗部署难度生态成熟度树莓派5CPU/GPU1~3 FPS5~10W低极成熟Jetson NanoCUDA15~25 FPS10~15W中成熟地平线RDK-X5BPU60~80 FPS10~15W中高中等但文档在补纯PCCPUCPU3~5 FPS100W低极成熟RDK-X5这块板子的BPU算力大约10 TOPS实际有效算力要看模型和算子支持情况官方文档的主推路线是“训练→导出ONNX→地平线工具链转换→BPU推理”。所以整体方案从设计上就决定了训练用PyTorch没毛病但最终跑在板子上的必须是从ONNX转出来的.bin模型而不是PyTorch的.pt权重这个思维切换非常关键。选型的时候还有一个容易被忽略的点RDK-X5的系统镜像和工具链绑定比较死。地平线提供了Ubuntu桌面版系统工具链、runtime、examples都在里面版本必须对齐比如我们现在用的OEOpenExplorer工具链是1.0.0a版本就要求板端系统镜像也要是配套的版本。这个我后面会专门说因为版本不一致会引出各种莫名其妙的报错。2. 环境准备与系统烧录一线板卡上手的关键步骤2.1 镜像烧录与开发环境初始化RDK-X5的起步不像树莓派那种“下载镜像、balenaEtcher一写就完事”这么简单。虽然烧录工具也是balenaEtcher但有几个小细节不注意起来会折腾半天。第一SD卡建议选64GB以上的A2卡实际写入速度很重要。我试过用一张Class10的老卡系统启动后IO频繁卡顿模型加载都会超时后来换了闪迪Extreme才稳定。烧录前先用SD Card Formatter做一次全卡格式化别直接用Windows右键格式化文件系统对齐方式不对。第二烧完镜像插卡开机RDK-X5会默认从SD卡启动。首次启动需要接HDMI显示器和USB键盘鼠标完成系统初始化设置这一步不能跳。设置好用户名密码后建议立刻开启SSH服务后面开发全靠远程连接不用一直插着显示器。第三板子联网后第一件事就是检查工具链版本是否和系统镜像版本匹配。终端执行cat /etc/version看系统版本号再去地平线开发者社区找对应的OE工具链版本。如果版本不匹配模型转换工具导出的bin在板端加载会直接报“load model failed”而且报错信息非常不友好不会告诉你到底是版本问题还是文件损坏。我那次排查到最后才发现是版本没对齐浪费了一下午。2.2 开发机与板卡的分工安排部署开发最好准备一台x86 Linux主机Ubuntu 20.04或22.04都行专门用来做模型转换和交叉编译。RDK-X5板端只负责推理和外围设备交互不要指望在板子上直接跑onnx转bin——虽然理论上可以但板端算力和内存都不适合干这种重活。主机上需要装的地平线工具链包括hb_mapper负责把ONNX模型转成地平线异构格式的.bin模型hb_perf模型性能分析工具可以看每一层的推理耗时和BPU占用率hbdk底层编译工具链一般随hb_mapper一起安装工具链安装包在官网上能找到下载后是一个.whl文件加一个.tar.gz的工具包。安装命令很常规pip3 install horizon_onnx-*.whl tar -zxvf hb_toolchain.tar.gz cd hb_toolchain bash install.sh装完以后务必执行hb_mapper --version确认能跑起来。工具链依赖的python版本一般要求3.8以上建议用conda单独建一个虚拟环境不要污染主机系统环境。我在Ubuntu服务器上折腾工具链的时候就因为系统python被改过路径导致hb_mapper找不到依赖库最后是重装了conda环境解决的。3. YOLOv5模型转换全流程从.pt权重到BPU能跑的.bin模型3.1 导出ONNX时的参数选择与常见坑YOLOv5部署到地平线平台第一关是导出ONNX。这一步90%的人会踩坑——直接跑export.py导出的ONNX带着YOLOv5的自定义Detect层包含anchor生成、grid生成等操作这些算子在BPU上根本跑不了。所以导出的时候必须简化模型结构。我用的命令是python export.py --weights yolov5s.pt --include onnx --opset 11 --simplify几个关键参数解释一下--opset 11ONNX算子集版本地平线工具链对opset 11的支持最全太高的版本有些新算子不支持--simplify用onnx-simplifier做计算图简化把一些冗余的reshape、transpose合并掉对后续转换帮助很大不能打开--nms选项NMS非极大值抑制只能在板端CPU上做不能集成进模型导出的ONNX默认输入是[1, 3, 640, 640]通道顺序是RGB这些信息后面转换的时候要用到。YOLOv5s导出后有三个输出节点[1, 255, 80, 80]、[1, 255, 40, 40]、[1, 255, 20, 20]分别对应大中小三个检测头。255 3 × (5 80)其中3是每个grid的anchor数量5是bbox的4个坐标加一个objectness置信度80是COCO数据集的类别数。给这些参数做一次心理有数后面后处理代码里需要精确对应。导出完成后建议用下面这个Python脚本快速验证ONNX模型的输入输出是否正常import onnx import onnxruntime as ort import numpy as np model onnx.load(yolov5s.onnx) onnx.checker.check_model(model) print(输入:, model.graph.input) print(输出:, model.graph.output) # 测试推理 sess ort.InferenceSession(yolov5s.onnx) x np.random.rand(1, 3, 640, 640).astype(np.float32) outs sess.run(None, {images: x}) for i, out in enumerate(outs): print(f输出{i}: shape {out.shape})如果导出后输出的shape对不上或者哪个输出名字变了后面转换和部署铁定出问题提前发现提前修。3.2 使用hb_mapper完成模型转换模型转换是整套流程里最核心也最容易出问题的一步。hb_mapper的输入是ONNX输出是量化后的.bin文件。先准备一个yaml配置文件文件名建议叫yolov5s_config.yaml关键内容如下model_parameters: onnx_model: yolov5s.onnx mar: yolov5s # 模型架构名称自定义 input_names: [images] # 输入节点名和ONNX一致 input_shape: [1, 3, 640, 640] output_names: [output0, output1, output2] # 三个检测头 norm_type: no_preprocess # 板端推理时预先处理输入数据 calibration_parameters: cal_data_dir: calibration_images/ # 校准图像目录 cal_data_type: float32 preprocess_on: False compiler_parameters: compile_mode: latency # 优先低延迟 optimize_level: O3 debug: False这里最容易被忽略的是cal_data_dir校准集的问题。地平线工具链把模型从float32量化成int8需要一个校准过程校准集的分布要尽量贴近模型的实际使用场景。我用的是安全帽检测数据集里抽出来的300张图包含白天、夜晚、不同角度、不同距离的目标这样量化后的模型在真实场景中掉精度不明显。校准图不要直接扔jpg进去需要预处理成模型输入要求的格式。网上有工具脚本我自己写了个简单的import os import cv2 import numpy as np def preprocess(img_path, dst_path): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # letterbox保持宽高比填充 h, w img.shape[:2] scale min(640 / w, 640 / h) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(img, (new_w, new_h)) canvas np.full((640, 640, 3), 114, dtypenp.uint8) canvas[:new_h, :new_w] resized # RGB排列shape为(1, 3, 640, 640) canvas canvas.transpose(2, 0, 1)[None, ...] canvas.tofile(dst_path)准备好校准图后执行转换hb_mapper makertbin --config yolov5s_config.yaml转换过程中会输出每一层的量化精度损失评估重点关注比较大的那几个层是不是集中在检测头附近。如果是可以尝试用混合量化即关键层保留float32只量化其他层。在yaml里通过node_config指定node_config: - name: model.24.m.0 # 第一个检测头模块 precision: float32这个操作对精度恢复很有效唯一代价是推理速度会慢一点。我当时检测头全保留float32帧率从78掉到59精度从mAP 0.72涨到0.78个人觉得完全划算。转换完成会生成一个yolov5s.bin文件这就是最终要部署到RDK-X5上的模型。顺手检查一下生成的yolov5s.html报告里面能看到每个算子在BPU上的运行情况和总耗时预估。3.3 关于YOLOv5网络结构与超参数的补充理解不少同学问过“为什么同样转出来的模型精度差距那么大”这里强烈建议先看一下yolov5s的网络结构图和超参数配置文件data/hyps/hyp.scratch-low.yaml。YOLOv5s是CSPDarknet骨干加PANet颈部加三个Detect头的结构超参数里的anchor_t、box_loss_gain、cls_loss_gain等会影响anchor匹配策略和损失权重这些因素直接决定了模型学到的东西是什么样的。如果你的模型是用默认COCO权重直接部署没太大问题但如果自己训练数据集最好把训练时的anchor设置弄明白因为转换过程中anchor并不会被“塞进”ONNX模型里它是在后处理阶段用的。你必须保证板端后处理用的anchor和训练时一致不然检测框会整体偏大或偏小而且没法通过调阈值修回来。我在部署安全帽数据集时遇到过这个问题训练时用的anchors是从我的数据集聚类出来的但后处理代码里取了YOLOv5默认COCO锚框结果检测框全部偏小。后来把锚框换成自己聚类那组一切恢复正常。4. 板端推理部署Runtime API调用与前后处理实现4.1 Python推理接口与模型加载RDK-X5板端提供了Python runtime接口调用形式和ONNXRuntime类似但底层走的是BPU。先看一段最小可运行的推理代码from hobot_dnn import pyeasy_dnn import numpy as np import cv2 # 加载模型 models pyeasy_dnn.load(yolov5s.bin) # 模型输入shape print(输入:, models[0].inputs[0].properties) # 模型输出shape for out in models[0].outputs: print(输出:, out.properties)注意pyeasy_dnn会对输入做一个内部处理这个处理和你导出ONNX时的预处理必须保持一致。比如我导出ONNX时没有做归一化输入的像素值范围是0~255那么传给BPU的张量也必须是0~255的float32数据不能再归一化到0~1。完整推理流程def inference(frame, models): # 1. letterbox缩放 img, ratio, dwdh letterbox(frame, new_shape(640, 640), color(114, 114, 114)) # 2. BGR转RGBHWC转CHW增加batch维度 img img[:, :, ::-1].transpose(2, 0, 1)[None, ...].astype(np.float32) # 3. BPU推理 outputs models[0].forward(img) # 4. 解析三个检测头的输出 preds parse_outputs(outputs, ratio, dwdh) return predsletterbox的目的是将任意分辨率的图像无损地缩放成640×640保持宽高比不变剩余区域用114填充这样可以避免图像被拉伸变形导致目标形状失真。4.2 后处理代码实现解码、NMS、坐标还原YOLOv5的后处理是整个部署中代码量最大也最容易写错的部分。模型输出的三个特征图每层对应一个尺度的检测头每个grid会预测多个anchor的偏移量。后处理要做的事情是把偏移量解码成实际框坐标过滤低置信度框做NMS去重。后处理代码核心部分def parse_outputs(outputs, ratio, dwdh): # 每个输出对应一个检测头 # output shape: (1, 255, grid_h, grid_w) anchors [ [[10, 13], [16, 30], [33, 23]], # P3层对应80x80 [[30, 61], [62, 45], [59, 119]], # P4层对应40x40 [[116, 90], [156, 198], [373, 326]] # P5层对应20x20 ] num_classes 80 boxes [] scores [] for out_idx, out in enumerate(outputs): # out.data是numpy数组 data out.data[0] # (255, h, w) h, w data.shape[1:] data data.reshape(3, 5 num_classes, h, w) for anchor_idx in range(3): reg data[anchor_idx, :4] # (4, h, w) obj_conf data[anchor_idx, 4:5] # (1, h, w) cls_conf data[anchor_idx, 5:] # (80, h, w) # sigmoid激活 reg 1 / (1 np.exp(-reg)) obj_conf 1 / (1 np.exp(-obj_conf)) cls_conf 1 / (1 np.exp(-cls_conf)) # 解码边框 grid_y, grid_x np.meshgrid(np.arange(h), np.arange(w), indexingij) cx (grid_x reg[0]) * 640 / w cy (grid_y reg[1]) * 640 / h bw anchors[out_idx][anchor_idx][0] * reg[2] bh anchors[out_idx][anchor_idx][1] * reg[3] # 转成xyxy格式 x1 cx - bw / 2 y1 cy - bh / 2 x2 cx bw / 2 y2 cy bh / 2 # 计算最终置信度 score obj_conf * cls_conf # (80, h, w) # 取出最高类别 cls_id np.argmax(score, axis0) max_score np.max(score, axis0) # 筛选 mask max_score 0.25 if not mask.any(): continue for i in range(len(x1[mask])): boxes.append([x1[mask][i], y1[mask][i], x2[mask][i], y2[mask][i]]) scores.append(max_score[mask][i])解码完成后做NMS这一步直接调用cv2的NMSBoxes接口就行比自己写省事且不容易出bug。NMS的IoU阈值我习惯设为0.45目标密集的场景可以放宽到0.5稀疏场景可以收紧到0.4需要实测调。最后要把检测框坐标回映射到原始图像尺寸。前面letterbox记录了一个ratio和dwdh偏移还原公式是x1_orig (x1 - dwdh[0]) / ratio y1_orig (y1 - dwdh[1]) / ratio4.3 性能调优BPU流水线与多线程方案RDK-X5的BPU推理速度很快但整个管线不止推理这一段。图像采集摄像头、预处理、后处理NMS都占CPU时间如果串行处理需要手脚麻利才跟得上。我的做法是摄像头采集线程用GStreamer或者OpenCV VideoCapture持续读帧把最新帧放到共享队列双缓冲即可推理线程从队列取最新帧做预处理、BPU推理、后处理主线程负责画框、叠加GPS信息、显示或上报三个线程各干各的用threading加一个collections.deque(maxlen1)基本够用。关键是不要存历史帧队列只保存最新帧否则延迟越来越高。实测下来摄像头30帧/秒输入YOLOv5s实际能跑到60帧/秒CPU占用率大约40%~50%还留有余量给GPS解析和日志写入。5. GPS模块集成检测结果与地理位置绑定5.1 串口接线与NMEA协议解析GPS模块我用的ATGM336H北斗加GPS双模价格便宜性能稳定。RDK-X5板子带40pin GPIO排针其中UART口在物理规范上和树莓派是兼容的接法如下GPS_TX → 板子UART_RX物理引脚10GPS_RX → 板子UART_TX物理引脚8GPS_VCC → 板子3.3V物理引脚1GPS_GND → 板子GND物理引脚6接线前务必确认GPS模块是3.3V电平还是5V电平。很多便宜模块虽然是3.3V供电但串口输出是5V电平直接接RDK-X5的3.3V串口可能烧坏板子的GPIO。我用的是模块自带电平转换的那种省事不少。系统默认的/dev/ttyS0设备需要授权才能访问sudo usermod -a -G dialout $USER sudo reboot然后写一个简单的串口读取程序import serial ser serial.Serial(/dev/ttyS0, baudrate9600, timeout1) while True: line ser.readline().decode(utf-8, errorsignore) if line.startswith($GNRMC): # 推荐最小定位信息 print(line)GPS模块启动后需要一段时间搜星定位室外空旷处一般30秒到2分钟不等。如果在室内测试搜不到星是正常的别怀疑代码有bug。5.2 经纬度解析与坐标叠加NMEA协议里$GNRMC句子包含了经纬度、速度、方位角、日期时间等信息。解析逻辑比较直白拆分逗号段就行def parse_rmc(line): parts line.split(,) if len(parts) 10: return None if parts[2] ! A: # 定位状态A有效 return None # 纬度格式ddmm.mmmm lat_raw parts[3] lat_hem parts[4] lat_deg float(lat_raw[:2]) lat_min float(lat_raw[2:]) lat lat_deg lat_min / 60.0 if lat_hem S: lat -lat # 经度格式dddmm.mmmm lon_raw parts[5] lon_hem parts[6] lon_deg float(lon_raw[:3]) lon_min float(lon_raw[3:]) lon lon_deg lon_min / 60.0 if lon_hem W: lon -lon speed_knots float(parts[7]) return {lat: lat, lon: lon, speed_kmh: speed_knots * 1.852}拿到经纬度后最直接的做法是把GPS坐标叠加到视频画面上方便现场调试。调用RDK-X5的OpenCV显示功能cv2.putText(frame, fLAT: {lat:.6f} LON: {lon:.6f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)如果需要把检测目标和坐标绑定上报建议统一输出JSON格式每帧包含timestamp、gps、detections三个字段这样后台服务器可以直接解析入库。5.3 GPS定位的坑波特率、TTL电平和时间同步GPS模块集成说大不大但坑是真多。我遇到的第一个坑是波特率不匹配。ATGM336H默认9600波特率有些模块出厂配置是38400或者被改过配置。用串口调试工具先扫一下常见波特率确认能读出一连串$GNGGA这种数据再继续。第二个坑是串口权限。Linux下访问串口设备需要dialout组权限如果忘记加组serial.Serial会报PermissionError板子重启后权限又丢了最好配置一个udev规则固定设备权限。第三个坑是时间同步。GPS模块本身输出的时间非常准UTC时间但RDK-X5系统时间不一定会自动和GPS时间对齐。如果要做目标时间戳与坐标的精确对应建议在代码里用GPS时间戳标记检测结果而不是用系统时间避免误差累积。第四个坑是定位漂移。车辆启动阶段GPS会出现几米的漂移低速状态下尤其明显。如果应用对坐标精度要求高可以考虑加一个简单的卡尔曼滤波或者剔除非定位状态的数据点。6. 常见问题与排查技巧实录从报错到稳跑部署过程中零零散散遇到的问题非常多我把典型的整理成速查表基本覆盖这套方案里最容易卡人的几个环节现象根本原因解决方案load model failed工具链版本和板端系统镜像不匹配检查cat /etc/version升级或降级OE工具链转换时报Unsupported op: GATHERONNX算子过大/过旧升级onnx-simplify重导ONNX手动替代部分算子转换时报Calibration data shape mismatch校准图预处理和模型输入不一致检查校准数据shape必须为[N, 3, 640, 640]推理结果全为空后处理anchor与训练不一致用训练时的anchor覆盖默认值检测框整体偏小letterbox预处理参数不对检查ratio和dwdh是否正确返算第一帧推理特别慢模型加载、内存分配等一次性开销预热推理10帧后再进入正式流程BPU推理偶尔卡死共享队列读写竞争给共享队列加锁或使用双缓冲机制GPS数据一直乱码波特率不对/电平不匹配用串口助手扫波特率确认3.3V电平系统刷完起不来SD卡速度太慢或镜像损坏换A2级SD卡重新烧录再校验MD5掉帧严重后处理NMS占用CPU过多减少候选框数量提前按置信度过滤再NMS下面拆几个典型问题讲一下排查思路。问题一模型转换阶段报错算子不支持。地平线BPU不是一个通用的GPU它对算子类型有严格的限制。早期版本的YOLOv5如果不用--simplify导出会在模型里残留一些如GridSample、Gather一类的算子。解决思路有两个一是尽量用高版本的export.py配合onnx-simplifier把能折叠的算子折叠掉二是手动重构模型把Detect头的解码部分从模型里拿掉只保留骨干和颈部网络让模型的输出就是未解码的特征图解码全部放到后处理代码里。后者麻烦一点但对工具链的兼容性最好。问题二量化后精度掉得厉害。先看转换日志里各个算子的量化误差报告找到误差最大的几个节点。通常集中在检测头的前几层卷积即输出通道较多、对数值范围变化比较敏感的地方。此时用混合量化把这些层固定为float32。如果混合量化后精度还是不够那就加大校准集的数量从200张加到1000张覆盖更多光照和尺度分布。我在安全帽检测场景里观察过一个很有意思的现象夜间图像里目标小且对比度低校准集里夜间图占比太低量化后模型对夜间漏检严重。后来调整校准集的昼夜比例到3比1夜间精度明显回升。问题三板上推理偶尔出现“输出全是同一个类别的噪音框”。这种情况多半是后处理代码里anchor取值和模型不对应。YOLOv5的anchor定义在models/yolov5s.yaml文件里而很多网上流传的后处理代码把anchor写死在代码里两者不匹配就会出这种诡异问题。务必以训练时的yaml文件为准逐个对照。问题四GPS模块定位正常但坐标有时候会“跳一下”。RMC句子里A状态表示有效定位但信号不太好的时候会出现几个连续有效帧中间夹着无效帧。解析时候不要只判断当前帧是否有效最好维护一个坐标滑动窗口超过3个连续无效帧才判定丢失定位否则沿用上一帧的有效坐标。这样处理以后实际测试中车辆转弯或过桥洞时坐标轨迹平滑了很多。7. 部署全流程的检查清单这篇内容的核心流程可以浓缩成一张检查清单部署的时候逐项打勾[ ] 系统镜像烧录成功SSH连接正常/etc/version和OE工具链版本匹配[ ] 导出ONNX成功输入输出shape和预期一致[ ] 校准集准备充分涵盖多场景[ ] hb_mapper转换成功生成.bin文件和量化报告[ ] 板端加载模型成功推理无报错[ ] 后处理解码正确检测框位置和原图吻合[ ] GPS模块串口读取正常NMEA数据解析正确[ ] 检测结果和GPS坐标成功融合输出[ ] 多线程跑通长时间运行无内存泄漏和卡死按照这个顺序走正常情况下两到三个工作日就能从零跑到一个功能完整、输出稳定的解决方案。最后再分享一个经验如果模型在板端跑出来的效果和训练时差异过大先把训练时的预处理参数归一化、颜色通道顺序、letterbox填充值逐个对照一遍90%的问题出在这里。我自己就是被RGB和BGR通道顺序坑过一次检测框位置对但颜色全反了排查了半天才发现是预处理少了cvtColor。这种低级错误越早发现越好别嫌麻烦把每一步的输入输出都print出来看一眼比单纯调试后处理逻辑高效得多。
网站建设高端定制企业官网