
1. 项目缘起与整体设计思路1.1 为什么要在MoveIt2里做实时3D避障做过机械臂抓取的朋友大概率都遇到过这个场景规划好的轨迹在仿真里跑得漂漂亮亮一上真机就撞了。原因往往不复杂——仿真环境里只有已知的桌面、已知的箱子而真实场景里可能突然多出来一个杯子、一只手、甚至只是操作员走动时衣服的一角。传统的MoveIt2避障依赖的是规划场景里预先塞进去的碰撞体这些碰撞体要么是手动添加的立方体/圆柱体要么是从CAD模型导入的网格本质上都是“静态已知”的。一旦环境发生变化规划器根本不知道。解决这个问题的标准做法就是引入Octomap八叉树地图。Octomap把三维空间递归地划分成八叉树结构每个叶子节点用一个概率值表示“这里被占据的可能性有多大”。深度相机的点云进来之后经过射线投射更新每个体素的占据概率最终形成一张可以动态更新的3D占据栅格地图。MoveIt2的PlanningScene可以直接订阅这张地图把它作为碰撞体加入规划场景这样规划器在搜索轨迹时就会自动绕开这些障碍。我选择奥比中光AstraPro作为深度传感器理由很直接性价比高、ROS2驱动成熟、深度图质量在1米到4米范围内足够用。配合ROS2 Humble和MoveIt2整条链路是当前社区里最主流、资料最全的组合。Ubuntu 22.04是Humble的官方支持平台装起来省心。1.2 整体数据流拆解整条链路的数据流可以这样理解AstraPro通过USB接入astra_camera驱动节点发布深度图话题/camera/depth/image_raw和相机内参/camera/depth/camera_info。depth_image_proc包里的point_cloud_xyz_node订阅深度图和内参把深度图转换成点云/camera/depth/points。octomap_server节点订阅点云结合TF变换把点云从相机坐标系转到地图坐标系然后做射线投射更新八叉树发布/octomap_full、/octomap_binary等话题同时通过/occupied_cells_vis_array发布可视化标记。MoveIt2的move_group节点配置中把Octomap的占据信息作为PlanningScene的碰撞体来源。具体做法是在move_group的launch里加载occupancy_map_monitor插件让它订阅Octomap话题并更新规划场景。这条链路里最容易出问题的环节是TF变换和坐标系对齐。相机坐标系到机械臂基座坐标系的变换必须准确否则点云会飘到错误的位置避障就变成了“避空气”。我后面会专门讲怎么标定和验证。1.3 方案选型背后的考量有人可能会问为什么不用moveit_ros_perception里自带的OccupancyMapMonitor直接接深度图答案是也可以但那条路对深度图的格式和QoS要求更严格而且调试起来不如Octomap独立节点直观。把Octomap单独跑起来你可以先用RViz2看地图对不对再接入MoveIt2问题定位会快很多。另一个选择是nvblox或voxblox这类基于TSDF的建图方案它们重建的表面更平滑但对GPU有要求而且和MoveIt2的集成不如Octomap成熟。Octomap虽然体素化之后看起来“块状”但对于避障来说块状反而更安全——它会把障碍物膨胀得稍微大一点给规划器留出余量。提示如果你用的是AstraPro Plus或者Astra 2驱动包可能不同但点云话题的名字基本一致本文的流程可以通用。2. 环境准备与驱动安装的实操细节2.1 Ubuntu 22.04 ROS2 Humble的安装要点Ubuntu 22.04是ROS2 Humble的官方目标平台安装方式我推荐用apt而不是源码编译。网上有很多“一键安装”脚本但我不建议在生产环境用因为脚本里往往锁定了特定版本后续升级会冲突。老老实实按官方步骤来# 设置locale sudo apt update sudo apt install locales sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALLen_US.UTF-8 LANGen_US.UTF-8 # 添加ROS2 apt源 sudo apt install software-properties-common sudo add-apt-repository universe sudo apt update sudo apt install curl -y sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release echo $UBUNTU_CODENAME) main | sudo tee /etc/apt/sources.list.d/ros2.list /dev/null # 安装ROS2 Humble桌面版 sudo apt update sudo apt install ros-humble-desktop装完之后记得source /opt/ros/humble/setup.bash并且把它加到~/.bashrc里。这里有个小坑如果你之前装过ROS1~/.bashrc里可能有source /opt/ros/noetic/setup.bash两个一起source会导致环境变量混乱。我的做法是注释掉ROS1那行需要的时候再手动source。MoveIt2的安装更简单sudo apt install ros-humble-moveitOctomap相关的包sudo apt install ros-humble-octomap ros-humble-octomap-ros ros-humble-octomap-server ros-humble-octomap-msgs2.2 AstraPro驱动在ROS2下的编译与配置奥比中光的AstraPro在ROS2下的驱动是astra_camera这个包在ROS2的apt源里没有需要从源码编译。我用的仓库是orbbec/ros_astra_camera的ros2分支。编译前先装依赖sudo apt install ros-humble-image-transport ros-humble-image-pipeline ros-humble-camera-info-manager ros-humble-diagnostic-updater然后创建工作空间mkdir -p ~/astra_ws/src cd ~/astra_ws/src git clone -b ros2 https://github.com/orbbec/ros_astra_camera.git cd ~/astra_ws rosdep install --from-paths src --ignore-src -r -y colcon build --symlink-install编译过程中最常见的报错是libuvc找不到。AstraPro用的是UVC协议需要libuvc库。Ubuntu 22.04的apt源里有libuvc-dev直接装就行。如果编译时提示udev规则问题需要把astra_camera/scripts下的56-orbbec-usb.rules拷贝到/etc/udev/rules.d/然后sudo udevadm control --reload-rules sudo udevadm trigger。启动相机的launch文件ros2 launch astra_camera astra_pro.launch.xml启动后检查话题ros2 topic list | grep camera你应该能看到/camera/depth/image_raw、/camera/depth/camera_info、/camera/color/image_raw等。如果深度图话题没有数据先用ros2 topic hz /camera/depth/image_raw看频率正常应该在30Hz左右。如果频率是0检查USB连接——AstraPro必须接USB 3.0口接2.0口会带宽不足导致深度流断掉。2.3 点云转换节点的配置深度图转点云用depth_image_procsudo apt install ros-humble-depth-image-proc写一个launch文件把转换节点拉起来from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packagedepth_image_proc, executablepoint_cloud_xyz_node, namepoint_cloud_xyz, remappings[ (image_rect, /camera/depth/image_raw), (camera_info, /camera/depth/camera_info), (points, /camera/depth/points), ], parameters[{queue_size: 10}] ), ])这里有个细节point_cloud_xyz_node要求输入是去畸变后的图像image_rect但AstraPro的深度图本身畸变很小直接拿image_raw用问题不大。如果你发现点云边缘有弯曲那就需要先过image_proc做去畸变。注意depth_image_proc的QoS默认是SENSOR_DATA而octomap_server默认是RELIABLE两者不匹配会导致收不到数据。解决办法是在octomap_server的参数里把QoS设成SENSOR_DATA或者用topic_tools做中继。我后面会讲具体怎么配。3. Octomap Server的配置与调参实战3.1 octomap_server的核心参数解析octomap_server的参数不少但真正影响避障效果的就那么几个。我列一个表把每个参数的作用和推荐值说清楚参数名作用推荐值说明resolution体素边长米0.02~0.05越小越精细但内存和计算量指数增长frame_id地图坐标系base_link或world必须和MoveIt2的规划坐标系一致sensor_model/max_range传感器最大有效距离3.0AstraPro在3米外噪声很大截断掉sensor_model/hit命中概率0.7默认值一般不用改sensor_model/miss未命中概率0.4默认值sensor_model/min最小概率0.12低于此值认为空闲sensor_model/max最大概率0.97高于此值认为占据occupancy_thres占据阈值0.5概率超过此值算障碍filter_ground是否过滤地面true机械臂场景建议开ground_filter/distance地面过滤距离0.04距离地面多近算地面pointcloud_min_z点云最小高度0.05过滤掉地面附近的点resolution的选择是个权衡。0.02米2厘米的体素对于机械臂避障来说足够精细但一张3米×3米×3米的地图会有超过3亿个体素内存直接爆掉。实际用0.05米5厘米比较稳妥障碍物会被膨胀到5厘米的粒度对于抓取任务来说这个精度够用。如果你做的是精密装配可以降到0.02但要把max_range也降到1.5米以内控制地图规模。3.2 启动文件与QoS配置octomap_server的启动launchfrom launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( packageoctomap_server, executableoctomap_server_node, nameoctomap_server, parameters[{ resolution: 0.05, frame_id: base_link, sensor_model/max_range: 3.0, occupancy_thres: 0.5, filter_ground: True, ground_filter/distance: 0.04, pointcloud_min_z: 0.05, base_frame_id: base_link, }], remappings[ (cloud_in, /camera/depth/points), ], outputscreen ), ])QoS的问题在这里必须解决。octomap_server默认订阅cloud_in用的是RELIABLE而depth_image_proc发布点云用的是SENSOR_DATA。两者不兼容表现就是octomap_server启动了但地图一直是空的。解决办法有两个方法一在octomap_server的源码里改QoS。找到octomap_server_node.cpp把订阅的QoS改成rclcpp::SensorDataQoS()。但这样每次更新包都要重新改不推荐。方法二用topic_tools的relay做中继把SENSOR_DATA转成RELIABLEros2 run topic_tools relay /camera/depth/points /camera/depth/points_reliable然后在octomap_server的remapping里把cloud_in指向/camera/depth/points_reliable。这个方法不用改源码但多了一个节点延迟会增加几毫秒。对于避障来说几毫秒可以接受。方法三在launch里用ComposableNode把point_cloud_xyz和octomap_server放到同一个容器里这样QoS可以自动协商。这是最优雅的方案但配置起来稍复杂。我实测下来方法二最省事稳定性也够。3.3 TF树的配置与验证TF是整条链路的命脉。AstraPro的驱动会发布camera_link到camera_depth_frame的静态TF但camera_link到机械臂base_link的变换需要你自己提供。有两种方式方式一如果相机固定在机械臂基座附近直接写一个静态TF发布ros2 run tf2_ros static_transform_publisher 0.3 0 0.5 0 0 0 base_link camera_link这表示相机在base_link前方0.3米、上方0.5米处姿态无旋转。方式二如果相机装在机械臂末端eye-in-hand那camera_link到base_link的变换是随机械臂运动的必须通过robot_state_publisher和URDF来维护。这种情况下Octomap的frame_id最好设成world或odom而不是base_link否则机械臂一动地图就跟着动避障会失效。验证TF是否正确ros2 run tf2_ros tf2_echo base_link camera_depth_frame如果输出里Translation和Rotation都是你期望的值说明TF没问题。如果报Lookup would require extrapolation说明时间戳对不上检查use_sim_time参数。实操心得我习惯在RViz2里同时显示TF树、点云和Octomap。点云应该和Octomap的占据体素重合如果点云飘在一边那一定是TF错了。这个可视化检查比看日志快十倍。4. MoveIt2接入Octomap的完整流程4.1 配置move_group的occupancy_map_monitorMoveIt2接入外部占据地图的标准接口是occupancy_map_monitor插件。在move_group的launch文件里需要加载occupancy_map_monitor的配置。具体做法是在move_group的config目录下创建一个occupancy_map_monitor.yamloctomap_frame: base_link octomap_resolution: 0.05 max_range: 3.0然后在move_group.launch.py里加载这个配置并且启动occupancy_map_monitor节点from launch import LaunchDescription from launch_ros.actions import Node from moveit_configs_utils import MoveItConfigsBuilder def generate_launch_description(): moveit_config MoveItConfigsBuilder(your_robot, package_nameyour_moveit_config).to_moveit_configs() # 加载occupancy_map_monitor配置 moveit_config.planning_pipelines[ompl][your_robot][octomap_frame] base_link moveit_config.planning_pipelines[ompl][your_robot][octomap_resolution] 0.05 move_group_node Node( packagemoveit_ros_move_group, executablemove_group, outputscreen, parameters[moveit_config.to_dict()], ) return LaunchDescription([move_group_node])这里的关键是octomap_frame必须和octomap_server的frame_id一致。如果octomap_server发布的是base_link这里也必须是base_link。4.2 让PlanningScene订阅Octomap话题occupancy_map_monitor默认订阅的话题是/occupied_cells_vis_array这是octomap_server发布的visualization_msgs/MarkerArray。但MoveIt2更推荐用/octomap_binary话题因为它是octomap_msgs/Octomap类型可以直接反序列化成八叉树效率更高。在move_group的配置里指定occupancy_map_monitor: occupancy_map_monitor: octomap_topic: /octomap_binary octomap_frame: base_link octomap_resolution: 0.05然后在move_group的launch里加载这个配置。如果配置正确启动move_group后你应该能在日志里看到[INFO] [move_group]: Found octomap topic: /octomap_binary [INFO] [move_group]: Octomap monitor started4.3 在RViz2里验证避障效果RViz2里需要添加几个显示PlanningScene显示MoveIt2的规划场景包括机器人模型和碰撞体。PointCloud2显示原始点云话题选/camera/depth/points。MarkerArray显示Octomap的占据体素话题选/occupied_cells_vis_array。Octomap如果装了octomap_rviz_plugins可以直接显示/octomap_full。验证步骤在机械臂前方放一个障碍物比如一个纸箱。观察RViz2里PlanningScene的碰撞体是否出现了纸箱对应的体素块。用MoveIt2的MotionPlanning面板规划一条从A点到B点的轨迹B点在纸箱后面。如果规划成功且轨迹绕开了纸箱说明避障生效了。如果碰撞体没出现检查move_group的日志里有没有Octomap monitor相关的报错。最常见的问题是octomap_frame和octomap_server的frame_id不一致导致变换失败。注意MoveIt2的PlanningScene默认只把Octomap的占据体素作为碰撞体但不会自动更新。如果障碍物移动了需要等octomap_server更新地图后PlanningScene才会跟着更新。这个更新频率取决于octomap_server的update_frequency参数默认是1Hz。对于动态避障可以调到5Hz但计算量会增大。5. 常见问题与排查技巧实录5.1 点云话题有数据但Octomap地图为空这是最常见的问题原因通常有三个原因一QoS不匹配。前面讲过depth_image_proc发的是SENSOR_DATAoctomap_server收的是RELIABLE。用ros2 topic info /camera/depth/points --verbose看QoS如果Reliability是BEST_EFFORT而octomap_server订阅的是RELIABLE那就收不到。解决办法是用topic_tools relay中继。原因二TF变换失败。octomap_server需要把点云从camera_depth_frame转到frame_id指定的坐标系。如果TF树里没有这条路径或者时间戳对不上点云会被丢弃。用ros2 run tf2_ros tf2_echo base_link camera_depth_frame检查。原因三max_range太小。如果max_range设成0.5米而障碍物在1米外点云全被截断地图自然是空的。检查max_range是否大于实际障碍物距离。5.2 地图体素抖动严重Octomap的体素抖动通常是因为深度图噪声大。AstraPro在2米以外的深度噪声可以达到几厘米导致同一个障碍物的体素在“占据”和“空闲”之间反复跳变。解决办法降低max_range到2米以内。增大sensor_model/hit和sensor_model/miss的差距比如hit0.8、miss0.3让概率更新更“果断”。在点云转换前加一个pcl_ros的VoxelGrid滤波把点云降采样到1厘米的粒度减少噪声点。5.3 MoveIt2规划时忽略Octomap障碍如果RViz2里能看到Octomap的体素但MoveIt2规划时还是撞上去检查以下几点move_group的occupancy_map_monitor是否真的加载了。在move_group的日志里搜octomap如果没有相关日志说明插件没加载。PlanningScene的octomap_frame是否和octomap_server的frame_id一致。规划组的kinematics_solver是否配置正确。有些求解器比如KDL对碰撞检测的支持不如OMPL换OMPL试试。5.4 常见问题速查表现象可能原因排查方法解决Octomap地图为空QoS不匹配ros2 topic info --verbose用topic_tools relay中继点云飘移TF错误tf2_echo base_link camera_depth_frame修正静态TF或URDF体素抖动深度噪声看原始点云降max_range、加滤波MoveIt2忽略障碍插件未加载看move_group日志检查occupancy_map_monitor配置地图更新慢update_frequency低看参数调到5Hz内存暴涨resolution太小free -h调到0.05实操心得我习惯在调试时把octomap_server的latch参数设成true这样即使点云断流地图也会保持最后一帧方便在RViz2里慢慢看。但正式跑的时候要设成false否则障碍物移走了地图还留着会导致“幽灵障碍”。6. 性能优化与扩展思路6.1 降低计算量的几个手段Octomap的射线投射是计算大头。每帧点云有几十万个点每个点都要从相机原点投射一条射线更新沿途所有体素的概率。如果resolution是0.02米一条3米长的射线要更新150个体素30万点就是4500万次更新CPU直接跑满。优化手段降采样点云用pcl_ros的VoxelGrid把点云降到1厘米粒度点数减少到几万计算量降一个数量级。限制max_range3米降到2米射线长度减少三分之一。增大resolution0.02米改成0.05米体素数量减少到原来的1/15。用octomap_server的filter_ground地面点占点云的一半以上过滤掉能省大量计算。我实测下来AstraPro在640×480分辨率下点云约30万点经过1厘米降采样后约5万点resolution0.05、max_range2.5在Intel i7-10700上CPU占用约15%完全够用。6.2 多传感器融合的扩展单个AstraPro的视野有限机械臂后方和侧面是盲区。如果要全向避障可以加一个激光雷达比如Mid-360或者第二个深度相机。Octomap支持多传感器输入只要把多个点云话题都remap到cloud_in就行。但要注意不同传感器的frame_id不同TF树要配全。多传感器融合的另一个好处是冗余。AstraPro在强光下深度图会失效激光雷达不受影响。两者互补地图更可靠。6.3 与导航栈的联动如果机械臂装在移动底盘上Octomap还可以同时给Nav2用。Nav2的costmap_2d可以订阅Octomap的/projected_map话题把3D占据投影到2D栅格作为全局代价地图。这样一套地图同时服务机械臂避障和底盘导航省去重复建图。配置方法是在Nav2的costmap参数里加obstacle_layer: observation_sources: octomap octomap: topic: /projected_map data_type: OccupancyGrid marking: true clearing: true/projected_map是octomap_server发布的2D投影地图类型是nav_msgs/OccupancyGridNav2直接能用。6.4 实时性调优的边界避障的实时性取决于从“障碍物出现”到“规划器感知到”的延迟。这条链路上每一环都有延迟相机曝光和深度计算约30ms点云转换约10msOctomap更新约50ms取决于点数和分辨率MoveIt2 PlanningScene更新约20ms规划器重新规划约100ms总延迟约200ms。对于低速机械臂末端速度0.1m/s够用高速场景需要进一步优化。优化方向是减少Octomap更新延迟比如用octomap_server的incremental模式只更新变化的体素而不是全图重建。我在实际项目里踩过最大的坑是TF时间戳。AstraPro的驱动发布TF的时间戳和点云的时间戳有时差几毫秒octomap_server默认用点云时间戳查TF如果TF缓存里没有对应时刻的变换就会报Lookup would require extrapolation into the future。解决办法是在octomap_server的参数里把tf_cache_time调大或者用tf2_ros的BufferCore设置更大的缓存。这个坑花了我整整一个下午才定位到希望你能避开。