1. 学生项目常见痛点:为什么“能飞”≠“能毕业”
做无人机毕设,很多同学第一步就卡在“飞起来”到“飞得稳”之间。实验室里常见的一幕:飞机刚离地半米就左右飘,PID 调参调得怀疑人生;好不容易稳了,再加个路径规划算法,CPU 瞬间飙到 90%,控制环直接 200 ms 延迟;导师一句“加个避障”,GitHub 上抄来的 YOLOv5 权重 80 MB,机载 TX2 直接罢工。总结下来,痛点就三条:
- 实时性不足:传统 A*、RRT 在 10 m×10 m 小场地还能跑,一旦地图分辨率提到 0.1 m,规划一次 300 ms,飞机撞墙了结果还没算完。
- 算法黑盒:TensorFlow 训练完,.h5 往机端一扔,内存暴涨 400 MB,导师问“为什么闪退”只能摊手。
- 调试链路长:Gazebo 里飞得好好的,真机一上螺旋桨就“抽风”,日志里全是
tf跳变,定位飘到隔壁教学楼。
2. 技术选型:FSM、Behavior Tree、TF Lite、ONNX 怎么选
先把控制逻辑和 AI 推理拆成两条线,再分别选型:
任务调度层
- FSM(有限状态机):代码直译就是
if/else山,状态一多就成“面条图”,毕业答辩 PPT 都画不下。 - Behavior Tree(BT):节点可复用、可插拔,用 XML 写策略,导师一眼能看懂,后期改需求只改 XML 不重新编译。
- FSM(有限状态机):代码直译就是
推理引擎
- TensorFlow Lite:ARM 后端成熟,但
libtensorflowlite.so静态链接后体积 11 MB,ROS 2 打包进.deb体积翻倍。 - ONNX Runtime:1.8 MB 的 runtime 就能把 2 MB 模型跑起来,还能直接调用 CUDA/TensorRT,后期换 Xavier 不用改代码。
- TensorFlow Lite:ARM 后端成熟,但
结论:BT + ONNX 是“能毕业”的最小可用组合,既照顾了机端 Flash 只有 1 GB 的可怜空间,也给后续模型蒸馏留余地。
3. 系统架构:三层解耦、两级通信
┌-----------------┐ │ Behavior Tree │ <-- 任务层(XML 可热更新) └--------+-------┘ │ ROS 2 Topic ┌--------v-------┐ │ Scheduler Node│ <-- 调度层(Python,负责把 BT 输出翻译成具体 cmd) └--------+-------┘ │ DDS ┌--------v-------┐ │ ONNX 推理节点 │ <-- AI 层(C++,发布 obstacle 坐标) └----------------┘- 全部节点用
Component方式加载,同进程零拷贝,省掉ros2 topic hz里 2 ms 的序列化损耗。 - 调度层与 AI 层用
std_msgs/UInt8MultiArray裸传张量,避免sensor_msgs/Image把 640×480×3 的图又复制一遍。
4. 核心实现细节
4.1 Behavior Tree 设计
任务被拆成“起飞→探索→避障→返航→降落”五棵子树,每棵子树内部再串并行节点。关键节点源码片段(带注释):
# bt_nav_node.py import py_trees from custom_nodes import TakeoffAction, Explore, ObstacleAvoid, RTL, Land def create_root(): root = py_trees.composites.Sequence("Mission", memory=True) root.add_children([ TakeoffAction(altitude=1.5), py_trees.composites.Selector("ExploreOrBreak", memory=False), Explore(timeout=30), ObstacleAvoid(model_path="/opt/drone/model/obstacle.onnx"), RTL(land_speed=0.3), Land() ]) return rootmemory=True保证节点执行失败后不再重试,防止飞机在天上“死循环”。- 避障节点内部调用 ONNX 推理,返回
py_trees.common.Status.RUNNING直到通道清空。
4.2 ONNX 模型集成
训练阶段用 PyTorch 把 640×640 的 RGB 图蒸馏成 128×128 灰度图,输出 3 类:无障碍、左障碍、右障碍。导出代码:
torch.onnx.export(model, dummy, "obstacle.onnx", input_names=['img'], output_names=['cls'], dynamic_axes={'img': {0: 'batch'}, 'cls': {0: 'batch'}}, opset_version=11)机端推理节点(C++)关键段:
Ort::Session session{env, "/opt/drone/model/obstacle.onnx", session_options}; std::vector<int64_t> input_shape = {1, 1, 128, 128}; auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor = Ort::Value::CreateTensor<float>( memory_info, img_ptr, 128*128, input_shape.data(), input_shape.size()); session.Run(Ort::RunOptions{nullptr inputs, outputs); int cls = std::distance(outputs[0].GetTensorMutableData<float>()).argmax();- 单次推理 3.2 ms(TX2 实测),CPU 占用 8 %,完全不会挤占 50 Hz 的控制环。
4.3 幂等性 & 错误处理
- 所有动作节点继承
py_trees.behaviour.Behaviour,覆写initialise()时把self.sent_command=False,保证同一 tick 内不会重复发指令。 - 若 ONNX 返回
Status.FAILURE,BT 自动回退到RTL子树,同时向上位机发mavros/cmd/command的MAV_CMD_DO_FLIGHTTERMINATION做失效保护。
5. 性能测试与安全性
| 指标 | 数值 | 测试条件 |
|---|---|---|
| 控制闭环延迟 | 18.4 ms | 50 Hz 里程计 + 200 Hz 姿态环 |
| ONNX 推理延迟 | 3.2 ms | TX2, 128×128 输入 |
| CPU 占用 | 31 % | 4 核全开,包含 BT、调度、MAVROS |
| 内存峰值 | 487 MB | 含 3 个 so 与 2 级日志缓存 |
安全方面:
- 通信加密:ROS 2 Foxy 起用
SROS2,Keystore 放/etc/sros,DDS 安全插件开ENCRYPTION=1,防止隔壁组用ros2 topic echo偷数据。 - 失效保护:BT 顶层加
Fallback节点,一旦心跳超时 500 ms 未更新,直接触发px4 flight termination,螺旋桨锁转。 - 传感器时间戳对齐:激光雷达与 IMU 用
message_filters/TimeSynchronizer,slop=0.025 s,防止tf跳变导致定位飞点。
6. 生产环境避坑指南
- QoS 陷阱:ROS 2 默认
Reliability=RELIABLE,图传 30 Mbps 把 DDS 带宽占满后,控制指令反而掉包。把/cmd_vel话题改成BEST_EFFORT + KEEP_LAST 1,延迟立降 5 ms。 - 传感器时间戳:USB 相机驱动常把
clock_type=SYSTEM_TIME,而飞控是MONOTONIC,二者混用tf2会报extrapolation into the future。统一在camera_node里加use_sensor_data_qos=true并override_timestamp=true。 - 行为树热更新:XML 放
~/bt_xml/目录,节点启动用py_trees.console.log_level=INFO,热重载前先blackboard.clear(),否则旧变量残留会让飞机“失忆”。 - 模型版本管理:ONNX 模型文件名带 Git commit 前 7 位,如
obstacle_1a2b3c4.onnx,防止实验室学弟拷错版本导致推理输出全 0。
7. 代码仓库与一键复现
完整工程已开源(MIT),目录结构:
drone_bt_onnx/ ├── src/ │ ├── bt_nav_node.py │ ├── scheduler_node.py │ └── obstacle_inference.cpp ├── config/ │ ├── mission.xml │ └── px4_config.yaml ├── launch/ │ └── drone_sim.launch.py ├── model/ │ └── obstacle_1a2b3c4.onnx └── test/ └── test_obstacle_avoid.py本地 TX2 一键编译:
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release source install/setup.bash ros2 launch drone_bt_onnx drone_sim.launch.pyGazebo 里给墙加随机方块,飞机可实时绕行;真机只需把use_sim_time设false并改串口/dev/ttyTHS1即可。
8. 留给读者的课后作业
- 行为树策略改造:把
Explore节点从时间触发改成“覆盖度”触发,用栅格地图计数已探索面积,看看能不能把 30 s 任务压到 20 s。 - 模型蒸馏:用 Knowledge Distillation 把 3 类网络压成 0.8 MB,再量化到 INT8,观察 TX2 推理延迟能否进 2 ms,同时记录 CPU 降了多少。
- 实时性思考:在资源受限设备上,AI 能力越强往往意味着计算量越大,能否用“事件触发”代替“逐帧推理”?比如只在 IMU 角速度大于阈值时才启动 ONNX,让飞机平时“闭眼飞”,关键时刻再“睁眼”——平衡的艺术就留给你们了。
祝各位毕业顺利,代码不炸机,答辩不炸老师。