MoveIt2 感知模块演化全景:moveit_ros_perception 的 OctoMap 更新链路与工程演进

  • 机器人
  • ROS

【免费下载链接】moveit2

:robot: MoveIt for ROS 2

项目地址: https://gitcode.com/gh_mirrors/mo/moveit2
点击查看 免费下载

导读:moveit_ros_perception 是 MoveIt2(ROS 2 版 MoveIt)中连接机器人感知数据的核心软件包,负责把深度相机图像与激光/深度点云实时融入碰撞检测所需的 OctoMap 占据地图。本文以该包的 CHANGELOG.rst 为时间主线,结合当前仓库(版本 2.15.2)的源码实现,系统讲解两大 OctoMap 更新器、GPU 网格自过滤、异步自由空间更新、机器人本体点云掩蔽等核心机制,以及 ROS 2 移植、QoS、构建系统等跨版本工程演进。读完本文,你将掌握这些感知模块的参数含义、底层调用链与排障思路,能够在自己的 MoveIt2 配置中正确选用和调优感知更新链路。

一、包定位:moveit_ros_perception 在 MoveIt2 中的角色

从 package.xml 可见,该包的描述是 “Components of MoveIt connecting to perception”,当前版本为 2.15.2,许可证为 BSD-3-Clause。它依赖 moveit_core、moveit_msgs、moveit_ros_occupancy_map_monitor、moveit_ros_planning 以及 image_transport、sensor_msgs、tf2*、cv_bridge、pluginlib 等 ROS 2 组件,还依赖 glut、libglew-dev、libomp-dev、opengl 等图形与并行库——这从依赖层面就暗示了它既处理传感器消息,也依赖 GPU 渲染与 OpenMP 并行加速。

从 CMakeLists.txt 可以看到它的内部结构:整个包由六个子库组成,分别对应六个子目录:

子库库目标名职责
depth_image_octomap_updatermoveit_depth_image_octomap_updater / _core用深度图像(sensor_msgs/Image + CameraInfo)更新 OctoMap
pointcloud_octomap_updatermoveit_pointcloud_octomap_updater / _core用点云(sensor_msgs/PointCloud2)更新 OctoMap
lazy_free_space_updatermoveit_lazy_free_space_updater异步批量计算自由空间(free cells)
mesh_filtermoveit_mesh_filter基于 OpenGL 的网格自过滤(self-filtering)
point_containment_filtermoveit_point_containment_filter判断点云中的点是否落在机器人自身形状内(ShapeMask)
semantic_worldmoveit_semantic_world订阅表格识别结果并生成放置(place)位姿

其中 mesh_filter 与 depth_image_octomap_updater 仅在 WITH_OPENGL 选项为 ON(默认 ON)时构建。CMake 里还创建了一个 INTERFACE 目标 moveit_ros_perception::moveit_ros_perception,下游包可以一次性链接全部六个库。两个 OctoMap 更新器都通过 pluginlib 导出插件,分别注册为 occupancy_map_monitor/DepthImageOctomapUpdater 与 occupancy_map_monitor/PointCloudOctomapUpdater,基类为 occupancy_map_monitor::OccupancyMapUpdater(见 occupancy_map_updater.hpp)。

二、两大 OctoMap 更新器:感知数据进入规划场景的入口

OccupancyMapMonitor(在 occupancy_map_monitor.cpp 中)通过 pluginlib 按 sensor_type 加载更新器,依次调用 setMonitor → setParams → initialize,最后 addUpdater 把更新器接入地图。两个更新器共享基类定义的生命周期:setParams(读参数)、initialize(建订阅/缓存)、start/stop(启停回调)、excludeShape/forgetShape(把机器人或物体的形状排除在占据判定之外)。

2.1 depth_image_octomap_updater:深度图像 → OctoMap

该更新器在 depth_image_octomap_updater.cpp 中实现,构造函数给出了全部参数的默认值,setParams 从节点参数服务器读取。参数清单与默认值如下:

