AllocNet 四旋翼避障轨迹规划与键盘遥操作

本模块在 AllocNet(KumarRobotics, RA-L 2024)的基础上,补齐了人机交互层:用键盘精确下发航点、把一次性发布的 地图转为常驻话题、记录轨迹数据、并采集演示素材。

上游 AllocNet 提供规划算法;本模块解决「怎么方便地用它做实验」。

1. 要解决的问题

上游 AllocNet 通过 RViz 的 2D Nav Goal 工具下发起终点:鼠标在平面上点两下。 用于教学和实验时,这个交互方式有三个不便:

问题 后果
无法精确控制高度 鼠标只给出 x-y,高度靠约定,无法定量研究
无法精确复现 手点坐标每次不同,不利于对比实验
无法连续操作 想「走一段、停一下、再规划」很别扭

本模块用键盘驱动的虚拟航点游标替代鼠标:坐标精确到步长(默认 0.5 m), 每次下发的航点都可复现,并且支持无图形环境下的脚本化驱动。

2. 方法

2.1 整体数据流

graph TB subgraph 地图层 SM["param_env/structure_map
随机障碍地图生成"] end subgraph 人机交互层 KB["planner/teleop_keyboard.py
键盘遥操作游标"] MR["planner/map_republisher.py
地图 latched 转发"] RV["rviz
可视化"] end subgraph 规划层 LP["planner/learning_planning
AllocNet 推理 + QP 优化"] end subgraph 记录层 RC["planner/record_trajectory.py"] CSV[("trajectory.csv")] end SM -- "/structure_map/global_gridmap
非 latched,只发一次" --> MR MR -- "/map/global_gridmap
latched" --> LP MR -- "/map/global_gridmap_vis
latched,降采样" --> RV KB -- "/move_base_simple/goal" --> LP KB -- "/teleop/cursor
/teleop/goal_markers" --> RV LP -- "/visualizer/trajectory
/visualizer/route" --> RV LP -- "/visualizer/speed" --> RC RC --> CSV

2.2 核心发现:高度被编码进 orientation.z

读上游 learning_planning.cpp 可以看到规划器忽略 pose.position.z, 真实高度由 pose.orientation.z 作为归一化比例决定:

const double zGoal = mapBound[4] + dilateRadius +
                     fabs(msg->pose.orientation.z) *
                         (mapBound[5] - mapBound[4] - 2 * dilateRadius);
const Eigen::Vector3d goal(msg->pose.position.x, msg->pose.position.y, zGoal);

代入本模块 launch 的参数(z_origin=0, z_size=5, dilate=0.2):

z_goal = 0.2 + |orientation.z| × (5 − 0.4) = 0.2 + 4.6 × ratio

因此 teleop_keyboard.py 在 altitude_to_orientation_z() 里做了反向换算:

span  = map_z_size - 2 * dilate              # 4.6
ratio = (z - map_z_origin - dilate) / span   # 反解出归一化比例
msg.pose.orientation.z = ratio

副作用

orientation.z 被高度占用了,所以偏航角无法通过该消息传给规划器。 AllocNet 的 Goal 本身也不支持终端偏航约束,因此 Q/E 只改变游标朝向,进而影响 W/S/A/D 的推进方向。

2.3 两段式 Goal 语义

规划器要求收到两个目标点才触发规划(第一个当起点,第二个当终点)。 因此 G 是两段式的:

  第 1 次 G ──▶ 记录 START ──▶ 等待
  第 2 次 G ──▶ 记录 GOAL  ──▶ 立即触发 AllocNet 规划
  第 3 次 G ──▶ 清空重来,作为新的 START

节点会在终端明确打印当前阶段,避免误操作:

  阶段: 已设起点 #1,等待第 2 次 G 触发规划

2.4 地图为什么要转发一层

structure_map 发布的 /structure_map/global_gridmap 不是 latched 话题, 点云只在生成时发一次。这带来两个后果:

  • RViz 看不到地图 —— RViz 通常比地图节点晚几秒启动,订阅时那一帧早已过去;
  • 规划器错过地图(更严重) —— 规划器要加载 Torch 模型,订阅建立得比地图 生成还晚,mapInitialized 永远是 false,此后所有目标点都被静默丢弃。

map_republisher.py 把地图接住后以 latched 方式重发。latched 话题会向 任意时刻加入的订阅者补发最后一帧,因此无论 RViz 和规划器多晚启动都能拿到 地图,也不需要反复去催 structure_map 重新生成地图。

/structure_map/global_gridmap  (非 latched,一次性)
    ├─> /map/global_gridmap      (latched) ──> 规划器 MapTopic
    └─> /map/global_gridmap_vis  (latched,降采样) ──> RViz

不要用 change_map 来「催」地图

很多资料建议用 /structure_map/change_map 迫使地图重发,但它的语义是 换一张新地图并重新随机化障碍比例,会把地图越堆越密,最终任何航点 都不可规划。正确的唤醒方式是发相同分辨率的 change_res:

rostopic pub -1 /structure_map/change_res std_msgs/Float32 "data: 0.1"

本模块的 map_republisher 内部就是这么做的,且有次数上限。

3. 运行效果

3.1 RViz 中的实时规划

这是在 RViz 里实际跑出来的画面(非离屏渲染、非事后绘制):

AllocNet 在 RViz 中的规划结果

颜色 含义
蓝 AllocNet 优化后的平滑轨迹
红 OMPL 前端几何路径(未做时间分配)
绿 安全飞行走廊边界
球体 键盘下发的起点(红,带 START 标签)与终点(橙)
灰点 真实障碍点云(半透明,便于看清轨迹穿行)

红蓝两条线的对比正是本算法的意义所在:红色折线是未经时间分配的前端路径, 蓝色是 AllocNet 推理 + QP 优化后的轨迹,明显更平滑,且始终走在绿色走廊的中心。

3.2 键盘遥操作动画

从「键盘移动游标 → 下发起点 → 下发终点 → 轨迹生成」的完整过程:

AllocNet 键盘遥操作演示

3.3 轨迹的三维与俯视视图

三维轨迹

俯视图把轨迹叠加在真实地图点云(按飞行高度切片)之上, 可以清楚看到轨迹如何在障碍之间穿行:

俯视图

3.4 速度曲线

速度曲线

3.5 实测数据

指标 数值
规划起终点 (0, −9, 1.5) → (9, 9, 1.5) m
直线距离 / 实际航程 20.1 m / 约 22 m(绕行)
规划耗时 93.6 ms(AllocNet 推理 + QP 优化)
规划轨迹采样点 1065
实飞跟踪采样点 402
峰值速度 4.03 m/s(受 MaxVelBox=4.0 约束)
起终点余隙 2.40 m / 约 1.5 m(到最近障碍点)

运行环境:Ubuntu 20.04.6 + ROS Noetic,CPU 推理(无 GPU 直通), 地图 20×20×5 m / 0.1 m 分辨率,约 30 万点。

关于规划成功率

AllocNet 按固定 5 段推理时间分配。当起终点的几何构型让某段时长被推理为 0 时, 规划器会打印 time and seg does not fit 并放弃本次规划(不报错、不出轨迹)。 实测成功率约 1/5,且与方向、距离强相关。做实验时建议多试几组起终点。

4. 快速开始

4.1 环境要求

项 版本
操作系统 Ubuntu 20.04(ROS Noetic)
ROS Noetic(rosversion 1.16.0)
Python 3.8(随 Noetic)
规划器依赖 libtorch (CPU)、OMPL、Eigen3、osqp + osqp-eigen

本模块不包含规划器本体

AllocNet 的 C++ 规划器依赖 libtorch / OMPL / osqp-eigen, 需要自行编译,不在本仓库内。本模块提供的是人机交互层, 安装到你的 AllocNet 工作区后即可使用。

4.2 安装本模块

安装分两步:先把上游编译出来(一次性),再装本模块。

4.2.1 编译上游(一次性)

planner 与 param_env 来自两个不同的仓库

planner 在 AllocNet 仓库内;param_env 在 kr_param_map 仓库内, 由 AllocNet 的 src/utils.rosinstall 单独拉取。 只克隆 AllocNet 不会得到 param_env —— 这是 main.py check 报 [缺] param_env 的头号原因。

(1) 系统依赖

sudo apt update
sudo apt install -y \
    libompl-dev libeigen3-dev \
    libsdl1.2-dev libsdl-image1.2-dev \
    libboost-all-dev cmake build-essential git wget unzip \
    python3-pip python3-rosdep

libsdl1.2-dev / libsdl-image1.2-dev 是 param_env 独有的依赖 (它的 package.xml 声明了 sdl / sdl-image 两个 rosdep 键): 缺了这两个包 planner 能编过、param_env 编不过。

(2) osqp 0.6.3 与 osqp-eigen(源码编译)

ROS 源里没有满足要求的 osqp,必须锁版本源码编译:

# osqp —— 版本必须是 release-0.6.3,新版 API 不兼容
cd /tmp && git clone -b release-0.6.3 --depth 1 https://github.com/osqp/osqp.git
cd osqp && git submodule update --init --recursive
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(nproc) && sudo make install
sudo ldconfig

# osqp-eigen —— 规划器的 QP 封装 osqp_solver.hpp 依赖它
cd /tmp && git clone --depth 1 https://github.com/robotology/osqp-eigen.git
cd osqp-eigen && mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(nproc) && sudo make install
sudo ldconfig

(3) libtorch(必须 CPU 版)

cd /tmp
wget -O libtorch.zip \
  https://download.pytorch.org/libtorch/nightly/cpu/libtorch-cxx11-abi-shared-with-deps-2.0.0.dev20230301%2Bcpu.zip
unzip -q libtorch.zip -d /tmp/lt
mkdir -p ~/allocnet_ws/src/AllocNet/src/planner
mv /tmp/lt/libtorch ~/allocnet_ws/src/AllocNet/src/planner/libtorch

本模块运行在无 GPU 直通的虚拟机中,GPU 版 libtorch 会因找不到 CUDA 而加载失败。 同时确认 learning_planner.hpp 里是 device(torch::kCPU)。

(4) 拉取两个上游仓库

mkdir -p ~/allocnet_ws/src && cd ~/allocnet_ws/src

# (1) AllocNet —— 提供 planner 包(本模块对应的分支)
git clone -b feature-keyboard-teleop \
    https://github.com/Xiangyuetang91/AllocNet.git

# (2) kr_param_map —— 提供 param_env 包(★ 不在 AllocNet 仓库内)
git clone --depth 1 https://github.com/KumarRobotics/kr_param_map.git

上游 src/utils.rosinstall 里 kr_param_map 用的是 SSH 地址 git@github.com:...,没有配 SSH key 时 wstool update 会直接失败, 因此这里显式用 HTTPS 克隆(本分支已把该地址改写为 HTTPS)。

(5) 编译

cd ~/allocnet_ws
catkin_make -DCMAKE_BUILD_TYPE=Release -D_GLIBCXX_USE_CXX11_ABI=1

不要加 --pkg planner

单包构建不会构建 param_env;而 param_env 缺失时 structure_map 会因找不到可执行文件而瞬间退出,表现为 「没有地图、规划器毫无反应」,且不留任何日志。

_GLIBCXX_USE_CXX11_ABI=1 必须与 libtorch 预编译包的 ABI 一致, 否则链接期会报大量 std::__cxx11 符号缺失。

产物校验:

ls devel/lib/planner/learning_planning   # 规划器
ls devel/lib/param_env/structure_map     # 地图生成

(6) source 环境

source ~/allocnet_ws/devel/setup.bash

每开一个新终端都要 source。未 source 时 ROS_PACKAGE_PATH 里没有 ~/allocnet_ws/src,rospack 看不到工作区内的任何包,main.py check 会报缺包。

4.2.2 安装本模块

# 1. 把本模块装入工作区的 planner 包
cd ros2/src/air/allocnet_teleop
python3 main.py install --ws ~/allocnet_ws

# 2. 重新编译(脚本需注册进 CMakeLists)
cd ~/allocnet_ws && catkin_make --pkg planner

# 3. 环境自检
python3 main.py check

第 2 步此处可以用 --pkg planner:param_env 在上一步已经构建好了, 这里只是把新增的 Python 脚本注册进 planner。

main.py check 会逐项确认 ROS、工作区、上游包与编译产物、本模块脚本、 launch 文件与可选工具是否就位,把「跑不起来」的原因提前暴露:

[1] ROS Noetic: 已安装
[2] AllocNet 工作区 /home/user/allocnet_ws: 已构建
[3] 上游 ROS 包:
     [OK] planner      /home/user/allocnet_ws/src/AllocNet/src/planner
     [OK] param_env    /home/user/allocnet_ws/src/kr_param_map/param_env

     [3b] 包编译产物:
     [OK] planner/learning_planning
     [OK] param_env/structure_map
[4] 本模块脚本:  teleop_keyboard.py / map_republisher.py / record_trajectory.py
[5] launch 文件:  teleop_planning.launch
[6] 可选工具:     rviz / import / Xvfb
 自检结果: 通过,可以运行 `main.py run`

其中 [3] 与 [3b] 是两级独立检查:前者问 rospack「包在哪」, 后者问「包编出来没有」。两者会分叉,因为 catkin 把 <ws>/src 整个加进 ROS_PACKAGE_PATH,没编译的包 rospack 照样找得到 —— 所以只查 rospack 会漏掉「包在但没构建」这种状态,而它恰恰是 structure_map 启动即退的原因。 报 [缺] 时 check 会直接打印该包的补救命令。

键盘节点的核心逻辑另有单元测试覆盖,不需要 ROS 环境即可运行 (测试内用 mock 的 rospy 打桩):

cd src/air/allocnet_teleop
python3 test_teleop_keyboard.py
Ran 18 tests in 0.010s

OK

覆盖高度与 orientation.z 的双向换算、W/S/A/D/Q/E/I/K/R/G 各按键对游标状态 的影响、边界与高度裁剪,以及两段式 Goal 语义。

4.3 启动

# 含 RViz
python3 main.py run

# 无图形界面(服务器 / 虚屏)
python3 main.py run --no-gui

# 不记录轨迹
python3 main.py run --no-record

# 用高对比配置启动(点云灰色,轨迹更醒目,适合截图)
python3 main.py run --rviz-config capture_view.rviz

启动后在 RViz 中会看到地图、游标与规划结果:

AllocNet 运行效果(RViz)

界面自上而下依次是 RViz 工具条、左侧 Displays 面板(Fixed Frame: odom) 与三维视图。图中:蓝色为 AllocNet 优化后的平滑轨迹,红色为 OMPL 前端几何路径,绿色为安全飞行走廊边界,球形标记为键盘下发的起点(红) 与终点(橙),灰色点云为真实障碍。本次运行规划耗时 206.01 ms、轨迹 968 点、 记录 CSV 803 行。

截图时换一套 RViz 配置

默认的 teleop_planner.rviz 把障碍点云按高度着彩虹色,20×20 m 地图上 约 3 万点,密度很高,会把蓝/红/绿三条轨迹线淹没在背景里。 截图请改用 capture_view.rviz(点云统一灰色),轨迹才看得清。

4.4 键盘操作

roslaunch 启动的节点 stdin 是 /dev/null,键盘节点读不到按键。 但它不会因此退出 —— 会常驻运行并持续发布 /teleop/cursor, 所以 RViz 里始终能看到游标。真正用键盘有两种方式:

方式一:另开终端跑交互版(两个实例可共存)

source ~/allocnet_ws/devel/setup.bash
rosrun planner teleop_keyboard.py

方式二:往话题注入按键,不需要 TTY,便于脚本化复现

# 移动游标 + 下发航点(G = 发送)
rostopic pub -1 /teleop/key std_msgs/String "data: 'wwwd'"
rostopic pub -1 /teleop/key std_msgs/String "data: 'g'"

# 也支持一次发整串按键,或写 "space"
rostopic pub -1 /teleop/key std_msgs/String "data: 'aassddg'"

按键速查表

按键 功能 说明
W / S 前进 / 后退 沿当前偏航方向,步长 0.5 m
A / D 向左 / 向右平移 垂直于偏航方向
I 或 Space 上升 步长 0.25 m,自动裁剪到地图高度范围
K 下降 同上
Q / E 偏航 −/+ 每次 10°,仅影响游标朝向
R 重置初始状态 游标回到初始位置,清空已下发航点
G 发送航点 两段式:第 1 次起点,第 2 次终点并触发规划
+ / − 调整平移步长 范围 0.1 – 5.0 m
H 显示帮助
Ctrl+C 退出 自动恢复终端属性

4.5 数据记录与绘图

# 记录节点随 launch 自动启动;也可单独指定输出
rosrun planner record_trajectory.py _output:=$HOME/run1.csv

# 生成图表(plot_trajectory.py 随本模块 install 到工作区,无需额外下载)
python3 src/planner/scripts/plot_trajectory.py --csv $HOME/run1.csv --out-dir docs/assets

没有 ROS 环境也可以先验证整条绘图链路:offline_demo.py 会产出一份 格式完全相同的 CSV(算法层离线复现,不作为实验证据),可直接喂给 plot_trajectory.py。

输出 CSV 格式:time,x,y,z,v,source,其中 source 区分 planned(规划轨迹)/ flight(实飞跟踪)/ goal / start。

5. 话题清单

订阅

话题 类型 来源
/structure_map/global_gridmap sensor_msgs/PointCloud2 structure_map 节点
/move_base_simple/goal geometry_msgs/PoseStamped 键盘节点 / RViz 2D Nav Goal
/teleop/key std_msgs/String 外部脚本(无 TTY 时注入按键)

发布

话题 类型 含义
/map/global_gridmap PointCloud2 地图 latched 转发(规划器订阅此话题)
/map/global_gridmap_vis PointCloud2 降采样版,供 RViz 流畅渲染
/teleop/cursor Marker 游标球 + 朝向箭头
/teleop/waypoints MarkerArray 已下发航点折线
/teleop/goal_markers MarkerArray 起终点标记(START/GOAL 标签)
/visualizer/trajectory Marker 优化后的多项式轨迹
/visualizer/route Marker 前端几何路径
/visualizer/speed Float64 当前速度

6. 文件说明

文件 作用
main.py 统一入口:install / check / run / capture
teleop_keyboard.py 键盘遥操作节点(游标、航点下发)
map_republisher.py 地图 latched 转发(解决规划器错过地图)
record_trajectory.py 轨迹数据记录
teleop_planning.launch 一键启动:地图 + 转发 + 规划器 + 键盘 + 记录 + RViz
teleop_planner.rviz RViz 配置(正常实验用)
capture_view.rviz RViz 配置(采集素材用,灰底高对比)
plot_trajectory.py 由 CSV 生成三维轨迹 / 俯视图 / 速度曲线
offline_demo.py 无 ROS 环境时的算法层离线复现(不作为实验证据)
test_teleop_keyboard.py 单元测试(mock rospy,18 项,无需 ROS)
capture_teleop_demo.py 演示素材采集(Xvfb 虚屏 + 抓屏)
make_assets.py 把抓到的帧裁成静态图与 GIF
README.md 运行环境、步骤与文件说明

7. 已知问题

7.1 launch-prefix 不要设成 false

launch-prefix 的值会作为命令前缀拼在可执行文件前执行,不是布尔开关。 写成 launch-prefix="false" 会执行 false <节点> —— false 是立即返回 退出码 1 的命令,节点注册进 master 后立刻死亡且不留任何日志,极难排查。 不用前缀时留空字符串。

7.2 无 GPU 环境下 GNOME 桌面渲染会冻结

在 VMware 无 GPU 直通时,GNOME 桌面(:0)的渲染会静默冻结:进程都活着、 xdpyinfo 正常、窗口树正常,但画面不再更新。表现极具迷惑性 —— scrot 每次都能"成功"返回一张图,但永远是同一张陈旧帧。

采集演示素材时请改用 Xvfb 虚屏:

sudo apt install -y xvfb imagemagick
Xvfb :99 -screen 0 1280x800x24 -ac +extension GLX +render -noreset &
export DISPLAY=:99
export LIBGL_ALWAYS_SOFTWARE=1
rviz -d $(find planner)/config/capture_view.rviz &
import -display :99 -window root /tmp/f.png

不要把 xdotool 用在 RViz 上

合成输入在虚屏/无焦点窗口上基本不生效,且反复操作后 RViz 会停止重绘。 相机角度和图层开关请直接写进 .rviz 配置文件,重启 RViz 生效。

7.3 工作区被重复挂载会导致 roslaunch 拒绝启动

若 devel/.catkin 记录了多个 source space,且其中一个是指向另一个的软链, 同一份代码会被挂两遍,roslaunch 报:

RLException: multiple files named [xxx.launch] in package [planner]

修法:移走多余的挂载点,删除 devel/.catkin,然后完整重新构建 (删掉 .catkin 后不重建,ROS_PACKAGE_PATH 会退化成只有 /opt/ros/noetic/share)。

单包构建的陷阱

catkin_make --pkg planner 不会构建 param_env。 若 devel/lib/param_env/ 是空的,structure_map 会因找不到可执行文件而 瞬间退出,表现为"没有地图、规划器毫无反应"。请至少完整构建一次。

8. 参考文献

  1. Wu Y, Sun X, Spasojevic I, et al. Deep Learning for Optimization of Trajectories for Quadrotors[J]. IEEE Robotics and Automation Letters, 2024, 9(3): 2479-2486. arXiv:2309.15191
  2. 上游仓库:KumarRobotics/AllocNet
  3. 前端路径搜索使用 OMPL(Open Motion Planning Library)

人工智能使用声明

本模块的开发过程使用了 AI 编程助手(Claude)辅助代码编写、调试与文档撰写。 所有提交内容由提交者本人审阅并对正确性负全部责任。实测数据与截图均来自 在 Ubuntu 20.04 + ROS Noetic 虚拟机中的真实运行,未经合成或修改。