Files
rtabmap_ros/rtabmap_util/doc/obstacles_detection.md
T
matlabbeandmathieu86 11edc01d6a rtabmap_odom tests and doc (#1456)
* rtabmap_odom tests and doc

* opengv note

* added ci checks or humble-latest flaky dep cmake errors

* Added real data tests for rgbd_odom and stereo_odom

* added real data for icp_odometry's deskewing test

* fixing json cmake error on lyrical/rolling

* test 2d icp odom deskewing branch

* first review of existing OdometryROS tests

* testing with imu used as guess

* tested imu arrivals sync

* Fixed odom reset on right pose when guess frame id is used

* fixing header errors in ci >=lyrical

* Added support for input rgbd_image topic with features for odom, added multicam rgbd_odometry test

* Added stereo odom support for features-only frames. Added multicam stereo tests.

* forcing latest rtabmap version

* updated OdometryROS API

* ci: dont build non-latest docker in pull requests

* splitting docker jobs

* doc edit

* Making publish_null_when_lost:=false continous when guess is provided (using guess covariance when we cannot register yet)

* updated stereo doc

* ficing rolling ci (rviz Ogre header)

* Added test coverage of alll rgbd_image callbacks

* fixing rolling ci

* making docker ci build/run the tests on pull requests

* fixing ros2 ci testing

* improved sync callback coverage

* improving stereo_odometry test coverage

* improved icp_odometry test coverage

* lyrical voxel_grid ptr error

* make multicam tests working as well without opengv

* removing deps of missing packages on rolling

* PCL empty cloud  conversion compiler errors fix

* fixing icp_odometry test failure on ci witohut libpointmatcher

* fixing nav2 costmap plugin build on lyrical

* joining thread when exiting

* updating icp test to work the same on pcl 1.15 (lyrical)

* Fix parallel tests seg fault

---------

Co-authored-by: mathieu86 <[email protected]>
2026-09-21 17:02:45 -07:00

8.1 KiB

obstacles_detection

Segments a point cloud into ground and obstacles.

The node takes a cloud, works out which points belong to the floor and which stick up from it, and publishes the two apart. Downstream that feeds navigation: obstacles into a costmap, ground into a traversability check.

The segmentation is RTAB-Map's own LocalGridMaker, so it is configured through the same Grid/* parameters as RTAB-Map itself and produces the same result the SLAM node would.

Contents

Usage

ros2 run rtabmap_util obstacles_detection --ros-args \
  -r cloud:=/camera/cloud \
  -p frame_id:=base_link \
  -p Grid/MaxObstacleHeight:=2.0
ComposableNode(
    package='rtabmap_util',
    plugin='rtabmap_util::ObstaclesDetection',
    name='obstacles_detection',
    parameters=[{'frame_id': 'base_link', 'Grid/MaxObstacleHeight': '2.0'}],
    remappings=[('cloud', '/camera/cloud')])

Feeding a nav2 costmap

The usual reason to run this node: nav2's costmap wants to be told separately what is floor and what is in the way. A depth camera gives neither directly, so the chain is depth image → cloud → segmented cloud → costmap.

point_cloud_xyz projects the depth image, with decimation and voxel_size set to keep the cost down, and this node splits the result:

Node(
    package='rtabmap_util', executable='point_cloud_xyz',
    parameters=[{'decimation': 2, 'max_depth': 3.0, 'voxel_size': 0.02}],
    remappings=[('depth/image', '/camera/depth/image_raw'),
                ('depth/camera_info', '/camera/camera_info'),
                ('cloud', '/camera/cloud')]),

Node(
    package='rtabmap_util', executable='obstacles_detection',
    parameters=[{'frame_id': 'base_link'}],
    remappings=[('cloud', '/camera/cloud'),
                ('ground', '/camera/ground'),
                ('obstacles', '/camera/obstacles')]),

The two outputs then become two observation sources on the costmap's voxel layer:

local_costmap:
  local_costmap:
    ros__parameters:
      plugins: ["voxel_layer", "inflation_layer"]
      voxel_layer:
        plugin: "nav2_costmap_2d::VoxelLayer"
        enabled: True
        publish_voxel_map: True
        origin_z: 0.0
        z_resolution: 0.05
        z_voxels: 16
        max_obstacle_height: 2.0
        mark_threshold: 0
        observation_sources: ground obstacles
        ground:
          topic: /camera/ground
          data_type: "PointCloud2"
          max_obstacle_height: 0.4
          marking: False        # the floor is not an obstacle...
          clearing: True        # ...but seeing it proves the space is free
          raytrace_max_range: 3.0
          raytrace_min_range: 0.0
          obstacle_max_range: 2.5
          obstacle_min_range: 0.0
        obstacles:
          topic: /camera/obstacles
          data_type: "PointCloud2"
          max_obstacle_height: 0.4
          marking: True
          clearing: True
          raytrace_max_range: 3.0
          raytrace_min_range: 0.0
          obstacle_max_range: 2.5
          obstacle_min_range: 0.0

The marking/clearing split is the whole point. Ground points only clear: they tell the costmap that the space the camera looked through is free, without writing an obstacle at floor level. Obstacle points do both, so an obstacle that moves away is cleared by the next observation instead of lingering.

Feeding the raw cloud in as a single source cannot do this — every floor point would mark an obstacle and the robot would refuse to move. Working from turtlebot3_rgbd.launch.py and its nav2 parameters will save some time.

A depth camera turned into ground and obstacle clouds for a costmap:

flowchart LR
    CAM["camera driver"]
    XYZ["point_cloud_xyz"]
    OBST["obstacles_detection<br>frame_id: base_link"]
    NAV["nav2 costmap"]
    CAM -->|"depth/image,<br>camera_info"| XYZ
    XYZ -->|cloud| OBST
    OBST -->|ground| NAV
    OBST -->|obstacles| NAV

Subscribed Topics

Topic Type Description
cloud sensor_msgs/msg/PointCloud2 The cloud to segment, in any frame that TF can relate to frame_id.

Published Topics

Topic Type Description
ground sensor_msgs/msg/PointCloud2 Points classified as floor. In the input cloud's frame.
obstacles sensor_msgs/msg/PointCloud2 Points classified as obstacles. In the input cloud's frame.
proj_obstacles sensor_msgs/msg/PointCloud2 The obstacles flattened onto the ground plane, in frame_id. This is the 2D footprint a planar costmap wants.

Each output is computed only if something is subscribed to it.

Required Transforms

Transform Description
frame_id → cloud frame Where the sensor sits on the robot. Segmentation happens in frame_id, so this is what makes "up" meaningful.
map_frame_id → frame_id Only when map_frame_id is set. See Levelling on a slope.

Parameters

Parameter Type Default Description
frame_id string "base_link" The robot frame. Its xy plane is the ground plane the segmentation works against.
map_frame_id string "" See Levelling on a slope.
wait_for_transform double 0.2 Seconds to wait for a transform before dropping the cloud.
qos int 0 Reliability of the subscription and the publishers: 0 system default, 1 reliable, 2 best effort.

Every RTAB-Map Grid/* parameter is also exposed as a ROS parameter of this node, and they are what actually control the segmentation. They are documented in RTAB-Map's parameter reference.

The one to decide first is Grid/NormalsSegmentation, which picks between two ways of finding the ground. Left on, it is segmented from surface normals, which copes with slopes and steps. Turned off, it is a plain height threshold: much cheaper, and exact when the floor really is flat, but Grid/MaxGroundHeight then has to be set, since it is that threshold.

Levelling on a slope

Segmentation is done in frame_id, so if the robot is pitched or rolled — on a ramp, or with a suspension that dips — the ground plane tilts with it and the floor ahead can be classified as an obstacle.

Setting map_frame_id makes the node take the robot's pose in that frame and apply its roll and pitch, so segmentation happens against a level plane rather than the robot's own tilt.

Height is a separate matter: the robot's z in the map frame is ignored unless Grid/MapFrameProjection is also set to true. That is usually what you want — a height threshold should be measured from the robot, not from an arbitrary map origin — but if you are mapping a multi-level building and want the thresholds relative to the map, enable it.

Notes

If obstacles comes back empty on an obviously cluttered scene, check Grid/RangeMax first. It is not unlimited by default, and everything beyond it is discarded before segmentation even runs.

The second thing to check is Grid/MinClusterSize against your cloud density. A sparse lidar can produce clusters smaller than the default, in which case every obstacle is thrown away as noise. Either lower it or raise Grid/ClusterRadius.