参数默认值说明
image_topic"depth"深度图像话题(与 CameraInfo 成对订阅)
queue_size5消息缓存队列长度
near_clipping_plane_distance0.3近裁剪平面距离(米)
far_clipping_plane_distance5.0远裁剪平面距离(米),决定感知范围
shadow_threshold0.04阴影阈值,用于区分“被模型遮挡”与“背景”
padding_scale0.0模型外扩的缩放系数
padding_offset0.02模型外扩的固定偏移(米)
max_update_rate0最大更新频率,0 表示不限速
skip_vertical_pixels4垂直方向跳过的边缘像素
skip_horizontal_pixels6水平方向跳过的边缘像素
filtered_cloud_topic空非空时发布过滤后的深度图(16UC1,毫米)
ns空发布话题的命名空间前缀

核心处理流程(见 depthImageCallback):

  1. 限频与统计:若 max_update_rate_ > 0,用 last_update_time_ 做节流;同时统计回调平均间隔 average_callback_dt_,供后续 TF 等待时长估算。
  2. TF 变换:把深度图像帧变换到地图帧(map_h_sensor)。若 TF 暂不可用,会按 queue_size 折算次数重试,并统计 good_tf_/failed_tf_,当失败数超过成功数时输出告警——这正是 CHANGELOG 中反复出现“TF unavailable”相关修复的落点。
  3. 网格过滤:利用 CameraInfo 的内参(k[0]、k[4]、k[2]、k[5])驱动 mesh_filter::StereoCameraModel,把机器人模型渲染成“模型深度”,与传感器深度比对后得到每个像素的标签(背景 / 模型 / 远裁剪)。
  4. 体素归类:背景像素按针孔模型反投影成 3D 点,tree_->coordToKey 得到占据体素(occupied_cells);落在远裁剪平面或模型上的像素记为 model_cells,之后把 occupied_cells 中与模型重叠的体素剔除(“与模型重叠的 cell 不视为占据”)。
  5. 地图更新:加写锁后 tree_->updateNode(occupied_cell, true) 标记占据,再通过 free_space_updater_->pushLazyUpdate(...) 把 occupied/model cell 集合与传感器原点交给后台线程做自由空间清理(见第四节)。
  6. 调试输出:debug_info_ 开启时,通过 image_transport 发布 model_depth、filtered_depth、filtered_label 三路调试图像;filtered_cloud_topic_ 非空时另发布一版 16UC1 过滤深度图(毫米)。

