PPYOLO垃圾检测+地平线旭日X3派部署(下):ONNX模型转换与端侧推理验证
1. 从 ONNX 到 X3 派可执行模型链路拆解与踩坑预判PPYOLO 垃圾检测模型在 PC 端训练完之后真正决定能不能落地的环节其实是模型转换和端侧推理验证。我见过太多项目卡在这一步ONNX 在 Netron 里看着结构没问题一上板就报算子不支持或者精度掉得离谱检测框全飘。地平线旭日 X3 派用的是 Bernoulli2 架构 BPU算力 5 TOPS跑 416×416 的 PPYOLO 理论上是够的但前提是模型得先经过地平线 AI 工具链转成.bin混合异构模型否则板端根本加载不了。这条链路可以拆成四段ONNX 导出确认、工具链 checker 校验、hb_mapper makertbin转换、板端 TROS 节点推理。每一段都有独立的失败模式。比如 checker 阶段如果打印出 CPU 算子说明有算子没被 BPU 接管这时候硬转出来的 bin 要么跑不了要么性能暴跌。再比如量化阶段校准集选得不好mAP 可能从 0.85 直接掉到 0.6这种精度损失在垃圾检测这种小目标场景里是致命的。这篇内容面向的是已经拿到ppyolo.onnx、准备往 X3 派上部署的开发者。我会把转换命令、config.yaml配置、板端ppyoloworkconfig.json和推理脚本完整给出来同时把精度对齐和耗时验证的动作也写清楚。整个流程我在类似项目里跑过不止一次下面这些配置和排错经验都是实测有效的。需要说明的是模型转换依赖地平线官方 AI 工具链 Docker 环境这个环境里集成了hb_mapper、hbdk、horizon_nn等组件。板端推理则依赖 TROSTogetherROS这是地平线机器人开发平台的操作系统层。两者版本要匹配工具链版本太新或太旧都可能导致转换出来的 bin 在板端加载失败。2. 转换前置工具链环境与 ONNX 模型自检在跑hb_mapper之前有两件事必须先确认工具链 Docker 能正常起来以及 ONNX 模型的输入输出节点名称、shape、opset 版本都符合预期。这两步看起来简单但实际项目里至少一半的转换失败都源于这里没检查。2.1 工具链 Docker 环境确认地平线 AI 工具链官方提供了 Docker 镜像拉起来之后进入容器先验证核心组件版本hb_mapper --version hbdk-cc --version python3 -c import horizon_nn; print(horizon_nn.__version__)正常输出应该能看到hb_mapper1.8.x、hbdk3.31.x、horizon_nn0.13.x 这一组版本。如果hb_mapper命令找不到说明环境变量没配好检查/etc/profile.d/下有没有工具链的 env 脚本。Docker 启动时要把工作目录挂载进去比如-v /mnt/trash_det:/mnt/trash_det否则容器里访问不到你的 ONNX 文件。2.2 ONNX 模型结构自检用 Netron 打开ppyolo.onnx重点看三个地方输入节点名是不是image输入 shape 是不是[1, 3, 416, 416]输出节点有几个、shape 分别是什么。PPYOLO 通常是两个输出分支对应 13×13 和 26×26 两个特征图每个分支的输出通道数是3 × (5 class_num)。垃圾检测如果类别数是 1那通道数就是 18。也可以用 Python 脚本快速打印import onnx model onnx.load(ppyolo.onnx) print(IR version:, model.ir_version) print(Opset version:, model.opset_import[0].version) for inp in model.graph.input: print(Input:, inp.name, [d.dim_value for d in inp.type.tensor_type.shape.dim]) for out in model.graph.output: print(Output:, out.name, [d.dim_value for d in out.type.tensor_type.shape.dim])如果 opset 版本高于 11建议在导出时降回 11地平线工具链对高版本 opset 的兼容性不是全覆盖的。输入节点名如果不是image要么改导出脚本要么在config.yaml里把input_name改成实际名称。2.3 用 hb_mapper checker 做算子支持性校验这是转换前最关键的一步。checker 会模拟 BPU 的算子映射告诉你哪些算子能上 BPU、哪些会 fallback 到 CPUhb_mapper checker --model-type onnx --march bernoulli2 --model ppyolo.onnx执行后会在当前目录生成hb_mapper_checker.log。重点看日志末尾的 node 信息表每一行会标注算子跑在 BPU 还是 CPU。如果看到Conv、LeakyRelu、MaxPool、Resize、Concat这些都在 BPU 上说明模型结构对 BPU 友好。PPYOLO 的主干是 ResNet 变体加 FPN这些算子在 Bernoulli2 上都是原生支持的。如果出现 CPU 算子常见的是自定义的YoloLayer或者某些Slice变体。这时候有两个选择一是把后处理从模型里剥离出来让模型只输出原始特征图后处理在板端用 C 或 Python 写二是用工具链的算子替换功能但后者对 PPYOLO 这种结构不一定适用。我一般推荐第一种因为后处理放板端反而更灵活阈值调整不用重新转模型。checker 通过之后日志里会显示End model checking并且所有算子都在 BPU 上。这时候再进入正式的makertbin转换成功率会高很多。3. 可复制配置config.yaml 与 makertbin 转换命令正式转换的核心是一个config.yaml它控制模型输入输出、量化校准、编译优化三个大块。这个文件写错一个参数转换出来的 bin 要么精度崩要么板端加载报错。下面这份配置是我在 PPYOLO 416×416 垃圾检测上实际用过的可以直接复制改路径。3.1 完整 config.yamlmodel_parameters: onnx_model: ppyolo.onnx march: bernoulli2 layer_out_dump: False log_level: debug working_dir: model_output output_model_file_prefix: ppyolo_trashdet_416x416_nv12 input_parameters: input_name: image input_type_rt: nv12 input_layout_rt: NHWC input_type_train: rgb input_layout_train: NCHW input_shape: 1x3x416x416 norm_type: data_mean_and_scale mean_value: 123.68 116.28 103.53 scale_value: 0.0171 0.0175 0.0174 calibration_parameters: cal_data_dir: ./images_f32 preprocess_on: True calibration_type: kl compiler_parameters: compile_mode: latency debug: False core_num: 2 optimize_level: O2几个参数需要重点解释。input_type_rt: nv12表示板端摄像头直接给 NV12 数据工具链会自动插入 YUV 到 RGB 的转换这样板端就不用手动做颜色空间转换省 CPU。input_type_train: rgb和input_layout_train: NCHW要和训练时一致PPYOLO 训练用的是 RGB、NCHW。mean_value和scale_value是 PaddleDetection 里 PPYOLO 的标准预处理参数如果训练时改过这里要同步改。calibration_type: kl用的是 KL 散度量化比max方法精度更好但需要校准集覆盖典型场景。cal_data_dir指向的目录里放 20 到 100 张垃圾检测场景的图片格式 JPEG 或 BMP 都行。注意这些图片要是真实场景的别拿纯色图或者过曝图凑数否则量化参数会偏。core_num: 2表示编译双核模型X3 派的 BPU 是双核的开双核能提升吞吐。optimize_level: O2是推荐的优化等级O3 编译时间太长O0 性能又不够。3.2 执行转换命令在工具链 Docker 里切到config.yaml所在目录执行hb_mapper makertbin --config config.yaml --model-type onnx转换过程会依次经过 parse、optimize、calibrate、quantize、compile 五个阶段。日志里会看到Start to calibrate the model、Run calibration model with kl method、Start to compile the model with march bernoulli2这些关键节点。整个流程视模型大小和校准集数量大概几分钟到十几分钟。转换成功后model_output目录下会生成四个文件文件名用途ppyolo_trashdet_416x416_nv12_original_float_model.onnx原始浮点模型用于对比ppyolo_trashdet_416x416_nv12_optimized_float_model.onnx优化后浮点模型ppyolo_trashdet_416x416_nv12_quantized_model.onnx量化后模型ppyolo_trashdet_416x416_nv12.bin板端可加载的混合异构模型真正上板用的是.bin文件。如果转换过程中报Unsupported op或者calibration failed先回去看 checker 日志大概率是某个算子没上 BPU或者校准集图片尺寸和input_shape对不上。3.3 精度对齐转换前后输出对比转换完别急着上板先在 PC 端做一次精度对齐。用同一张测试图分别跑原始 ONNX 和量化后的 ONNX对比输出特征图的余弦相似度。工具链自带hb_mapper的精度对比工具也可以自己写脚本import numpy as np import onnxruntime as ort def run_onnx(model_path, input_data): sess ort.InferenceSession(model_path) input_name sess.get_inputs()[0].name outputs sess.run(None, {input_name: input_data}) return outputs img np.random.randn(1, 3, 416, 416).astype(np.float32) orig_out run_onnx(ppyolo.onnx, img) quant_out run_onnx(model_output/ppyolo_trashdet_416x416_nv12_quantized_model.onnx, img) for i, (o, q) in enumerate(zip(orig_out, quant_out)): cos_sim np.dot(o.flatten(), q.flatten()) / (np.linalg.norm(o.flatten()) * np.linalg.norm(q.flatten())) print(fOutput {i} cosine similarity: {cos_sim:.4f})余弦相似度在 0.99 以上说明量化损失很小0.95 到 0.99 之间可以接受低于 0.95 就要检查校准集或者量化方法了。这一步能提前发现精度问题避免上板之后才发现检测框全乱。4. 板端推理验证TROS 节点配置与实时运行模型转成.bin之后下一步是在 X3 派上跑起来。板端用的是 TROS推理节点是dnn_node_example配置文件是ppyoloworkconfig.json。这个 JSON 决定了模型怎么加载、后处理怎么做、阈值怎么设。4.1 ppyoloworkconfig.json 完整配置{ model_file: /opt/tros/lib/mono2d_trash_detection/config/ppyolo_trashdet_416x416_nv12.bin, model_name: ppyolo_trashdet, dnn_Parser: yolov3, model_output_count: 2, class_num: 1, cls_names_list: [trash], strides: [13, 26], anchors_table: [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119]], score_threshold: 0.4, nms_threshold: 0.45, nms_top_k: 100 }dnn_Parser设为yolov3因为 PPYOLO 的输出格式和 YOLOv3 一致都是两个分支的特征图工具链内置的 yolov3 parser 可以直接解析。model_output_count是 2对应两个输出分支。strides是每个分支的下采样步长13 对应 416/3226 对应 416/16。anchors_table要和训练时的 anchor 配置一致PPYOLO 默认的 anchor 就是这两组。score_threshold和nms_threshold是后处理阈值垃圾检测场景建议 score 设 0.4 左右太低会误检太高会漏检。nms_top_k是 NMS 保留的框数100 够用了。4.2 实时运行命令把配置文件复制到工作目录然后启动推理节点cp -r /opt/tros/lib/mono2d_trash_detection/config/ . export CAM_TYPEmipi ros2 launch dnn_node_example hobot_dnn_node_example.launch.py \ config_file:config/ppyoloworkconfig.json \ msg_pub_topic_name:ai_msg_mono2d_trash_detection \ image_width:1920 \ image_height:1080CAM_TYPEmipi表示用 MIPI 摄像头X3 派板载的摄像头接口就是 MIPI。image_width和image_height是摄像头分辨率1920×1080 是常见配置。启动后节点会订阅摄像头数据跑推理然后把检测结果发布到ai_msg_mono2d_trash_detection这个 topic 上。终端日志里会打印每帧的推理耗时和 FPS。实测下来416×416 的 PPYOLO 在双核 BPU 上能跑到 30 FPS 左右满足实时检测需求。如果 FPS 明显偏低检查core_num是不是设成了 2以及compile_mode是不是latency。4.3 本地回灌验证没有摄像头或者想用固定图片测试的时候用回灌模式cp -r /opt/tros/lib/mono2d_trash_detection/config/ . ros2 launch dnn_node_example hobot_dnn_node_example_feedback.launch.py \ config_file:config/ppyoloworkconfig.json \ image:config/trashDet0028.jpg回灌模式会把检测结果渲染到图片上保存到本地方便肉眼对比。终端日志里会打印每个检测框的类别、置信度和坐标。如果框的位置明显偏移大概率是anchors_table或者strides配错了如果置信度普遍偏低检查score_threshold和量化精度。4.4 耗时验证与性能观察板端推理的耗时可以从两个维度看单帧推理耗时和端到端延迟。单帧推理耗时在节点日志里有打印通常在 30ms 左右。端到端延迟包括摄像头采集、预处理、推理、后处理、发布整体在 50ms 以内算正常。如果想更细粒度地看 BPU 利用率可以用hrut_somstatus命令查看板端资源占用。BPU 利用率在推理时应该稳定在较高水平如果偏低说明模型没跑满双核或者数据供给跟不上。5. 常见报错排查从 checker 失败到板端加载异常转换和部署过程中会遇到各种报错下面这几个是我实际踩过的按出现频率排序。5.1 hb_mapper checker 报 Unsupported op报错信息类似Unsupported op type: XXX说明某个算子不在 BPU 支持列表里。先确认算子名称如果是YoloLayer或者自定义后处理把后处理从模型里去掉让模型只输出原始特征图。如果是Slice、Gather这类检查 opset 版本降到 11 再试。PPYOLO 主干里的Resize在 Bernoulli2 上是支持的但如果用了Resize的某些非标准模式可能会 fallback。5.2 makertbin 报 calibration failed校准失败通常是校准集的问题。检查cal_data_dir路径是否正确图片格式是否支持图片数量是否够。另外preprocess_on: True时工具链会用 skimage 做 resize如果图片本身尺寸差异太大resize 后的分布会偏。建议校准集图片统一预处理成 416×416 再放进去。5.3 板端加载 bin 报 model file not found这个一般是路径问题。ppyoloworkconfig.json里的model_file要用绝对路径或者相对于启动目录的路径。另外确认.bin文件已经拷贝到板端权限没问题。如果报model version mismatch说明工具链版本和板端 TROS 版本不匹配需要对齐版本。5.4 推理结果全为空或置信度极低先检查score_threshold是不是设太高了降到 0.1 看看有没有框出来。如果有框但位置乱检查anchors_table和strides。如果框的位置对但置信度低大概率是量化精度损失回去看第 3.3 节的余弦相似度。还有一种可能是输入颜色空间不对input_type_rt设成nv12但板端给的是 RGB导致预处理错乱。5.5 401 / local proxy failed / reading choices 类报错如果你在调用云端 API 做辅助验证时遇到401 Unauthorized先检查 API Key 是否有效、是否过期。local proxy failed通常是本地网络配置问题检查环境变量里有没有残留的 proxy 设置。reading choices报错一般出现在流式响应解析时检查请求体里的stream参数和服务端返回格式是否匹配。OAuth 相关报错则要确认 token 刷新逻辑access token 过期后要用 refresh token 重新获取。对于需要长期在板端跑编码或 Agent 任务的场景可以考虑用 Coding Plan 来管理调用配额和模型切换。如果只是验证模型对话效果用模型对话页面直接测试就行。接入文档里有完整的 Base URL、Key、Model ID 三件套说明配置的时候三个都要填对缺一个都会报错。6. 部署闭环收尾从转换到上板的完整动作清单把整个链路串起来从 ONNX 到 X3 派上跑通核心动作就是这几步checker 校验算子、写 config.yaml、跑 makertbin、拷贝 bin 到板端、配 ppyoloworkconfig.json、启动 TROS 节点、看日志确认 FPS 和检测结果。每一步都有对应的验证动作checker 看算子分布makertbin 看输出文件板端看日志和渲染图。精度对齐这块别省PC 端余弦相似度对比花不了几分钟但能避免上板后返工。耗时验证也是30 FPS 是及格线低于这个数就要查双核编译和优化等级。垃圾检测场景对实时性有要求延迟太高的话检测结果传到下游控制节点就滞后了。板端推理节点跑起来之后检测结果是通过 ROS2 topic 发布的下游可以接机械臂、小车或者其他执行机构。TROS 的生态里有很多现成的机器人开发组件检测结果可以直接喂给这些组件做二次开发。整个部署闭环到这里就算完成了后面就是业务逻辑的活了。