CHANGELOG 中与该更新器相关的关键修复包括:

  • 2.1.1:Port moveit_ros_perception/depth_image_octomap_updater (#354),并修复两个移植错误——“无法收到深度图像”与“时间源不匹配”。源码中 last_update_time_ = rclcpp::Time(0, 0, RCL_ROS_TIME)(见 depth_image_octomap_updater.hpp)的注释正是为了“prevent exception about time sources mismatch”。
  • 2.8.0:改用 isnan 判断相机内参是否有效(避免 px == px 这类写法被静态分析器误报),对应源码中 isnan(px) 分支。
  • 2.11.0:Fixed segmentation fault in depth_image_octomap_updater (#2963)——这类崩溃通常与图像尺寸变化导致内部缓存(x_cache_/y_cache_)与 filtered_labels_ 扩容不同步有关,源码中所有缓存都按“小于则 resize”的规则按需扩容。
  • 2.15.0:fix depth_image_octomap_updater for image_transport 7.x (#3756),对应源码中按 RCLCPP_VERSION_GTE(30, 0, 0) 区分 ImageTransport(*node_) 与 ImageTransport(node_) 的写法,并改用 image_transport::create_camera_subscription(*node_, ...)(见 depth_image_octomap_updater.cpp 与 start()),以兼容 Humble 到 Rolling 的不同 image_transport 版本。

2.2 pointcloud_octomap_updater:点云 → OctoMap

该更新器在 pointcloud_octomap_updater.cpp 中实现。与深度图更新器不同,它的参数全部为必选——setParams 会收集缺失参数名并打印 Missing parameters under '...' 错误日志(见 setParams):

参数默认值说明
point_cloud_topic无(必填)点云话题
max_range无(必填)最大感知距离,超出视为裁剪(clip)点
padding_offset无(必填)机器人形状外扩偏移
padding_scale无(必填)机器人形状外扩缩放
point_subsample无(必填)点采样步长,大于 1 可降低计算量
max_update_rate无(必填)最大更新频率
filtered_cloud_topic无(必填,可为空串)非空时发布过滤后的点云
ns空(可选)命名空间前缀

核心处理流程(见 cloudMsgCallback):

  1. 订阅使用 message_filters::Subscriber + tf2_ros::MessageFilter(目标帧为地图帧),并采用 rclcpp::SensorDataQoS()(RCLCPP_VERSION_GTE(28, 3, 0) 之后)与独立互斥回调组,避免传感器大消息阻塞主逻辑。
  2. 对每个点,先用 ShapeMask::maskContainment 得到三类掩码:INSIDE(落在机器人模型内,记为 model_cells)、CLIP(超出传感器范围,沿射线方向归一化后截断到 max_range_,记为 clip_cells)、OUTSIDE(正常占据,记为 occupied_cells)。对 NaN 点直接跳过。
  3. 射线求自由空间:对每个 occupied / model / clip 端点用 tree_->computeRayKeys 计算射线穿过的体素并放入 free_cells;clip 单元格本身也视为自由(“超出量程的方向上没有障碍物”)。
  4. 写地图:free_cells 标为 false、occupied_cells 标为 true、model_cells 设为最小 log-odds(getClampingThresMinLog() - getClampingThresMaxLog()),随后 triggerUpdateCallback() 通知规划场景刷新。
  5. 若 filtered_cloud_topic_ 非空,把保留下来的有效点重新打包成 PointCloud2 发布(仅含 xyz 字段,尺寸动态 resize 到实际保留点数)。

CHANGELOG 中与之相关的修复:

  • 2.5.3:Don't return false when not finding optional parameter (#1418)——ns 是可选的,找不到时不应导致整个初始化失败,对应源码中 get_parameter_or(name_space + ".ns", ns_, std::string()) 的写法。
  • 2.13.0:fix: pointcloud_octomap_updater not cleaning objects at max_range (#3294)——修复了量程边界处物体残留的问题,正是通过把 clip 端点体素本身加入 free_cells 并在射线求交时一并处理实现的(见 clip_cells 处理)。同版本还修复了“OctoMap 与 Filtered_Cloud 在运动执行期间不更新”的问题。
  • 2.14.1:More user-friendly log message for pointcloud_octomap_updater (#3514),把参数缺失时的报错改为逐条列出缺失键名。

三、mesh_filter:用 OpenGL 把“机器人本体”从深度图中滤掉

自过滤(self-filtering)是 MoveIt 感知里最实用也最容易出问题的环节:深度相机必然会扫到机械臂本体,若不滤除,OctoMap 会把机器人自身当成障碍物。mesh_filter 子模块用 GPU 渲染解决这一问题。

在 mesh_filter_base.cpp 中可以看到其经典双 pass 架构:

  • Pass 1(渲染):mesh_renderer_ 用真实相机内参(GLRenderer 内部通过 setCameraParameters(fx, fy, cx, cy) 构建透视投影)把机器人网格渲染到离屏 FBO,输出模型深度纹理与模型标签(每个网格一个唯一 handle 对应的颜色标签)。
  • Pass 2(过滤):depth_filter_ 把传感器深度图作为纹理(GL_DEPTH_COMPONENT,支持 GL_FLOAT 与 GL_UNSIGNED_SHORT 两种编码),与模型深度在同一片段着色器中按 shadow_threshold 阈值比较,输出每个像素的标签:背景(BACKGROUND)、模型(对应 mesh handle)、远裁剪(FAR_CLIP 及以上)。
  • 异步执行:MeshFilterBase 构造函数启动独立 filter_thread_,所有渲染/读取操作以 FilterJob 任务队列的方式提交(见 run()),保证 OpenGL 上下文只在单线程中使用,这正是多次“段错误修复”的工程基础。
  • 外扩控制:padding_coefficients = sensor_parameters->getPaddingCoefficients() * padding_scale_ + (0,0,padding_offset_),对应参数 padding_scale/padding_offset 的物理含义——模型沿射线方向外扩,防止把紧贴机械臂的物体误滤。

gl_renderer.cpp 中 GLRenderer 创建自己的 OpenGL 上下文与帧缓冲(fbo_id_/rbo_id_/rgb_id_/depth_id_),并通过 freeglut 保证无显示环境(如无头服务器)下也能工作。CHANGELOG 相关条目:

  • 0.6.6:为 glut 窗口注册空显示回调,规避 freeglut 3.0 下 “No display callback registered for window” 的启动失败。
  • 2.10.0:Fix segmentation fault in mesh_filter/gl_renderer (#2834),属于渲染上下文生命周期类问题。
  • 2.9.0:Converts float to double (#2343) 大 PR 波及此模块——GLRenderer 函数定义改为 double,同时保持虚函数覆写所需参数类型不变,消除了大量 clang-tidy 告警。
  • 1.0.6 / 1.0.4 / 1.0.3:多次提高 mesh_filter_test 的接受阈值并修复测试,mesh_filter_test.cpp 至今保留着基于随机网格 + 渲染比对的自过滤验证用例。
  • 0.10.2:make OpenGL parts optional (#698) 对应的正是 CMake 中的 WITH_OPENGL 选项,gl_renderer 相关代码仅在开启时编译。

此外,mesh_filter 还提供一个 nodelet 插件 mesh_filter/DepthSelfFiltering(见 moveit_depth_self_filter.xml),描述为 “Nodelet for filtering meshes from depth images, e.g. meshes of the robot or any attached object where a transformation can be provided for”,即把同一套 GPU 过滤逻辑暴露给需要深度自过滤的其他节点复用。

四、lazy_free_space_updater:把“清除自由空间”挪到后台

占据地图不只标记障碍物,还要定期把传感器射线穿过的体素清为“自由”,否则机器人移动后旧障碍会残留。这一计算量大,LazyFreeSpaceUpdater 用两个后台线程异步完成(见 lazy_free_space_updater.cpp):

  • lazyUpdateThread(聚合线程):把深度/点云回调里 pushLazyUpdate 进来的 occupied/model cell 集合与传感器原点按批聚合;只要传感器原点位移不超过 max_sensor_delta_(默认 1 mm)就并入同一批,达到 max_batch_size_ 或原点漂移后交给处理线程。
  • processThread(处理线程):对每批数据用 tree_->computeRayKeys 求射线穿过的体素,统计每个自由 cell 被射线命中的次数,再以 lg_miss(miss 的 log-odds)加权更新树;模型 cell 置为最小 log-odds。两个方向(occupied 端点、model 端点)的射线求交用 #pragma omp sections 并行。
  • 背压策略:处理线程的队列长度固定为 1,若上一批未完成则丢弃新批并输出警告 “Previous batch update did not complete”——这是有意的丢数据设计,避免花过多时间清理地图。

CHANGELOG 相关条目:0.6.5 中 “adding RAII-based locking for OccMapTree”、“moving lazy_free_space_updater into it's own library”,以及 0.6.6 中 “Remove OpenMP parallelization”(后又在 0.10.6 重新 “Enforce OpenMP support for perception”),反映了并行策略在早期版本中的反复权衡。

五、point_containment_filter:用包围球加速的本体点云掩蔽

ShapeMask(见 shape_mask.cpp)负责回答“点云里的这个点是否落在机器人/被排除物体内部”。实现要点:

  1. 每个被排除形状(来自 excludeShape)构造一个 bodies::Body(createEmptyBodyFromShapeType 按形状类型创建),记录其体积并按体积排序(std::set<SeeShape, SortBodies>)。
  2. 处理点云时,先为每个 body 计算包围球,再用 bodies::mergeBoundingSpheres 求整个机器人的总包围球:只有落入总包围球内的点才需要逐个 body 做精确的 containsPoint 判定,这是最主要的加速手段。
  3. 掩码结果三态:OUTSIDE(保留)、INSIDE(机器人本体,映射为 model cell)、CLIP(超出传感器量程,映射为 clip cell)。

CHANGELOG 相关条目:2.9.0 的 (moveit_ros) add missing CYLINDER check (#2640) 明确指出“该检查缺失导致后面的 CYLINDER 分支不可达”,这是对 semantic_world 中 generatePlacePoses 形状类型分发逻辑的修复——当前源码中该函数已完整覆盖 MESH、SPHERE、BOX、CYLINDER、CONE 五种形状(见 semantic_world.cpp)。1.0.3 中 “Allow subclassing of point_containment_filter::ShapeMask (#1457)” 则是把该类设计为可被下游扩展。

六、semantic_world:表格识别与放置位姿生成

semantic_world 是感知与操作(manipulation)衔接的桥梁:它订阅 object_recognition_msgs/TableArray,把识别出的桌子加入 PlanningScene 碰撞世界,并生成可供机械臂放置(place)物体的候选位姿。核心能力(见 semantic_world.cpp):

  • addTablesToCollisionWorld:先发布 diff 场景移除旧桌子,再把每张桌子的凸包顶点构建成网格,经 orientPlanarPolygon 统一法线朝上后,以 CollisionObject 形式 ADD 进 planning_scene 话题。
  • generatePlacePoses:对桌面的 convex_hull 用 OpenCV 的 cv::findContours + cv::pointPolygonTest 做多边形内外判定,按 resolution 步长、num_heights 层高度(delta_height 间隔)枚举候选点,并依据物体形状(MESH/BOX/SPHERE/CYLINDER/CONE)计算 min_distance_from_edge 与 height_above_table,保证位姿不悬空、不越界。
  • findObjectTable / isInsideTableContour:给定一个物体位姿,反查它位于哪张桌子之上。
  • tableCallback:收到 TableArray 后通过 planning scene 的帧变换把桌子位姿统一到目标坐标系(transformTableArray),并触发用户注册的回调。

该模块在 CHANGELOG 中的痕迹包括 0.9.6 对编译开关的修复,以及 2.9.0 的 CYLINDER 检查补漏;它的 table_subscriber_ 也随 2.7.2 从 qos_event.hpp 迁移到 event_handler.hpp(见 semantic_world.cpp 的版本条件包含)。

七、工程化演进主线:从 ROS 1 移植到现代 ROS 2 实践

CHANGELOG 的时间线(0.5.x → 2.15.2)完整记录了该包十多年的工程演化,可归纳为几条主线:

1. ROS 2 移植与 API 适配(2.0 ~ 2.2)

  • 2.1.1(2021-04)集中移植 depth_image_octomap_updater 与 pointcloud_octomap_updater,修复“收不到深度图像”与“时间源不匹配”;2.0.0(2020-10)是 ROS 2 版本线的起点。
  • 2.2.0 修复 macOS 编译;2.3.0 修复 Galactic/Rolling 的告警并用 __has_include 兼容废弃头文件,同时把 OccMapTree 迁入 moveit_core/collision_detection。

2. QoS 与线程模型的演进(2.3 ~ 2.8)

  • 2.3.1:clipped points should only be considered up to the max_range——修复 clip 点处理;同版本还启用了 codespell 拼写检查与 package.xml format 3。
  • 2.7.0:传感器消息改用 SensorDataQoS(sensor_msgs::Image 与 PointCloud2),源码中两个更新器目前分别以 rclcpp::SensorDataQoS() 创建订阅。
  • 2.7.2:Switch from qos_event.hpp to event_handler.hpp (#2111),为同时兼容 Humble 与 main 分支增加了条件编译。
  • 2.8.0:Replaced numbers with SystemDefaultsQos() (#2271),统一 QoS 常量使用。

3. tf2 与现代 C++ 规范(2.5 ~ 2.15)

  • 2.5.x:boost::bind → std::bind → lambda(“banish bind()”),new 改 make_shared。
  • 2.7.0:启用 -Wold-style-cast,全面改用 C++ 风格类型转换、<> 引用非本地头文件。
  • 2.9.0:大规模 float → double 重构、统一 moveit_core/moveit_ros 节点日志、新增 clang-tidy 规则并落地 clang-format-14 的 QualifierAlignment。
  • 2.13.0:Update deprecated tf2 imports from .h to .hpp (#3197)——源码中 __has_include(<tf2/LinearMath/Vector3.hpp>)、<tf2_ros/buffer.hpp> 等条件包含正是这一迁移的兼容层(见 pointcloud_octomap_updater.cpp)。
  • 2.15.0:Remove use of ament_target_dependencies (#3726),改用导出 CMake targets(如 moveit_ros_planning::moveit_ros_planning);同时为 GCC 15 补充 <cstdint>/<fstream> 显式包含。
  • 2.15.1:适配 tf2_ros 移除双接口构造函数后的 CreateTimerROS 新 API(源码中按 RCLCPP_VERSION_GTE(29, 6, 0) 分支选择 CreateTimerROS(*node) 或 CreateTimerROS(base_interface, timers_interface),见 pointcloud_octomap_updater.cpp),并弃用 FindBoost.cmake。
  • 2.15.2:本轮 clang-tidy 清理。

4. 组织与平台(2.10 ~ 2.15)

  • 2.10.0:Migrate ros-planning org to moveit (#2847),仓库与镜像地址全面迁移到 moveit 组织;同版本统一日志命名空间(如 moveit.ros.depth_image_octomap_updater,见源码中 moveit::getLogger("moveit.ros.depth_image_octomap_updater"))。
  • 2.12.0:头文件从 .h 全面转向 .hpp(Enhancement/use hpp for headers (#3113))。
  • 2.15.0/2.15.1:持续修复 Ubuntu 26.04 CI 与 macOS 链接问题。

八、如何在自己的 MoveIt2 中使用与验证

该包随 MoveIt2 源码一起构建,标准流程为在 MoveIt2 工作空间下:

colcon build --packages-select moveit_ros_perception

(若不需要 OpenGL 相关子模块,可在构建时关闭 WITH_OPENGL。)两个 OctoMap 更新器作为插件注册在 pointcloud_octomap_updater_plugin_description.xml 与 depth_image_octomap_updater_plugin_description.xml 中,最终由 moveit_ros_occupancy_map_monitor 包按 sensor_type 加载,因此配置感知时通常需要在 occupancy map monitor 的参数中指定 sensor_type: PointCloudOctomapUpdater(或 DepthImageOctomapUpdater)以及上文列出的参数。调试时可开启更新器的 debug_info_(publishDebugInformation),通过 RViz 订阅 model_depth / filtered_depth / filtered_label / 过滤点云话题,直观检查自过滤与量程设置是否合理。

本文所引源码与配置均位于当前仓库的 moveit_ros/perception 目录及其依赖的 moveit_ros/occupancy_map_monitor 目录,读者可结合 CHANGELOG.rst 对照各版本修复在源码中的落点,深入理解 MoveIt2 感知链路的演进脉络。

  • 机器人
  • ROS

【免费下载链接】moveit2

:robot: MoveIt for ROS 2

项目地址: https://gitcode.com/gh_mirrors/mo/moveit2
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值