From 11edc01d6adc0f4fe1e2a5336e1841d3754352eb Mon Sep 17 00:00:00 2001 From: matlabbe Date: Mon, 21 Sep 2026 17:02:45 -0700 Subject: [PATCH] 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 --- .github/workflows/coverage.yml | 4 +- .github/workflows/docker-ros2.yml | 92 +- .github/workflows/ros2.yml | 24 +- README.md | 11 +- codecov.yml | 9 +- docker/humble/latest/Dockerfile | 24 +- docker/iron/latest/Dockerfile | 12 +- docker/jazzy/latest/Dockerfile | 24 +- docker/kilted/latest/Dockerfile | 24 +- docker/lyrical/latest/Dockerfile | 26 +- docker/verify_deps.sh | 63 + rtabmap_conversions/CMakeLists.txt | 2 +- rtabmap_conversions/README.md | 15 +- .../PointCloudConversion.h | 75 + rtabmap_conversions/src/MsgConversion.cpp | 5 +- rtabmap_costmap_plugins/CMakeLists.txt | 1 + .../rtabmap_costmap_plugins/voxel_layer.hpp | 9 + rtabmap_costmap_plugins/src/voxel_layer.cpp | 115 +- rtabmap_odom/CMakeLists.txt | 48 + rtabmap_odom/README.md | 273 +++ rtabmap_odom/doc/icp_odometry.md | 238 ++ rtabmap_odom/doc/rgbd_odometry.md | 211 ++ rtabmap_odom/doc/stereo_odometry.md | 219 ++ .../include/rtabmap_odom/OdometryROS.h | 87 + .../include/rtabmap_odom/icp_odometry.hpp | 11 + .../include/rtabmap_odom/rgbd_odometry.hpp | 25 +- .../include/rtabmap_odom/stereo_odometry.hpp | 24 +- rtabmap_odom/package.xml | 6 + rtabmap_odom/rosdoc2.yaml | 35 + rtabmap_odom/src/OdometryROS.cpp | 278 ++- rtabmap_odom/src/nodelets/icp_odometry.cpp | 16 +- rtabmap_odom/src/nodelets/rgbd_odometry.cpp | 281 ++- rtabmap_odom/src/nodelets/stereo_odometry.cpp | 264 ++- rtabmap_odom/test/bag_playback.hpp | 79 + rtabmap_odom/test/camera_rig.hpp | 319 +++ rtabmap_odom/test/data/README.md | 103 + .../data/lidar/ouster_half_turn/metadata.yaml | 52 + .../ouster_half_turn/ouster_half_turn.mcap | Bin 0 -> 857414 bytes rtabmap_odom/test/data/rgbd/calib/154.yaml | 16 + rtabmap_odom/test/data/rgbd/calib/17.yaml | 16 + rtabmap_odom/test/data/rgbd/depth/154.png | Bin 0 -> 123097 bytes rtabmap_odom/test/data/rgbd/depth/17.png | Bin 0 -> 116855 bytes rtabmap_odom/test/data/rgbd/rgb/154.jpg | Bin 0 -> 78853 bytes rtabmap_odom/test/data/rgbd/rgb/17.jpg | Bin 0 -> 82283 bytes .../test/data/stereo/raw/left/420.jpg | Bin 0 -> 59318 bytes .../test/data/stereo/raw/left/425.jpg | Bin 0 -> 60695 bytes .../test/data/stereo/raw/right/420.jpg | Bin 0 -> 54879 bytes .../test/data/stereo/raw/right/425.jpg | Bin 0 -> 55597 bytes .../test/data/stereo/raw/stereo_left.yaml | 34 + .../test/data/stereo/raw/stereo_pose.yaml | 30 + .../test/data/stereo/raw/stereo_right.yaml | 34 + .../test/data/stereo/rect/left/50.jpg | Bin 0 -> 68515 bytes .../test/data/stereo/rect/left/60.jpg | Bin 0 -> 67197 bytes .../test/data/stereo/rect/right/50.jpg | Bin 0 -> 67018 bytes .../test/data/stereo/rect/right/60.jpg | Bin 0 -> 65240 bytes .../test/data/stereo/rect/stereo_left.yaml | 24 + .../test/data/stereo/rect/stereo_right.yaml | 24 + rtabmap_odom/test/msg_builders.hpp | 362 +++ rtabmap_odom/test/node_test_utils.hpp | 256 ++ rtabmap_odom/test/scan_scenes.hpp | 115 + rtabmap_odom/test/test_data.hpp | 268 +++ rtabmap_odom/test/test_icp_odometry.cpp | 2103 +++++++++++++++++ rtabmap_odom/test/test_odometry_ros.cpp | 1350 +++++++++++ rtabmap_odom/test/test_rgbd_odometry.cpp | 800 +++++++ rtabmap_odom/test/test_stereo_odometry.cpp | 1272 ++++++++++ rtabmap_python/README.md | 16 +- rtabmap_rviz_plugins/src/MapCloudDisplay.cpp | 5 +- rtabmap_rviz_plugins/src/MapGraphDisplay.cpp | 12 + rtabmap_sync/README.md | 18 +- rtabmap_sync/doc/rgb_sync.md | 10 + rtabmap_sync/doc/rgbd_sync.md | 12 + rtabmap_sync/doc/rgbdx_sync.md | 11 + rtabmap_sync/doc/stereo_sync.md | 11 + rtabmap_util/README.md | 6 + rtabmap_util/doc/db_player.md | 12 +- rtabmap_util/doc/disparity_to_depth.md | 8 + rtabmap_util/doc/imu_to_tf.md | 16 +- rtabmap_util/doc/lidar_deskewing.md | 11 + rtabmap_util/doc/map_assembler.md | 11 + rtabmap_util/doc/obstacles_detection.md | 11 + rtabmap_util/doc/point_cloud_aggregator.md | 11 + rtabmap_util/doc/point_cloud_assembler.md | 14 + rtabmap_util/doc/point_cloud_xyz.md | 9 + rtabmap_util/doc/point_cloud_xyzrgb.md | 9 + rtabmap_util/doc/pointcloud_to_depthimage.md | 11 + rtabmap_util/doc/rgbd_relay.md | 9 + rtabmap_util/doc/rgbd_split.md | 9 + rtabmap_util/src/MapsManager.cpp | 17 +- .../src/nodelets/obstacles_detection.cpp | 9 +- rtabmap_util/src/nodelets/point_cloud_xyz.cpp | 5 +- .../src/nodelets/point_cloud_xyzrgb.cpp | 5 +- 91 files changed, 9736 insertions(+), 350 deletions(-) create mode 100755 docker/verify_deps.sh create mode 100644 rtabmap_conversions/include/rtabmap_conversions/PointCloudConversion.h create mode 100644 rtabmap_odom/README.md create mode 100644 rtabmap_odom/doc/icp_odometry.md create mode 100644 rtabmap_odom/doc/rgbd_odometry.md create mode 100644 rtabmap_odom/doc/stereo_odometry.md create mode 100644 rtabmap_odom/rosdoc2.yaml create mode 100644 rtabmap_odom/test/bag_playback.hpp create mode 100644 rtabmap_odom/test/camera_rig.hpp create mode 100644 rtabmap_odom/test/data/README.md create mode 100644 rtabmap_odom/test/data/lidar/ouster_half_turn/metadata.yaml create mode 100644 rtabmap_odom/test/data/lidar/ouster_half_turn/ouster_half_turn.mcap create mode 100644 rtabmap_odom/test/data/rgbd/calib/154.yaml create mode 100644 rtabmap_odom/test/data/rgbd/calib/17.yaml create mode 100644 rtabmap_odom/test/data/rgbd/depth/154.png create mode 100644 rtabmap_odom/test/data/rgbd/depth/17.png create mode 100644 rtabmap_odom/test/data/rgbd/rgb/154.jpg create mode 100644 rtabmap_odom/test/data/rgbd/rgb/17.jpg create mode 100644 rtabmap_odom/test/data/stereo/raw/left/420.jpg create mode 100644 rtabmap_odom/test/data/stereo/raw/left/425.jpg create mode 100644 rtabmap_odom/test/data/stereo/raw/right/420.jpg create mode 100644 rtabmap_odom/test/data/stereo/raw/right/425.jpg create mode 100644 rtabmap_odom/test/data/stereo/raw/stereo_left.yaml create mode 100644 rtabmap_odom/test/data/stereo/raw/stereo_pose.yaml create mode 100644 rtabmap_odom/test/data/stereo/raw/stereo_right.yaml create mode 100644 rtabmap_odom/test/data/stereo/rect/left/50.jpg create mode 100644 rtabmap_odom/test/data/stereo/rect/left/60.jpg create mode 100644 rtabmap_odom/test/data/stereo/rect/right/50.jpg create mode 100644 rtabmap_odom/test/data/stereo/rect/right/60.jpg create mode 100644 rtabmap_odom/test/data/stereo/rect/stereo_left.yaml create mode 100644 rtabmap_odom/test/data/stereo/rect/stereo_right.yaml create mode 100644 rtabmap_odom/test/msg_builders.hpp create mode 100644 rtabmap_odom/test/node_test_utils.hpp create mode 100644 rtabmap_odom/test/scan_scenes.hpp create mode 100644 rtabmap_odom/test/test_data.hpp create mode 100644 rtabmap_odom/test/test_icp_odometry.cpp create mode 100644 rtabmap_odom/test/test_odometry_ros.cpp create mode 100644 rtabmap_odom/test/test_rgbd_odometry.cpp create mode 100644 rtabmap_odom/test/test_stereo_odometry.cpp diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 39b6a390..03d5ee80 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -60,7 +60,7 @@ jobs: # tested package depends on them (--packages-up-to), just not measured. - uses: ros-tooling/action-ros-ci@v0.4 with: - package-name: rtabmap_conversions rtabmap_util rtabmap_sync rtabmap_python + package-name: rtabmap_conversions rtabmap_util rtabmap_sync rtabmap_odom rtabmap_python target-ros2-distro: humble # RTAB-Map is installed in the image, not as an apt package, so rosdep # cannot resolve the key and must not try. @@ -120,7 +120,7 @@ jobs: . /opt/ros/humble/setup.sh # C++ packages only -- rtabmap_python emits no .gcno for lcov to read, # and is measured by the coveragepy step below instead. - PKGS="rtabmap_conversions rtabmap_util rtabmap_sync" + PKGS="rtabmap_conversions rtabmap_util rtabmap_sync rtabmap_odom" # Baseline from the .gcno files. Without it a source file that no test # ever loaded is missing from the report altogether rather than # counted as 0%, which quietly inflates the result. diff --git a/.github/workflows/docker-ros2.yml b/.github/workflows/docker-ros2.yml index 6c9f7957..541de0e0 100644 --- a/.github/workflows/docker-ros2.yml +++ b/.github/workflows/docker-ros2.yml @@ -12,56 +12,37 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + # Images built from this tree (docker/*/latest), the ones a change here can break. docker: # A manual dispatch is honored only on ros2, the only ref we push from. if: ${{ github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/ros2' }} runs-on: ubuntu-latest - + strategy: fail-fast: false matrix: - docker_tag: [humble, humble-latest, jazzy, jazzy-latest, kilted, kilted-latest, lyrical-latest] + docker_tag: [humble-latest, jazzy-latest, kilted-latest, lyrical-latest] include: - - docker_tag: humble - docker_path: 'humble' - docker_platforms: | - linux/amd64 - docker_tag: humble-latest docker_path: 'humble/latest' docker_platforms: | linux/amd64 linux/arm64 - - docker_tag: jazzy - docker_path: 'jazzy' - docker_platforms: | - linux/amd64 - linux/arm64 - docker_tag: jazzy-latest docker_path: 'jazzy/latest' docker_platforms: | linux/amd64 linux/arm64 - - docker_tag: kilted - docker_path: 'kilted' - docker_platforms: | - linux/amd64 - linux/arm64 - docker_tag: kilted-latest docker_path: 'kilted/latest' docker_platforms: | linux/amd64 - #Disabled till rtabmap_ros is released on lyrical - #- docker_tag: lyrical - # docker_path: 'lyrical' - # docker_platforms: | - # linux/amd64 - # linux/arm64 - docker_tag: lyrical-latest docker_path: 'lyrical/latest' docker_platforms: | linux/amd64 linux/arm64 - + steps: - name: Checkout @@ -90,8 +71,73 @@ jobs: context: . push: ${{ github.event_name != 'pull_request' }} platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || matrix.docker_platforms }} + # Run the test suites inside the image being built. Nothing of them is kept, and + # the build fails if one does, so no image is published from a tree that fails. + build-args: | + RUN_TESTS=1 file: ./docker/${{ matrix.docker_path }}/Dockerfile tags: introlab3it/rtabmap_ros:${{ matrix.docker_tag }} no-cache: true cache-to: type=inline + # Images that install a released rtabmap_ros from apt (docker/): they hold + # nothing from the tree under review, so building them on a pull request would only + # report that the release still installs. Left to the pushes that publish them. + docker-released: + if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/ros2') }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + docker_tag: [humble, jazzy, kilted, lyrical] + include: + - docker_tag: humble + docker_path: 'humble' + docker_platforms: | + linux/amd64 + - docker_tag: jazzy + docker_path: 'jazzy' + docker_platforms: | + linux/amd64 + linux/arm64 + - docker_tag: kilted + docker_path: 'kilted' + docker_platforms: | + linux/amd64 + linux/arm64 + - docker_tag: lyrical + docker_path: 'lyrical' + docker_platforms: | + linux/amd64 + linux/arm64 + + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: all + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: ${{ matrix.docker_platforms }} + file: ./docker/${{ matrix.docker_path }}/Dockerfile + tags: introlab3it/rtabmap_ros:${{ matrix.docker_tag }} + no-cache: true + cache-to: type=inline diff --git a/.github/workflows/ros2.yml b/.github/workflows/ros2.yml index 0723b173..6e0cf1f5 100644 --- a/.github/workflows/ros2.yml +++ b/.github/workflows/ros2.yml @@ -25,24 +25,26 @@ jobs: include: - ros_distro: humble skip_keys: '' - packages: 'rtabmap_ros rtabmap_conversions' + packages: 'rtabmap_ros rtabmap_conversions rtabmap_odom rtabmap_sync rtabmap_util rtabmap_python' - ros_distro: jazzy skip_keys: '' - packages: 'rtabmap_ros rtabmap_conversions' + packages: 'rtabmap_ros rtabmap_conversions rtabmap_odom rtabmap_sync rtabmap_util rtabmap_python' - ros_distro: kilted skip_keys: 'grid_map_ros' - packages: 'rtabmap_ros rtabmap_conversions' + packages: 'rtabmap_ros rtabmap_conversions rtabmap_odom rtabmap_sync rtabmap_util rtabmap_python' - ros_distro: lyrical - skip_keys: 'nav2_bringup nav2_msgs velodyne grid_map_ros realsense2_camera libpointmatcher' - # rtabmap_costmap_plugins cannot be built, missing nav2 on lyrical, build other packages: - packages: 'rtabmap_launch rtabmap_demos rtabmap_python rtabmap_examples rtabmap_rviz_plugins rtabmap_conversions' + skip_keys: 'velodyne grid_map_ros libpointmatcher' + packages: 'rtabmap_ros rtabmap_conversions rtabmap_odom rtabmap_sync rtabmap_util rtabmap_python' - ros_distro: rolling - # libpointmatcher and gtsam are rtabmap deps not yet available on rolling - # (same skip keys as introlab/rtabmap's cmake-ros workflow) - skip_keys: 'nav2_bringup nav2_msgs velodyne libpointmatcher gtsam' + # Rolling has moved to Ubuntu resolute and much of it has not been rebuilt for + # that yet, so rosdep finds nothing to install for these. They are all optional + # to rtabmap, which just builds without them. + skip_keys: 'velodyne libpointmatcher gtsam grid_map_ros' use_ros2_testing: true # Rolling is using ros2-testing (nightly) - # rtabmap_costmap_plugins cannot be built, missing nav2 on rolling, build other packages: - packages: 'rtabmap_launch rtabmap_demos rtabmap_python rtabmap_examples rtabmap_rviz_plugins rtabmap_conversions' + # nav2 is missing from resolute too, and it is not optional: rtabmap_slam and + # rtabmap_costmap_plugins need it to compile at all, and the rtabmap_ros + # metapackage pulls both in. So rolling names the packages it can build. + packages: 'rtabmap_conversions rtabmap_odom rtabmap_sync rtabmap_util rtabmap_python' fail-fast: false container: image: osrf/ros:${{ matrix.ros_distro }}-desktop-full diff --git a/README.md b/README.md index 51bb9c3c..0ede8901 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ The stack is split into small packages so a pipeline only pulls in what it uses. | Package | Description | |---|---| | `rtabmap_slam` | The `rtabmap` node itself: appearance-based loop closure detection, graph optimization, memory management and map assembly. | -| `rtabmap_odom` | Odometry nodes — `rgbd_odometry`, `stereo_odometry` and `icp_odometry`. Any external odometry can be used instead. | +| [`rtabmap_odom`](rtabmap_odom/README.md) | Odometry nodes — `rgbd_odometry`, `stereo_odometry` and `icp_odometry`. Any external odometry can be used instead. | | [`rtabmap_sync`](rtabmap_sync/README.md) | Synchronizes camera and lidar topics into a single message so they reach the SLAM node together — `rgbd_sync`, `stereo_sync`, `rgbdx_sync`. | ### Sensor processing @@ -100,6 +100,15 @@ sudo apt install ros-$ROS_DISTRO-rtabmap-ros colcon build --symlink-install --cmake-args -DRTABMAP_SYNC_MULTI_RGBD=ON -DRTABMAP_SYNC_USER_DATA=ON -DCMAKE_BUILD_TYPE=Release ``` +### Testing + +```bash +cd ~/ros2_ws +colcon build --base-paths src/rtabmap_ros +colcon test --base-paths src/rtabmap_ros +colcon test-result --verbose +``` + # Usage * For sensor integration examples (stereo and RGB-D cameras, 3D LiDAR), see [rtabmap_examples](https://github.com/introlab/rtabmap_ros/tree/ros2/rtabmap_examples/launch) sub-folder. diff --git a/codecov.yml b/codecov.yml index 2e315d02..fe2e77e4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -56,6 +56,10 @@ component_management: name: rtabmap_sync paths: - rtabmap_sync/** + - component_id: rtabmap_odom + name: rtabmap_odom + paths: + - rtabmap_odom/** - component_id: rtabmap_python name: rtabmap_python paths: @@ -66,8 +70,8 @@ comment: behavior: default require_changes: true # stay quiet when coverage doesn't move -# Only rtabmap_conversions, rtabmap_util, rtabmap_sync and rtabmap_python have tests -# today, so +# Only rtabmap_conversions, rtabmap_util, rtabmap_sync, rtabmap_odom and rtabmap_python +# have tests today, so # everything else would report as 0% and drag the total down to a number that # says nothing. As a package gains tests, drop its line here and add it to # individual_components above. @@ -77,7 +81,6 @@ ignore: - "rtabmap_examples/**" - "rtabmap_launch/**" - "rtabmap_msgs/**" - - "rtabmap_odom/**" - "rtabmap_rviz_plugins/**" - "rtabmap_slam/**" - "rtabmap_viz/**" diff --git a/docker/humble/latest/Dockerfile b/docker/humble/latest/Dockerfile index fcbb8551..cf80889e 100644 --- a/docker/humble/latest/Dockerfile +++ b/docker/humble/latest/Dockerfile @@ -4,14 +4,32 @@ RUN mkdir -p ros2_ws/src COPY . ros2_ws/src/rtabmap_ros +# No rosdep "-r": a bad mirror must fail here. RUN source /ros_entrypoint.sh && \ cd ros2_ws && \ - export MAKEFLAGS="-j2" && \ + echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries && \ rosdep init && \ rosdep update && \ apt-get update && \ - rosdep install --from-paths src --ignore-src -r -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap" && \ - apt-get clean && rm -rf /var/lib/apt/lists/ && \ + rosdep install --from-paths src --ignore-src -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap" && \ + bash src/rtabmap_ros/docker/verify_deps.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +# Tests run in the build layer, before the workspace is deleted, so none of them reach +# the image. CI asks for them; a target that is not the architecture being built on +# skips them, that one being emulated. Only buildx sets BUILDPLATFORM, so anything +# else building with RUN_TESTS=1 runs them. +ARG RUN_TESTS=0 +ARG BUILDPLATFORM +ARG TARGETPLATFORM + +RUN source /ros_entrypoint.sh && \ + cd ros2_ws && \ + export MAKEFLAGS="-j2" && \ colcon build --executor sequential --event-handlers console_direct+ --install-base /opt/ros/humble --merge-install --cmake-args -DRTABMAP_SYNC_MULTI_RGBD=ON -DCMAKE_BUILD_TYPE=Release && \ + if [ "$RUN_TESTS" = "1" ] && [ "${BUILDPLATFORM:-$TARGETPLATFORM}" = "$TARGETPLATFORM" ]; then \ + colcon test --executor sequential --event-handlers console_direct+ --install-base /opt/ros/humble --merge-install && \ + colcon test-result --verbose; \ + fi && \ cd && \ rm -rf ros2_ws diff --git a/docker/iron/latest/Dockerfile b/docker/iron/latest/Dockerfile index 3051956f..87b5919b 100644 --- a/docker/iron/latest/Dockerfile +++ b/docker/iron/latest/Dockerfile @@ -6,15 +6,21 @@ RUN source /ros_entrypoint.sh && \ COPY . ros2_ws/src/rtabmap_ros +# No rosdep "-r": a bad mirror must fail here. RUN source /ros_entrypoint.sh && \ cd ros2_ws && \ - export MAKEFLAGS="-j1" && \ + echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries && \ rosdep init && \ rosdep update && \ apt-get update && \ - rosdep install --from-paths src --ignore-src -r -y -t build_export -t test -t build -t buildtool_export -t buildtool && \ + rosdep install --from-paths src --ignore-src -y -t build_export -t test -t build -t buildtool_export -t buildtool && \ apt remove ros-$ROS_DISTRO-rtabmap* -y && \ - apt-get clean && rm -rf /var/lib/apt/lists/ && \ + bash src/rtabmap_ros/docker/verify_deps.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +RUN source /ros_entrypoint.sh && \ + cd ros2_ws && \ + export MAKEFLAGS="-j1" && \ colcon build --event-handlers console_direct+ --install-base /opt/ros/iron --merge-install --cmake-args -DRTABMAP_SYNC_MULTI_RGBD=ON -DCMAKE_BUILD_TYPE=Release && \ cd && \ rm -rf ros2_ws diff --git a/docker/jazzy/latest/Dockerfile b/docker/jazzy/latest/Dockerfile index 62259cc4..95c67b07 100644 --- a/docker/jazzy/latest/Dockerfile +++ b/docker/jazzy/latest/Dockerfile @@ -6,14 +6,32 @@ RUN source /ros_entrypoint.sh && \ COPY . ros2_ws/src/rtabmap_ros +# No rosdep "-r": a bad mirror must fail here. RUN source /ros_entrypoint.sh && \ cd ros2_ws && \ - export MAKEFLAGS="-j2" && \ + echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries && \ rosdep init && \ rosdep update && \ apt-get update && \ - rosdep install --from-paths src --ignore-src -r -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap grid_map_ros" && \ - apt-get clean && rm -rf /var/lib/apt/lists/ && \ + rosdep install --from-paths src --ignore-src -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap grid_map_ros" && \ + bash src/rtabmap_ros/docker/verify_deps.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +# Tests run in the build layer, before the workspace is deleted, so none of them reach +# the image. CI asks for them; a target that is not the architecture being built on +# skips them, that one being emulated. Only buildx sets BUILDPLATFORM, so anything +# else building with RUN_TESTS=1 runs them. +ARG RUN_TESTS=0 +ARG BUILDPLATFORM +ARG TARGETPLATFORM + +RUN source /ros_entrypoint.sh && \ + cd ros2_ws && \ + export MAKEFLAGS="-j2" && \ colcon build --event-handlers console_direct+ --install-base /opt/ros/$ROS_DISTRO --merge-install --cmake-args -DRTABMAP_SYNC_MULTI_RGBD=ON -DCMAKE_BUILD_TYPE=Release && \ + if [ "$RUN_TESTS" = "1" ] && [ "${BUILDPLATFORM:-$TARGETPLATFORM}" = "$TARGETPLATFORM" ]; then \ + colcon test --executor sequential --event-handlers console_direct+ --install-base /opt/ros/$ROS_DISTRO --merge-install && \ + colcon test-result --verbose; \ + fi && \ cd && \ rm -rf ros2_ws diff --git a/docker/kilted/latest/Dockerfile b/docker/kilted/latest/Dockerfile index 685cbec1..3190fbc1 100644 --- a/docker/kilted/latest/Dockerfile +++ b/docker/kilted/latest/Dockerfile @@ -6,14 +6,32 @@ RUN source /ros_entrypoint.sh && \ COPY . ros2_ws/src/rtabmap_ros +# No rosdep "-r": a bad mirror must fail here. RUN source /ros_entrypoint.sh && \ cd ros2_ws && \ - export MAKEFLAGS="-j2" && \ + echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries && \ rosdep init && \ rosdep update && \ apt-get update && \ - rosdep install --from-paths src --ignore-src -r -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap nav2_bringup realsense2_camera nav2_msgs grid_map_ros" && \ - apt-get clean && rm -rf /var/lib/apt/lists/ && \ + rosdep install --from-paths src --ignore-src -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap nav2_bringup realsense2_camera nav2_msgs grid_map_ros" && \ + bash src/rtabmap_ros/docker/verify_deps.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +# Tests run in the build layer, before the workspace is deleted, so none of them reach +# the image. CI asks for them; a target that is not the architecture being built on +# skips them, that one being emulated. Only buildx sets BUILDPLATFORM, so anything +# else building with RUN_TESTS=1 runs them. +ARG RUN_TESTS=0 +ARG BUILDPLATFORM +ARG TARGETPLATFORM + +RUN source /ros_entrypoint.sh && \ + cd ros2_ws && \ + export MAKEFLAGS="-j2" && \ colcon build --event-handlers console_direct+ --install-base /opt/ros/$ROS_DISTRO --merge-install --cmake-args -DRTABMAP_SYNC_MULTI_RGBD=ON -DCMAKE_BUILD_TYPE=Release && \ + if [ "$RUN_TESTS" = "1" ] && [ "${BUILDPLATFORM:-$TARGETPLATFORM}" = "$TARGETPLATFORM" ]; then \ + colcon test --executor sequential --event-handlers console_direct+ --install-base /opt/ros/$ROS_DISTRO --merge-install && \ + colcon test-result --verbose; \ + fi && \ cd && \ rm -rf ros2_ws diff --git a/docker/lyrical/latest/Dockerfile b/docker/lyrical/latest/Dockerfile index 6517694d..0551179a 100644 --- a/docker/lyrical/latest/Dockerfile +++ b/docker/lyrical/latest/Dockerfile @@ -6,14 +6,32 @@ RUN source /ros_entrypoint.sh && \ COPY . ros2_ws/src/rtabmap_ros +# No rosdep "-r": a bad mirror must fail here. RUN source /ros_entrypoint.sh && \ cd ros2_ws && \ - export MAKEFLAGS="-j2" && \ + echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries && \ rosdep init && \ rosdep update && \ apt-get update && \ - rosdep install --from-paths src --ignore-src -r -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap nav2_bringup realsense2_camera nav2_msgs grid_map_ros nav2_costmap_2d" && \ - apt-get clean && rm -rf /var/lib/apt/lists/ && \ - colcon build --packages-skip rtabmap_costmap_plugins rtabmap_ros --event-handlers console_direct+ --install-base /opt/ros/$ROS_DISTRO --merge-install --cmake-args -DRTABMAP_SYNC_MULTI_RGBD=ON -DCMAKE_BUILD_TYPE=Release && \ + rosdep install --from-paths src --ignore-src -y -t build_export -t test -t build -t buildtool_export -t buildtool --skip-keys="rtabmap nav2_bringup realsense2_camera nav2_msgs grid_map_ros" && \ + bash src/rtabmap_ros/docker/verify_deps.sh && \ + apt-get clean && rm -rf /var/lib/apt/lists/ + +# Tests run in the build layer, before the workspace is deleted, so none of them reach +# the image. CI asks for them; a target that is not the architecture being built on +# skips them, that one being emulated. Only buildx sets BUILDPLATFORM, so anything +# else building with RUN_TESTS=1 runs them. +ARG RUN_TESTS=0 +ARG BUILDPLATFORM +ARG TARGETPLATFORM + +RUN source /ros_entrypoint.sh && \ + cd ros2_ws && \ + export MAKEFLAGS="-j2" && \ + colcon build --event-handlers console_direct+ --install-base /opt/ros/$ROS_DISTRO --merge-install --cmake-args -DRTABMAP_SYNC_MULTI_RGBD=ON -DCMAKE_BUILD_TYPE=Release && \ + if [ "$RUN_TESTS" = "1" ] && [ "${BUILDPLATFORM:-$TARGETPLATFORM}" = "$TARGETPLATFORM" ]; then \ + colcon test --executor sequential --event-handlers console_direct+ --install-base /opt/ros/$ROS_DISTRO --merge-install && \ + colcon test-result --verbose; \ + fi && \ cd && \ rm -rf ros2_ws diff --git a/docker/verify_deps.sh b/docker/verify_deps.sh new file mode 100755 index 00000000..d59648c5 --- /dev/null +++ b/docker/verify_deps.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# Sanity-check the build sysroot right after "rosdep install". +# +# apt/dpkg can be left in a half-applied state, most often on the QEMU-emulated +# arm64 CI leg (ports.ubuntu.com is a single, frequently desynced mirror). CMake +# does not notice, because both PCL and VTK look their files up in ways that +# degrade silently: +# +# * PCLConfig.cmake resolves each component with +# find_library(... HINTS ${PCL_LIBRARY_DIRS} NO_DEFAULT_PATH), so a missing +# libpcl_*.so only yields "Could NOT find PCL_COMMON (missing: +# PCL_COMMON_LIBRARY)" on stderr and configuring still succeeds. +# +# * VTK-targets.cmake creates every VTK::* imported target, then loads their +# IMPORTED_LOCATION from the per-configuration files it picks up with +# file(GLOB VTK-targets-*.cmake). An empty glob is not an error, so the +# targets survive with no location at all and the build only dies at the +# generate step with "IMPORTED_LOCATION not set for imported target +# VTK::CommonCore configuration Release". +# +# Both surface hours into the build, in whichever package first links those +# targets (rtabmap_odom, via pcl_ros). Fail here instead, where the cause is +# still readable. + +set -euo pipefail +shopt -s nullglob + +status=0 + +fail() { + echo "verify_deps: $*" >&2 + status=1 +} + +# PCLConfig.cmake and the libpcl_*.so development symlinks both ship in +# libpcl-dev, so finding the config without them means the package is not +# fully installed. +for config in /usr/lib/*/cmake/pcl/PCLConfig.cmake /usr/lib/cmake/pcl/PCLConfig.cmake; do + # nullglob only drops patterns, not wildcard-free words. + [ -e "${config}" ] || continue + libdir=${config%/cmake/pcl/PCLConfig.cmake} + for component in common io kdtree search surface filters registration \ + sample_consensus segmentation visualization; do + if [ ! -e "${libdir}/libpcl_${component}.so" ]; then + fail "${libdir}/libpcl_${component}.so is missing while ${config} is installed (libpcl-dev is incomplete)" + fi + done +done + +for targets in /usr/lib/*/cmake/vtk-*/VTK-targets.cmake /usr/lib/cmake/vtk-*/VTK-targets.cmake; do + [ -e "${targets}" ] || continue + if ! compgen -G "${targets%.cmake}-*.cmake" > /dev/null; then + fail "no VTK-targets-.cmake next to ${targets}, every VTK::* target would have no IMPORTED_LOCATION (libvtk-dev is incomplete)" + fi +done + +if [ "${status}" -ne 0 ]; then + echo "verify_deps: dependency installation left an inconsistent sysroot, aborting before the build" >&2 + exit 1 +fi + +echo "verify_deps: PCL and VTK sysroot look consistent" diff --git a/rtabmap_conversions/CMakeLists.txt b/rtabmap_conversions/CMakeLists.txt index 7300b3ea..f0db191f 100644 --- a/rtabmap_conversions/CMakeLists.txt +++ b/rtabmap_conversions/CMakeLists.txt @@ -30,7 +30,7 @@ find_package(tf2_eigen REQUIRED) find_package(tf2_geometry_msgs REQUIRED) find_package(tf2_ros REQUIRED) -find_package(RTABMap 0.23.10 REQUIRED) +find_package(RTABMap 0.23.12 REQUIRED) # libraries SET(Libraries diff --git a/rtabmap_conversions/README.md b/rtabmap_conversions/README.md index 91e04544..2909600e 100644 --- a/rtabmap_conversions/README.md +++ b/rtabmap_conversions/README.md @@ -6,6 +6,13 @@ This package is a library only — it contains no nodes, no launch files and no You only need it directly if you are writing your own node against RTAB-Map's C++ API and want to publish or subscribe to `rtabmap_msgs`. +## Contents + +- [Usage](#usage) +- [What it covers](#what-it-covers) +- [Conventions worth knowing](#conventions-worth-knowing) +- [License](#license) + ## Usage Add the dependency to your `package.xml` and `CMakeLists.txt`: @@ -56,14 +63,6 @@ These cut across the whole API and are not obvious from the signatures. Per-func **`CameraInfo` matrices are fixed-size arrays.** `k`, `r` and `p` are `std::array`, so they are never "empty" — an unset matrix is all zeros. `cameraModelFromROS()` treats a zero `k[0]`/`p[0]` (the focal length) as absent. -## Building and testing - -```bash -colcon build --packages-select rtabmap_conversions -colcon test --packages-select rtabmap_conversions -colcon test-result --verbose -``` - ## License BSD-3-Clause. See the [repository root](https://github.com/introlab/rtabmap_ros#license). diff --git a/rtabmap_conversions/include/rtabmap_conversions/PointCloudConversion.h b/rtabmap_conversions/include/rtabmap_conversions/PointCloudConversion.h new file mode 100644 index 00000000..95d898a5 --- /dev/null +++ b/rtabmap_conversions/include/rtabmap_conversions/PointCloudConversion.h @@ -0,0 +1,75 @@ +/* +Copyright (c) 2010-2026, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. (BSD-3-Clause, see the repository root.) +*/ + +#ifndef RTABMAP_CONVERSIONS_POINTCLOUDCONVERSION_H_ +#define RTABMAP_CONVERSIONS_POINTCLOUDCONVERSION_H_ + +#include + +#include +#include + +/** + * @file + * @brief pcl::toROSMsg and pcl::fromROSMsg, minus their empty-cloud crash. + * + * Both take the address of the first point, and of the first byte of the output, before + * checking that there is one (see pcl/conversions.h): for an empty cloud that indexes + * past the end of an empty vector. Nothing notices while the standard library does not + * check, which is why it went unseen for years -- Ubuntu enables those checks from + * resolute on, and then the process aborts outright. + * + * An empty cloud is ordinary here rather than exceptional: a scan whose points were all + * filtered out, a frame with no obstacles in it, an occupancy grid with nothing new. Each + * of those still has to be published, so the conversions are used through this. + */ + +namespace rtabmap_conversions { + +/** + * @brief @p cloud as a PointCloud2 message. + * + * An empty cloud is converted as a single point and emptied afterwards, so the message + * still carries the field layout the installed PCL would have given it. + */ +template +void toPointCloud2Msg( + const pcl::PointCloud & cloud, sensor_msgs::msg::PointCloud2 & msg) +{ + if(!cloud.empty()) + { + pcl::toROSMsg(cloud, msg); + return; + } + + pcl::PointCloud onePoint; + onePoint.header = cloud.header; + onePoint.is_dense = cloud.is_dense; + onePoint.push_back(PointT()); + pcl::toROSMsg(onePoint, msg); + msg.width = 0; + msg.height = 1; + msg.row_step = 0; + msg.data.clear(); +} + +/// @brief @p msg as a point cloud, an empty message included. +template +void fromPointCloud2Msg( + const sensor_msgs::msg::PointCloud2 & msg, pcl::PointCloud & cloud) +{ + if(msg.data.empty()) + { + cloud.clear(); + cloud.is_dense = msg.is_dense; + pcl_conversions::toPCL(msg.header, cloud.header); + return; + } + pcl::fromROSMsg(msg, cloud); +} + +} // namespace rtabmap_conversions + +#endif /* RTABMAP_CONVERSIONS_POINTCLOUDCONVERSION_H_ */ diff --git a/rtabmap_conversions/src/MsgConversion.cpp b/rtabmap_conversions/src/MsgConversion.cpp index 1f2c4685..9e00d0a1 100644 --- a/rtabmap_conversions/src/MsgConversion.cpp +++ b/rtabmap_conversions/src/MsgConversion.cpp @@ -25,6 +25,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include #include "rtabmap_conversions/MsgConversion.h" #include @@ -2808,7 +2809,7 @@ bool convertScanMsg( if(hasIntensity) { pcl::PointCloud::Ptr pclScan(new pcl::PointCloud); - pcl::fromROSMsg(scanOut, *pclScan); + rtabmap_conversions::fromPointCloud2Msg(scanOut, *pclScan); pclScan->is_dense = true; data = rtabmap::util3d::laserScan2dFromPointCloud(*pclScan, laserToOdom).data(); // put back in laser frame format = rtabmap::LaserScan::kXYI; @@ -2816,7 +2817,7 @@ bool convertScanMsg( else { pcl::PointCloud::Ptr pclScan(new pcl::PointCloud); - pcl::fromROSMsg(scanOut, *pclScan); + rtabmap_conversions::fromPointCloud2Msg(scanOut, *pclScan); pclScan->is_dense = true; data = rtabmap::util3d::laserScan2dFromPointCloud(*pclScan, laserToOdom).data(); // put back in laser frame format = rtabmap::LaserScan::kXY; diff --git a/rtabmap_costmap_plugins/CMakeLists.txt b/rtabmap_costmap_plugins/CMakeLists.txt index d592a057..eab748d7 100644 --- a/rtabmap_costmap_plugins/CMakeLists.txt +++ b/rtabmap_costmap_plugins/CMakeLists.txt @@ -45,6 +45,7 @@ IF("$ENV{ROS_DISTRO}" STRLESS "lyrical") IF("$ENV{ROS_DISTRO}" STRLESS "jazzy") target_compile_definitions(rtabmap_costmap_plugins PRIVATE -DPRE_ROS_JAZZY) ENDIF() + target_compile_definitions(rtabmap_costmap_plugins PRIVATE -DPRE_ROS_LYRICAL) ament_target_dependencies(rtabmap_costmap_plugins ${AmentLibraries}) ELSE() target_link_libraries(rtabmap_costmap_plugins PRIVATE ${Libraries}) diff --git a/rtabmap_costmap_plugins/include/rtabmap_costmap_plugins/voxel_layer.hpp b/rtabmap_costmap_plugins/include/rtabmap_costmap_plugins/voxel_layer.hpp index affc632d..5e1115d1 100644 --- a/rtabmap_costmap_plugins/include/rtabmap_costmap_plugins/voxel_layer.hpp +++ b/rtabmap_costmap_plugins/include/rtabmap_costmap_plugins/voxel_layer.hpp @@ -284,6 +284,15 @@ protected: rcl_interfaces::msg::SetParametersResult dynamicParametersCallback(std::vector parameters); + /** + * @brief Declares one of this layer's parameters and returns its value + * @param node the node the layer runs in + * @param name the parameter's name, without the layer prefix + * @param defaultValue the value to use when nothing else sets it + */ + template + T declareOrGetParameter(NodeT & node, const std::string & name, const T & defaultValue); + // Dynamic parameters handler rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr dyn_params_handler_; }; diff --git a/rtabmap_costmap_plugins/src/voxel_layer.cpp b/rtabmap_costmap_plugins/src/voxel_layer.cpp index dcb6ca3f..27a99792 100644 --- a/rtabmap_costmap_plugins/src/voxel_layer.cpp +++ b/rtabmap_costmap_plugins/src/voxel_layer.cpp @@ -58,42 +58,76 @@ using rcl_interfaces::msg::ParameterType; namespace rtabmap_costmap_plugins { +namespace +{ + +/// nav2's Observation::cloud_ used to be a raw pointer and is now the cloud itself, so +/// it is reached through this rather than dereferenced directly. +inline const sensor_msgs::msg::PointCloud2 & cloudOf( + const sensor_msgs::msg::PointCloud2 & cloud) +{ + return cloud; +} + +inline const sensor_msgs::msg::PointCloud2 & cloudOf( + const sensor_msgs::msg::PointCloud2 * cloud) +{ + return *cloud; +} + +/// nav2 hands out observations by value up to kilted and by shared pointer after it. +inline const nav2_costmap_2d::Observation & obsOf( + const nav2_costmap_2d::Observation & observation) +{ + return observation; +} + +inline const nav2_costmap_2d::Observation & obsOf( + const std::shared_ptr & observation) +{ + return *observation; +} + +} // namespace + +/// nav2 declared a layer's parameters through Layer::declareParameter up to kilted and +/// through the node itself after it. +template +T VoxelLayer::declareOrGetParameter( + NodeT & node, const std::string & name, const T & defaultValue) +{ +#ifdef PRE_ROS_LYRICAL + declareParameter(name, rclcpp::ParameterValue(defaultValue)); + T value = defaultValue; + node->get_parameter(name_ + "." + name, value); + return value; +#else + return node->declare_or_get_parameter(name_ + "." + name, defaultValue); +#endif +} + void VoxelLayer::onInitialize() { nav2_costmap_2d::ObstacleLayer::onInitialize(); - declareParameter("enabled", rclcpp::ParameterValue(true)); - declareParameter("footprint_clearing_enabled", rclcpp::ParameterValue(true)); - declareParameter("min_obstacle_height", rclcpp::ParameterValue(0.0)); - declareParameter("max_obstacle_height", rclcpp::ParameterValue(2.0)); - declareParameter("z_voxels", rclcpp::ParameterValue(10)); - declareParameter("origin_z", rclcpp::ParameterValue(0.0)); - declareParameter("z_resolution", rclcpp::ParameterValue(0.2)); - declareParameter("unknown_threshold", rclcpp::ParameterValue(15)); - declareParameter("mark_threshold", rclcpp::ParameterValue(0)); - declareParameter("combination_method", rclcpp::ParameterValue(1)); - declareParameter("publish_voxel_map", rclcpp::ParameterValue(false)); - declareParameter("robot_base_frame", rclcpp::ParameterValue("base_link")); - auto node = node_.lock(); if (!node) { throw std::runtime_error{"Failed to lock node"}; } - node->get_parameter(name_ + "." + "enabled", enabled_); - node->get_parameter(name_ + "." + "footprint_clearing_enabled", footprint_clearing_enabled_); - node->get_parameter(name_ + "." + "min_obstacle_height", min_obstacle_height_); - node->get_parameter(name_ + "." + "max_obstacle_height", max_obstacle_height_); - node->get_parameter(name_ + "." + "z_voxels", size_z_); - node->get_parameter(name_ + "." + "origin_z", origin_z_); - node->get_parameter(name_ + "." + "z_resolution", z_resolution_); - node->get_parameter(name_ + "." + "unknown_threshold", unknown_threshold_); - node->get_parameter(name_ + "." + "mark_threshold", mark_threshold_); - node->get_parameter(name_ + "." + "publish_voxel_map", publish_voxel_); - node->get_parameter(name_ + "." + "robot_base_frame", robot_base_frame_); + enabled_ = declareOrGetParameter(node, "enabled", true); + footprint_clearing_enabled_ = declareOrGetParameter(node, "footprint_clearing_enabled", true); + min_obstacle_height_ = declareOrGetParameter(node, "min_obstacle_height", 0.0); + max_obstacle_height_ = declareOrGetParameter(node, "max_obstacle_height", 2.0); + size_z_ = declareOrGetParameter(node, "z_voxels", 10); + origin_z_ = declareOrGetParameter(node, "origin_z", 0.0); + z_resolution_ = declareOrGetParameter(node, "z_resolution", 0.2); + unknown_threshold_ = declareOrGetParameter(node, "unknown_threshold", 15); + mark_threshold_ = declareOrGetParameter(node, "mark_threshold", 0); + publish_voxel_ = declareOrGetParameter(node, "publish_voxel_map", false); + robot_base_frame_ = declareOrGetParameter(node, "robot_base_frame", std::string("base_link")); - int combination_method_param{}; - node->get_parameter(name_ + "." + "combination_method", combination_method_param); + const int combination_method_param = declareOrGetParameter(node, "combination_method", 1); #ifdef PRE_ROS_JAZZY combination_method_ = combination_method_param; #else @@ -169,7 +203,11 @@ void VoxelLayer::updateBounds( useExtraBounds(min_x, min_y, max_x, max_y); bool current = true; +#ifdef PRE_ROS_LYRICAL std::vector observations, clearing_observations; +#else + std::vector observations, clearing_observations; +#endif // get the marking observations current = getMarkingObservations(observations) && current; @@ -182,16 +220,15 @@ void VoxelLayer::updateBounds( // raytrace freespace for (unsigned int i = 0; i < clearing_observations.size(); ++i) { - raytraceFreespace(clearing_observations[i], min_x, min_y, max_x, max_y); + raytraceFreespace(obsOf(clearing_observations[i]), min_x, min_y, max_x, max_y); } // place the new obstacles into a priority queue... each with a priority of zero to begin with - for (std::vector::const_iterator it = observations.begin(); it != observations.end(); - ++it) + for (auto it = observations.begin(); it != observations.end(); ++it) { - const nav2_costmap_2d::Observation & obs = *it; + const nav2_costmap_2d::Observation & obs = obsOf(*it); - const sensor_msgs::msg::PointCloud2 & cloud = *(obs.cloud_); + const sensor_msgs::msg::PointCloud2 & cloud = cloudOf(obs.cloud_); double sq_obstacle_max_range = obs.obstacle_max_range_ * obs.obstacle_max_range_; double sq_obstacle_min_range = obs.obstacle_min_range_ * obs.obstacle_min_range_; @@ -277,7 +314,9 @@ void VoxelLayer::raytraceFreespace( { auto clearing_endpoints_ = std::make_unique(); - if (clearing_observation.cloud_->height == 0 || clearing_observation.cloud_->width == 0) { + const sensor_msgs::msg::PointCloud2 & clearing_cloud = cloudOf(clearing_observation.cloud_); + + if (clearing_cloud.height == 0 || clearing_cloud.width == 0) { return; } @@ -311,8 +350,8 @@ void VoxelLayer::raytraceFreespace( } clearing_endpoints_->data.clear(); - clearing_endpoints_->width = clearing_observation.cloud_->width; - clearing_endpoints_->height = clearing_observation.cloud_->height; + clearing_endpoints_->width = clearing_cloud.width; + clearing_endpoints_->height = clearing_cloud.height; clearing_endpoints_->is_dense = true; clearing_endpoints_->is_bigendian = false; @@ -331,9 +370,9 @@ void VoxelLayer::raytraceFreespace( double map_end_y = origin_y_ + getSizeInMetersY(); double map_end_z = origin_z_ + getSizeInMetersZ(); - sensor_msgs::PointCloud2ConstIterator iter_x(*(clearing_observation.cloud_), "x"); - sensor_msgs::PointCloud2ConstIterator iter_y(*(clearing_observation.cloud_), "y"); - sensor_msgs::PointCloud2ConstIterator iter_z(*(clearing_observation.cloud_), "z"); + sensor_msgs::PointCloud2ConstIterator iter_x(clearing_cloud, "x"); + sensor_msgs::PointCloud2ConstIterator iter_y(clearing_cloud, "y"); + sensor_msgs::PointCloud2ConstIterator iter_z(clearing_cloud, "z"); for (; iter_x != iter_x.end(); ++iter_x, ++iter_y, ++iter_z) { double wpx = *iter_x; @@ -431,7 +470,7 @@ void VoxelLayer::raytraceFreespace( if (publish_clearing_points) { clearing_endpoints_->header.frame_id = global_frame_; - clearing_endpoints_->header.stamp = clearing_observation.cloud_->header.stamp; + clearing_endpoints_->header.stamp = clearing_cloud.header.stamp; clearing_endpoints_pub_->publish(std::move(clearing_endpoints_)); } diff --git a/rtabmap_odom/CMakeLists.txt b/rtabmap_odom/CMakeLists.txt index 7b10ad25..4a1a72e6 100644 --- a/rtabmap_odom/CMakeLists.txt +++ b/rtabmap_odom/CMakeLists.txt @@ -157,4 +157,52 @@ install(DIRECTORY include/ FILES_MATCHING PATTERN "*.h" ) +############# +## Testing ## +############# +if(BUILD_TESTING) + find_package(ament_cmake_gtest REQUIRED) + find_package(OpenCV REQUIRED COMPONENTS core imgcodecs) + # Recorded sensor input is replayed by the tests themselves, not by "ros2 bag play". + find_package(rosbag2_cpp REQUIRED) + + # Real frames the visual odometry tests register against, read from the source tree: + # these binaries are never installed, and the fixtures are not either. See + # test/data/README.md for where they come from. + set(rtabmap_odom_test_data_root "${CMAKE_CURRENT_SOURCE_DIR}/test/data") + + # Each node gets its own test binary: a crash or a stuck executor in one node cannot + # take the others down, and every binary starts with a clean DDS graph. + # + # Each binary also gets its own DDS domain. colcon tests packages in parallel and ctest + # can run these binaries in parallel, while these suites share topic names -- odom, + # rgbd_image, scan_cloud -- with rtabmap_sync's and rtabmap_util's. On a shared domain + # they discover each other's publishers and assertions then see traffic the test never + # sent. rtabmap_util numbers from 30 and rtabmap_sync from 50; keep the ranges apart. + set(rtabmap_odom_test_domain_id 70) + macro(rtabmap_odom_add_node_test test_name) + ament_add_gtest(${test_name} test/${test_name}.cpp + ENV ROS_DOMAIN_ID=${rtabmap_odom_test_domain_id} + TIMEOUT 300) + math(EXPR rtabmap_odom_test_domain_id "${rtabmap_odom_test_domain_id} + 1") + if(TARGET ${test_name}) + target_include_directories(${test_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/test) + target_compile_definitions(${test_name} PRIVATE + RTABMAP_ODOM_TEST_DATA_ROOT="${rtabmap_odom_test_data_root}") + target_link_libraries(${test_name} rtabmap_odom_plugins rtabmap_odom + opencv_core opencv_imgcodecs rosbag2_cpp::rosbag2_cpp rtabmap::core) + if("$ENV{ROS_DISTRO}" STRLESS "lyrical") + ament_target_dependencies(${test_name} ${AmentLibraries}) + else() + target_link_libraries(${test_name} ${Libraries} ${PublicLibraries}) + endif() + endif() + endmacro() + + rtabmap_odom_add_node_test(test_odometry_ros) + rtabmap_odom_add_node_test(test_rgbd_odometry) + rtabmap_odom_add_node_test(test_stereo_odometry) + rtabmap_odom_add_node_test(test_icp_odometry) +endif() + ament_package() diff --git a/rtabmap_odom/README.md b/rtabmap_odom/README.md new file mode 100644 index 00000000..9c77e6cf --- /dev/null +++ b/rtabmap_odom/README.md @@ -0,0 +1,273 @@ +# rtabmap_odom + +Odometry for [RTAB-Map](https://github.com/introlab/rtabmap): where the robot is relative to a local fixed frame, estimated from its own sensors — a pose that moves continuously and never jumps, but drifts over time. + +SLAM needs a pose for every measurement it maps. These nodes produce one by registering each new frame against the last — visually from an RGB-D or stereo camera, or geometrically from a lidar — and integrating the result into a [`nav_msgs/msg/Odometry`](https://docs.ros.org/en/jazzy/p/nav_msgs/msg/Odometry.html) and a TF. + +## Contents + +- [Nodes](#nodes) + - [Choosing a sensor modality for the environment](#choosing-a-sensor-modality-for-the-environment) +- [Library](#library) +- [Conventions](#conventions) + - [Frames and TF](#frames-and-tf) + - [RTAB-Map's own parameters](#rtab-maps-own-parameters) + - [Feeding in an external guess](#feeding-in-an-external-guess) + - [IMU](#imu) + - [Update rates and dropped frames](#update-rates-and-dropped-frames) + - [Lost frames, resets and new maps](#lost-frames-resets-and-new-maps) +- [Services](#services) +- [Published topics](#published-topics) + - [Outputting filtered scans and features](#outputting-filtered-scans-and-features) +- [Diagnostics](#diagnostics) +- [License](#license) + +## Nodes + +| Node | Description | +|---|---| +| [rgbd_odometry](doc/rgbd_odometry.md) | Visual odometry from a color image and a depth image registered to it. | +| [stereo_odometry](doc/stereo_odometry.md) | Visual odometry from a stereo pair. | +| [icp_odometry](doc/icp_odometry.md) | Geometric odometry from a 2D or 3D lidar. | + +Every node is a [composable node](https://docs.ros.org/en/jazzy/Tutorials/Intermediate/Composition.html) as well as a standalone executable. + +### Choosing a sensor modality for the environment + +Which to use is a question about the **environment**, not about which sensor is better. A camera tracks visual texture; a lidar tracks geometry. Each fails where its own cue is missing, and the two failures do not overlap much. + +| Environment | Use | Why | +|---|---|---| +| Visually textured and well lit — offices, cluttered rooms, daylight outdoors | **Camera** | Plenty of features to match, and appearance gives loop closure for free. | +| Textureless but geometrically rich — bare corridors with doorways and furniture, warehouse aisles | **Lidar** | Blank walls give a camera nothing; the shape of the space still constrains ICP. | +| Dark, or lighting that changes abruptly | **Lidar** | A camera is simply blind. Lidar does not care. | +| Geometrically plain but visually rich — a large open hall with a patterned floor, textured flat walls | **Camera** | [Degenerate geometry](doc/icp_odometry.md#degenerate-geometry) defeats ICP here, while the texture is exactly what a camera needs. | +| Both plain and textureless — an empty warehouse, a long featureless tunnel | **Wheel odometry**, with either as a corrector | Neither cue is present. This is the case where wheel odometry carries the pose. | +| Repetitive and self-similar — tiled floors, rows of racking, a long colonnade | **Wheel odometry as the guess**, with either on top | Both cues are present but *ambiguous*: a camera matches the wrong copy of a feature, a lidar the wrong bay of shelving. See [Repetitive patterns](doc/rgbd_odometry.md#repetitive-patterns). | +| Outdoors at range | **Stereo camera or 3D lidar** | RGB-D depth stops working outdoors; both of these keep going. | + +**Do not underestimate wheel odometry.** On a wheeled robot it is locally excellent and only drifts over distance — the opposite failure from both of the above, which are locally noisy but not systematically biased. It is also the only one of the three that keeps working when the environment offers no cue at all — and, because it is indifferent to what the scene *looks* like, the only one that is not fooled when the scene repeats itself. + +**Fuse the wheels with an IMU before feeding them in.** [`robot_localization`](https://docs.ros.org/en/jazzy/p/robot_localization/) is the standard way: its EKF combines wheel odometry with IMU orientation and angular rates into one filtered `odom` topic, which is a markedly better guess than the wheels alone. [FusionCore](https://github.com/manankharwar/fusioncore) is another EKF that does the same job. The IMU fixes exactly what encoders are worst at — yaw through a turn, and wheel slip, which encoders report as motion that never happened. Where the camera or lidar fails outright, that filtered estimate is what carries the robot through, and a pipeline built this way degrades instead of breaking. + +Which is why the robust arrangement is rarely one of them alone: feed wheel odometry in as `guess_frame_id` and the registration starts near the answer every frame. That covers the camera's fast-motion and blank-wall failures and the lidar's degenerate-corridor failure, while the camera or lidar in turn corrects the wheels' drift. See [Feeding in an external guess](#feeding-in-an-external-guess). + +**For 2D indoor odometry a lidar usually costs less computation.** Registering a few hundred scan points is far less CPU than detecting, describing and matching visual features on every frame, and it needs no GPU — which is what decides whether odometry keeps up on the small onboard computers these robots carry. + +With both a camera and a lidar, the usual arrangement is `icp_odometry` for the pose and the camera for appearance — see [Combining a camera and a lidar](doc/icp_odometry.md#combining-a-camera-and-a-lidar). + +## Library + +The package installs a C++ library, documented in the [C++ API reference](https://docs.ros.org/en/jazzy/p/rtabmap_odom/generated/index.html) generated from the headers. + +**`OdometryROS`** is the base class all three nodes derive from, and it is where most of this package's behaviour actually lives. It owns the RTAB-Map `Odometry` object, the pose integration, the TF broadcast, the IMU intake, the services and the diagnostics. Each node subclasses it to do one thing: turn its own topics into a `rtabmap::SensorData` and hand it over. That is why the three nodes share nearly all of their parameters and publish exactly the same topics. + +It also runs the registration on **its own thread**. A frame arriving while the previous one is still being processed does not block the subscription callback; see [Update rates and dropped frames](#update-rates-and-dropped-frames). + +## Conventions + +These apply to all three nodes. + +### Frames and TF + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `frame_id` | `string` | `"base_link"` | The robot frame being tracked. The pose published is this frame's, not the sensor's — the sensor-to-robot transform is read from TF. | +| `odom_frame_id` | `string` | `"odom"` | The fixed frame the pose is expressed in. | +| `publish_tf` | `bool` | `true` | Broadcast the pose on TF. **Turn this off if something else already publishes that transform**, or the two fight and TF alternates between them. What exactly is broadcast depends on `guess_frame_id`: without it, `odom_frame_id` → `frame_id`; with it, a correction `odom_frame_id` → `guess_frame_id` ([why](#it-also-keeps-tf-alive-through-a-failure)). | +| `wait_for_transform` | `double` | `0.1` | Seconds to wait for a needed transform before giving up on the frame. | +| `initial_pose` | `string` | `""` | Starting pose, `"x y z roll pitch yaw"`. Also settable at runtime through `reset_odom_to_pose`. | +| `ground_truth_frame_id` | `string` | `""` | When set, the pose is taken from this TF instead of being computed — for replaying a dataset with a known trajectory. | +| `ground_truth_base_frame_id` | `string` | value of `frame_id` | The robot frame within the ground truth TF tree. | +| `guess_frame_id` | `string` | `""` | A frame carrying another odometry source, used as the initial guess for each registration. Documented with its companions under [Feeding in an external guess](#feeding-in-an-external-guess) -- **the highest-value parameter here for a wheeled robot**. | + +The sensor must be connected to `frame_id` in TF **before the first frame arrives**, or that frame is dropped with a warning. A static publisher is the usual answer. + +### RTAB-Map's own parameters + +Everything in RTAB-Map's odometry parameter set is exposed as a ROS parameter **under its RTAB-Map name**, so tuning is done directly: + +```bash +ros2 run rtabmap_odom rgbd_odometry --ros-args \ + -p "Odom/Strategy:='1'" \ + -p "Vis/MinInliers:='15'" \ + -p "Odom/ResetCountdown:='1'" +``` + +**Note the quoting.** Every RTAB-Map parameter is declared as a **string**, whatever it looks like, because that is how RTAB-Map's own parameter map stores them. Writing `-p Odom/Strategy:=1` makes ROS infer an integer, and the node throws on startup rather than starting with the wrong value: + +``` +parameter 'Odom/Strategy' has invalid type: Wrong parameter type, +parameter {Odom/Strategy} is of type {string}, setting it to {integer} is not allowed. +``` + +The inner quotes are what keeps it a string. Shell quotes alone do not help, since the value is parsed as YAML after the shell is done with it. In a launch file the same rule reads naturally: `{'Odom/Strategy': '1'}`. + +This applies **only** to RTAB-Map's own parameters. The nodes' ROS parameters -- `frame_id`, `publish_tf`, `scan_voxel_size`, `approx_sync` -- are declared with their real types and take plain values. + +Which parameters exist depends on the node: each declares the set matching its sensor, so `Vis/*` appears on the visual nodes and `Icp/*` only on `icp_odometry`. `ros2 param list` on a running node is the authoritative list; the meaning of each is in [RTAB-Map's parameter reference](https://github.com/introlab/rtabmap/blob/master/corelib/include/rtabmap/core/Parameters.h). + +The two worth knowing before anything else: + +- **`Odom/Strategy`** selects the registration algorithm — `0` frame-to-map (default, more accurate), `1` frame-to-frame (cheaper), and others for the external VO libraries RTAB-Map can be built against. +- **`Odom/ResetCountdown`** automatically resets odometry after this many consecutive lost frames instead of staying lost forever. `0` disables it, which is the default. + +`config_path` loads the same parameters from an INI file; only the odometry ones are taken from it. + +### Feeding in an external guess + +Registration works far better when it starts near the answer. Two ways to supply one: + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `guess_frame_id` | `string` | `""` | A TF frame carrying another odometry source — wheels, IMU-integrated, a base driver. Its motion between frames becomes the initial guess. | +| `guess_min_translation` | `double` | `0.0` | Skip frames whose guessed motion is below this, in meters. `0` disables. | +| `guess_min_rotation` | `double` | `0.0` | Same, in radians. | +| `guess_min_time` | `double` | `0.0` | Same, in seconds. | +| `guess_linear_variance` | `double` | `0.001` | Covariance of the published pose when the guess is used directly. | +| `guess_angular_variance` | `double` | `0.001` | Same, rotational. | + +**`guess_frame_id` is the single biggest improvement available to a wheeled robot.** Wheel odometry is locally excellent and globally hopeless; visual and ICP registration is the reverse. Giving the registration a wheel-odometry guess makes it converge more often, faster, and survive the frames where the camera sees nothing. + +The `guess_min_*` parameters additionally suppress processing while the robot is stationary, which stops a static scene from accumulating drift and saves the CPU. + +#### It also keeps TF alive through a failure + +Setting `guess_frame_id` also changes *how* the pose is broadcast. This is worth understanding before odometry fails on a real robot, because it decides what the rest of the system sees while it is lost. + +With a guess frame configured, the node no longer publishes `odom_frame_id` → `frame_id` directly. It publishes a **correction** instead, `odom_frame_id` → `guess_frame_id`. + +So the guess source keeps the conventional `odom` frame -- whatever produces it, the robot's driver or [`robot_localization`](https://docs.ros.org/en/jazzy/p/robot_localization/) -- and **this node takes a different name for its own `odom_frame_id`**. The examples in this repository use `vo` for the visual nodes and `icp_odom` for the lidar one: + +```mermaid +flowchart TD + ODOM(["/vo
odom_frame_id"]) + GUESS(["/odom
guess_frame_id"]) + BASE(["/base_link
frame_id"]) + SENSOR(["/camera or /lidar
the sensor's header.frame_id"]) + ODOM -->|correction, this node
e.g. ~10 Hz, ~50 ms delay| GUESS + GUESS -->|robot driver or robot_localization
e.g. ~50 Hz, ~1 ms delay| BASE + BASE -->|static| SENSOR +``` + +*The rates and delays above are examples only — yours depend on the sensor, the base driver and the computer.* + +**That chain keeps being published while registration is lost.** The correction freezes at the last successfully computed pose composed with the motion the guess has accumulated since, so `base_link` keeps moving in TF at the guess source's rate, driven entirely by the guess. Nothing downstream stalls or jumps; the pose just accumulates that source's drift until registration recovers. Without `guess_frame_id` there is no correction to publish and **no TF at all is broadcast while lost**, which is what breaks the tree. + +Pair it with `Odom/ResetCountdown` and the recovery is complete. Take a robot turning to face a white wall: visual odometry loses tracking, TF keeps flowing from the wheels, and after the configured number of failed frames the odometry resets — not to where it was when it got lost, but to `last computed pose × guess motion`, which is where the wheels say the robot has got to in the meantime. Registration restarts from there and the trajectory carries on with only the drift the wheels accumulated. + +What it resets to depends on what is available, in this order: + +1. **A guess** — resets to the last pose composed with the guess motion, as above. +2. **No guess, but `odom_frame_id` → `frame_id` exists in TF at the sensor frame's stamp** — resets to that pose. This is the `publish_tf:=false` arrangement: this node publishes only its odometry topic, [`robot_localization`](https://docs.ros.org/en/jazzy/p/robot_localization/) fuses that topic with the wheels and the IMU, and the filter owns the transform. The reset therefore lands on the filter's current estimate — the odometry gets restarted from where the fused solution says the robot is, having contributed to that solution itself while it was working. `publish_tf` has to be off for this to mean anything, and **`publish_null_when_lost` should be off too**: the null pose is a signal for consumers that read it as one, and a filter fusing this topic is not — it would be handed an invalid pose to fuse. With it off the node simply stops publishing while lost, and the filter carries on from its other inputs until registration recovers. +3. **Neither** — resets to the last computed pose, so the robot resumes believing it never moved while lost. + +After an automatic reset the countdown is left armed, so if odometry still cannot initialize on the next frames it keeps re-resetting to the latest guess rather than getting stuck. + +### IMU + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `wait_imu_to_init` | `bool` | `false` | Hold off until an IMU message has arrived, so gravity is known from the first frame. | +| `imu_queue_size` | `int` | `200` | Depth of the IMU buffer. IMUs run far faster than cameras; this is why it is large. | +| `qos_imu` | `int` | value of `qos` | Reliability of the `imu` subscription. | +| `always_check_imu_tf` | `bool` | `false` | Re-read the IMU-to-robot transform every message rather than caching it. | + +The `imu` topic is optional on all three nodes. Supplying it lets odometry know which way is down, which constrains roll and pitch — worth doing on any robot that has an IMU, and close to mandatory for a handheld or aerial one. + +**With no `guess_frame_id`, the IMU also supplies the rotation half of each frame's guess.** The rotation measured between the previous frame and this one becomes the guess's orientation, leaving the translation to the motion model. That is often the difference between tracking a fast turn and losing it, since rotation is what breaks feature matching first. An external guess takes precedence when there is one: `guess_frame_id` is used whole, and the IMU is not consulted for the guess at all. + +### Update rates and dropped frames + +Registration runs on its own thread, so a slow frame does not block the subscription. What happens to the frames arriving meanwhile is a choice: + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `always_process_most_recent_frame` | `bool` | `true` | Drop frames that arrive while registration is still running and take the newest. `false` registers every frame in order, on the subscription thread. | +| `expected_update_rate` | `double` | `0.0` | The rate frames are expected at, in Hz. Used only when `max_update_rate` is unset, and it is a **ceiling**: a frame arriving sooner than `1/expected_update_rate` after the last one is skipped, with a warning that the input is faster than expected. `0` disables. | +| `max_update_rate` | `double` | `0.0` | Throttle registration to at most this rate, skipping frames silently. Takes precedence over `expected_update_rate`. `0` disables. | +| `min_update_rate` | `double` | `0.0` | Treat odometry as **lost and reset it** when more than `1/min_update_rate` passes between updates — the motion assumption no longer holds across a gap that long. `0` disables. | + +**`always_process_most_recent_frame` already bounds the delay.** A frame arriving while registration is still running is dropped on the spot rather than queued, so the worker always picks up the newest frame and the published pose is at most one registration behind the sensor. No backlog ever forms. `/diagnostics` reports how many frames went this way. + +That is why **`max_update_rate` is about CPU, not latency**: given the skipping above, the worst-case delay is roughly the same whether it is set or not. What it changes is how many frames get registered at all. Set it to give the rest of the robot its cores back — not to make the pose fresher, which it will not do. + +Setting `always_process_most_recent_frame:=false` is the opposite trade: every frame is registered, in order, on the subscription thread. That is what you want when replaying a bag, where dropping frames loses data you meant to process. + +### Lost frames, resets and new maps + +A frame that cannot be registered is *lost*: the node publishes an all-zero pose with `9999` down the diagonal of both covariance matrices, which says there is no pose here to use. + +The first frame after a reset — from `reset_odom`, `reset_odom_to_pose` or `Odom/ResetCountdown` — carries the same `9999` for a different reason. It is an *initialization* rather than a registration: nothing to measure against, no velocity to carry over. Its pose is real and meant to be used; what the covariance says is that it does not continue the last valid one. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `publish_null_when_lost` | `bool` | `true` | Publish a null pose, with `9999` on the covariance diagonals, when a frame cannot be registered. `false` publishes nothing. | + +**Leave it on, unless a filter is consuming this topic.** A consumer that sees the null message knows odometry is lost; one that sees nothing cannot tell that apart from a node that died or a topic that was never connected. `rtabmap` relies on it to know the frame should not be mapped. + +`rtabmap` reads an identity pose, or both covariances at `9999`, as a discontinuity, and starts a **new map** rather than deforming the graph across a jump the robot never made: + +``` +Odometry is reset (identity pose or high variance detected). Increment map id! +``` + +While it is lost, `publish_null_when_lost:=true` publishes a null pose and no velocity for every frame, both marked `9999`. With `:=false` it publishes nothing — except that a guess frame keeps it going: every frame that re-initialises the map, which `Odom/ResetCountdown` makes frequent, is published with the guess's pose and confidence, so the topic has no gap for as long as the guess is there. What differs between configurations is the first frame after the reset, and where it restarts from: + +| | First frame after the reset | Second frame | TF while lost | `rtabmap` | +|---|---|---|---|---| +| `publish_null_when_lost:=true` (default), with or without a guess | recovered pose, `9999` on both pose and velocity | registered, from the recovered pose | unbroken with a guess, absent without one | new map | +| `publish_null_when_lost:=false` with `guess_frame_id` | recovered pose and the guess's velocity, both with the guess's covariance | registered, from the recovered pose | unbroken | one session | +| `publish_null_when_lost:=false`, `publish_tf:=false`, another node publishing `odom` → `base_link` | not published | registered, from the recovered pose | unbroken, published by the other node | one session | +| `publish_null_when_lost:=false` with neither | not published | registered, from the pose held before the loss | absent until it recovers | one session, across the gap | + +*Registered* is the ordinary case: a pose and a velocity measured against the previous valid frame, with the covariance the registration computed. + +The two middle rows are the ones to build on. Either an external source is named through `guess_frame_id`, and the restarting frame is published as a continuation of the trajectory — the poses *and* the covariances staying continuous for as long as that guess is published — or a filter such as `robot_localization` owns `odom` → `base_link`, and the reset adopts whatever pose it holds. + +**The last row is a trap.** With nothing to say where the robot went while the odometry was lost, the reset resumes at the pose from before it, that motion is dropped from the trajectory, and since no `9999` ever reaches `rtabmap` the map is deformed across the gap rather than split at it. The node reports that combination as an error at startup. + +For finer control, put an intermediate node between this one and `rtabmap` and let it set the covariances itself. It decides what counts as a discontinuity, instead of that being inferred from the reset alone — starting a new map when the guess frame has gone quiet, say, and the newly computed pose may be wrong even though registration reported success. + +Recovering from lost is what `Odom/ResetCountdown` is for, or the `reset_odom` service. Combined with `guess_frame_id` it also keeps the TF tree intact throughout — see [It also keeps TF alive through a failure](#it-also-keeps-tf-alive-through-a-failure). + +## Services + +| Service | Type | Description | +|---|---|---| +| `reset_odom` | [`std_srvs/srv/Empty`](https://docs.ros.org/en/jazzy/p/std_srvs/srv/Empty.html) | Drop the internal local map and restart the pose — at the identity, or, with `guess_frame_id` configured, at whatever pose the guess frame currently holds, so that odometry restarts where the other source says the robot is. | +| `reset_odom_to_pose` | [`rtabmap_msgs/srv/ResetPose`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/srv/ResetPose.html) | Reset to a given `x y z roll pitch yaw`. | +| `pause_odom` | [`std_srvs/srv/Empty`](https://docs.ros.org/en/jazzy/p/std_srvs/srv/Empty.html) | Stop processing incoming frames. | +| `resume_odom` | [`std_srvs/srv/Empty`](https://docs.ros.org/en/jazzy/p/std_srvs/srv/Empty.html) | Resume. | +| `log_debug`, `log_info`, `log_warning`, `log_error` | [`std_srvs/srv/Empty`](https://docs.ros.org/en/jazzy/p/std_srvs/srv/Empty.html) | Change RTAB-Map's own log level at runtime. | + +## Published topics + +Common to all three nodes. **Every one of them, `odom` included, is published only when something is subscribed** -- the work of building each message is skipped otherwise. The TF broadcast is not gated this way and happens whenever `publish_tf` is on -- except while registration is lost with no guess frame configured, when there is nothing to broadcast. + +| Topic | Type | Description | +|---|---|---| +| `odom` | [`nav_msgs/msg/Odometry`](https://docs.ros.org/en/jazzy/p/nav_msgs/msg/Odometry.html) | The pose and velocity. Covariance is meaningful: it grows with registration uncertainty, and is `9999` on the diagonal when lost. | +| `odom_info` | [`rtabmap_msgs/msg/OdomInfo`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/OdomInfo.html) | Everything about how the frame was registered — inlier count, matches, features, timings. The first thing to look at when odometry misbehaves. | +| `odom_info_lite` | [`rtabmap_msgs/msg/OdomInfo`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/OdomInfo.html) | The same without the per-feature arrays, for logging or a slow link. | +| `odom_local_map` | [`sensor_msgs/msg/PointCloud2`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/PointCloud2.html) | The feature map the current frame was registered against. Visual paths only — built from the frame's visual words, so `icp_odometry` never fills it. | +| `odom_local_scan_map` | [`sensor_msgs/msg/PointCloud2`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/PointCloud2.html) | The scan map, the ICP path's equivalent of `odom_local_map`. | +| `odom_last_frame` | [`sensor_msgs/msg/PointCloud2`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/PointCloud2.html) | The current frame's **features**, in the odom frame — not its scan or its pixels. Visual paths only, for the same reason as `odom_local_map`; for the filtered scan see `odom_sensor_data/*`. | +| `odom_rgbd_image` | [`rtabmap_msgs/msg/RGBDImage`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/RGBDImage.html) | The frame **as odometry processed it**, not the input as it arrived. See [Outputting filtered scans and features](#outputting-filtered-scans-and-features). | +| `odom_sensor_data/raw`, `/features`, `/compressed` | [`rtabmap_msgs/msg/SensorData`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/SensorData.html) | The same frame as `SensorData`. `/features` strips the images and scan and keeps only the extracted features; `/compressed` carries JPEG/PNG images instead of raw. | + +### Outputting filtered scans and features + +`odom_rgbd_image` and `odom_sensor_data/*` republish the frame **after** odometry has worked on it, which is the point of them — they are what odometry actually registered, not a copy of the input: + +- **Features are included.** Registration writes the keypoints, their 3D positions and their descriptors back into the frame, so these topics carry them. `odom_sensor_data/features` is that alone, with the images and scan removed. +- **The scan is the filtered one.** `icp_odometry` builds the frame after deskewing, voxelization, range filtering and normal estimation, so what comes out here is the decimated cloud ICP saw — not the raw sweep the lidar published. Subscribe to the driver's topic if you want the original. +- **Images are converted.** `rgbd_odometry` hands over grayscale unless `keep_color` is set, so that is what these carry too. + +## Diagnostics + +All three publish to `/diagnostics`: the input rate, the output rate, and how many frames were processed versus dropped. A healthy input rate with a low output rate means frames are arriving but not registering — check `odom_info` before touching anything else. + +## License + +BSD-3-Clause. See the [repository root](https://github.com/introlab/rtabmap_ros#license). diff --git a/rtabmap_odom/doc/icp_odometry.md b/rtabmap_odom/doc/icp_odometry.md new file mode 100644 index 00000000..1877614d --- /dev/null +++ b/rtabmap_odom/doc/icp_odometry.md @@ -0,0 +1,238 @@ +# icp_odometry + +Odometry from a 2D or 3D lidar, by registering each scan against the previous one with ICP. + +No features and no appearance: the motion is whatever transform best aligns this scan's points with the last. That makes it indifferent to lighting and texture — it works in the dark, and on the blank white corridor where [rgbd_odometry](rgbd_odometry.md) has nothing to track. + +What it is sensitive to instead is **geometry**. ICP can only recover motion that the scene's shape constrains, and a scene can fail to constrain it: see [Degenerate geometry](#degenerate-geometry), which is the failure mode worth understanding before deploying this. + +The shared parameters — frames, TF, guesses, the IMU, RTAB-Map's own, the services — are in the [package README](../README.md#conventions). This page covers what is specific to this node. + +## Contents + +- [Usage](#usage) +- [Subscribed Topics](#subscribed-topics) +- [Published Topics](#published-topics) + - [Reusing the filtered scan downstream](#reusing-the-filtered-scan-downstream) +- [Parameters](#parameters) + - [Where these defaults come from](#where-these-defaults-come-from) + - [Making the correspondence ratio mean something](#making-the-correspondence-ratio-mean-something) +- [Preparing the scan](#preparing-the-scan) +- [Deskewing](#deskewing) +- [Degenerate geometry](#degenerate-geometry) +- [Combining a camera and a lidar](#combining-a-camera-and-a-lidar) +- [When it loses track](#when-it-loses-track) + +## Usage + +2D lidar: + +```bash +ros2 run rtabmap_odom icp_odometry --ros-args \ + -r scan:=/scan \ + -p frame_id:=base_link +``` + +3D lidar: + +```bash +ros2 run rtabmap_odom icp_odometry --ros-args \ + -r scan_cloud:=/velodyne_points \ + -p frame_id:=base_link \ + -p "Icp/PointToPlane:='true'" \ + -p scan_normal_k:=10 \ + -p scan_voxel_size:=0.1 +``` + +```python +ComposableNode( + package='rtabmap_odom', + plugin='rtabmap_odom::ICPOdometry', + name='icp_odometry', + parameters=[{'frame_id': 'base_link', + 'scan_voxel_size': 0.1, + 'scan_normal_k': 10, + 'Icp/PointToPlane': 'true'}], + remappings=[('scan_cloud', '/velodyne_points')]) +``` + +## Subscribed Topics + +One of the two scan topics, not both. + +| Topic | Type | Description | +|---|---|---| +| `scan` | [`sensor_msgs/msg/LaserScan`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/LaserScan.html) | A 2D lidar. | +| `scan_cloud` | [`sensor_msgs/msg/PointCloud2`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/PointCloud2.html) | A 3D lidar, or a 2D one already converted to a cloud. | +| `imu` | [`sensor_msgs/msg/Imu`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/Imu.html) | Optional, and more useful here than elsewhere — it pins roll and pitch, which a lidar alone constrains poorly. | + +## Published Topics + +Most are common to all three nodes; see [the README](../README.md#published-topics). Two belong to this path: + +| Topic | Type | Description | +|---|---|---| +| `odom_local_scan_map` | [`sensor_msgs/msg/PointCloud2`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/PointCloud2.html) | The accumulated scan map the current scan was registered against. | +| `odom_filtered_input_scan` | [`sensor_msgs/msg/PointCloud2`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/PointCloud2.html) | The input scan **after** deskewing, voxelization, range filtering and normal estimation — exactly what ICP registered, carrying the original header. | + +### Reusing the filtered scan downstream + +Remap `odom_filtered_input_scan` onto `rtabmap`'s `scan_cloud` and the map is built from the scan this node already prepared, rather than from the raw sweep. + +```mermaid +flowchart LR + LIDAR["lidar"] + ICP["icp_odometry"] + MAP["rtabmap
subscribe_scan_cloud:=true"] + LIDAR -->|scan_cloud| ICP + ICP -->|odom_filtered_input_scan| MAP + ICP -->|odom + TF| MAP +``` + +That skips the expensive half twice over. Voxelization and normal estimation are not repeated, since the cloud arrives already decimated and carrying `normal_*` fields, and the deskewing this node did is carried along with it. + +The alternative for deskewing is to do it **before** odometry, with [`lidar_deskewing`](../../rtabmap_util/doc/lidar_deskewing.md), and fan the corrected cloud out to both nodes: + +```mermaid +flowchart LR + LIDAR["lidar"] + DESKEW["lidar_deskewing"] + ICP["icp_odometry"] + MAP["rtabmap
subscribe_scan_cloud:=true"] + LIDAR -->|scan_cloud| DESKEW + DESKEW -->|deskewed cloud| ICP & MAP + ICP -->|odom + TF| MAP +``` + +That is the only way to give `rtabmap` **every point of the sweep**. `odom_filtered_input_scan` carries the decimated cloud ICP registered, so a map built from it inherits whatever voxelization odometry applied — and outdoors that is 30 to 50 cm. Deskewing upstream separates the two: odometry can filter as hard as it likes while the map is built from the full-resolution cloud, at the cost of an extra node and an extra copy of every sweep. + +## Parameters + +Specific to this node: the scan is filtered **before** ICP sees it, and these control that. The registration itself is tuned through RTAB-Map's `Icp/*` parameters. + +These two sets overlap, and the node resolves the overlap for you — see [Where these defaults come from](#where-these-defaults-come-from), because the defaults below are not what the source's initializers suggest. + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `scan_voxel_size` | `double` | `0.05` | Downsample to one point per voxel of this size, in meters. From `Icp/VoxelSize`. `0` disables it here. | +| `scan_downsampling_step` | `int` | `1` | Keep every Nth point. From `Icp/DownsamplingStep`. Cheaper than voxelization but density-dependent; prefer `scan_voxel_size`. | +| `scan_range_min` | `double` | `0.0` | Drop points closer than this, in meters. From `Icp/RangeMin`. `0` disables. Useful against the robot's own body. | +| `scan_range_max` | `double` | `0.0` | Drop points farther than this. From `Icp/RangeMax`. `0` disables. | +| `scan_normal_k` | `int` | `5` | Estimate each point's normal from this many neighbours. From `Icp/PointToPlaneK`. **Point-to-plane ICP needs normals**; without them it has nothing to work with. | +| `scan_normal_radius` | `double` | `0.0` | Estimate normals from neighbours within this radius instead. From `Icp/PointToPlaneRadius`. `0` disables. | +| `scan_normal_ground_up` | `double` | `0.0` | Force normals to point upward when within this dot-product threshold of vertical. From `Icp/PointToPlaneGroundNormalsUp`. Helps on ground planes. | +| `scan_cloud_max_points` | `int` | `-1` | How many points a full sweep of the lidar holds. It is the **denominator of the correspondence ratio** — see [Making the correspondence ratio mean something](#making-the-correspondence-ratio-mean-something). `-1` leaves it unset; an organized cloud fills it in automatically. | +| `scan_cloud_is_2d` | `bool` | `false` | Treat `scan_cloud` as a planar scan even though it carries a `z` field, so it is registered as a 2D scan rather than a 3D one. For a 2D lidar already converted to a cloud. | +| `deskewing` | `bool` | `false` | Correct for motion during the sweep. See [Deskewing](#deskewing). | +| `deskewing_slerp` | `bool` | `false` | Interpolate the deskewing transform rather than looking up TF per point. Faster, slightly less accurate. | +| `topic_queue_size` | `int` | `1` | Queue depth of the scan subscription. Deliberately small: a stale scan is worse than a dropped one. | + +### Where these defaults come from + +Filtering a scan and filtering it again inside ICP would be wasted work, so at startup the node moves each filter from RTAB-Map's parameter to its own: + +> `IcpOdometry: Transferring value 5 of "Icp/PointToPlaneK" to ros parameter "scan_normal_k" for convenience.` + +That log line is normal, not a warning about your configuration. Each `scan_*` parameter above **takes its default from the matching `Icp/*` parameter**, and the `Icp/*` one is then set to `0` so the filter runs once, here, rather than twice. This is why `scan_voxel_size` is `0.05` and `scan_normal_k` is `5` out of the box rather than disabled. + +Setting the ROS parameter explicitly wins: the transfer is skipped and the `Icp/*` value is zeroed instead. Setting **both** is the case to avoid — the node warns that both are set, and the scan is then filtered twice: + +``` +IcpOdometry: Both parameter "Icp/VoxelSize" and ros parameter "scan_voxel_size" are set. +``` + +So tune through `scan_*` **or** through `Icp/*`, not both. + +### Making the correspondence ratio mean something + +`Icp/CorrespondenceRatio` decides whether a registration is trustworthy: the points ICP managed to pair, over the points it could have paired. `scan_cloud_max_points` is what sets that second number. Set it to the theoretical maximum points per sweep. No need to set it explicitly for organized clouds though, `width × height` will be used as maximum points. + +Left at `-1`, the denominator becomes the size of the larger of the two scans being matched (for dense clouds). Take two scans that came back with 30 and 50 points — a lidar staring at open space, where most rays returned nothing. Dividing by 50 says "we matched most of what we saw", and the ratio looks healthy. But the sensor emits 10000 rays a sweep, so 50 returns means almost nothing was in range, and the registration is resting on nearly no evidence. Told that a full sweep is 10000 points, ICP divides by that instead and the ratio collapses to what the overlap actually was, so the threshold rejects the frame. + +## Preparing the scan + +ICP cost grows with the number of points, and a 3D lidar produces far more than registration needs — a 64-beam sensor is a hundred thousand points per sweep, and aligning them all is both slow and *no more accurate* than aligning a well-spread subset. Voxelization is therefore on by default at 5 cm. + +What it buys beyond speed is even density, which matters more than the point count: a raw lidar sweep is dense near the sensor and sparse far away, so an unvoxelized ICP is dominated by whatever is closest — often the robot itself or the ground right under it. Size `scan_voxel_size` to the environment: **0.05 to 0.2 m indoors**, and **0.3 to 0.5 m outdoors**, where the scene is far larger and the extra resolution buys nothing but CPU. + +**Move `Icp/MaxCorrespondenceDistance` with it — a good rule of thumb is ten times the voxel size.** The two are coupled: voxelizing at 0.3 m leaves neighbouring points that far apart, so a correspondence distance of 0.1 m cannot pair anything and ICP returns nothing at all. + +**Point-to-plane ICP converges better than point-to-point** on the flat surfaces that dominate most environments, and it is what the default `scan_normal_k` of 5 is there to support: + +```bash +-p "Icp/PointToPlane:='true'" -p scan_normal_k:=10 +``` + +The quoting is not optional: RTAB-Map parameters are strings, and an unquoted `true` makes the node throw on startup ([why](../README.md#rtab-maps-own-parameters)). + +Whether it is on by default depends on how RTAB-Map was built — `Icp/PointToPlane` defaults to `true` only with libpointmatcher available, and `false` otherwise — so set it explicitly if you care. + +`scan_range_min` is worth setting on any robot whose lidar can see parts of itself. Those points are perfectly self-consistent between scans, so they pull the alignment toward "no motion" — a bias that looks like the robot under-travelling rather than like an error. + +## Deskewing + +A spinning lidar measures its points over a whole revolution, not at an instant. If the robot moves during that revolution, the scan is a smear — the points are in a frame that no longer exists by the time the sweep ends. At walking pace with a 10 Hz lidar this is centimeters; on a fast vehicle it dominates the error budget. + +`deskewing:=true` corrects it. It needs the cloud to carry **per-point timestamps**, in a field named `t`, `time`, `stamps` or `timestamp`; without one the node logs an error and drops the frame rather than guessing. + +There are two ways it gets the motion to correct with, and which one is used depends on whether you gave it an external guess: + +- **With `guess_frame_id` set** — the motion comes from that TF. This is the accurate route, and the reason to pair deskewing with wheel odometry or an IMU-integrated frame. +- **Without it** — a constant-velocity model from the previous frame's estimate. It cannot deskew the very first frame, and it degrades exactly when velocity changes fastest, which is when deskewing matters most. + +`deskewing_slerp` interpolates between the sweep's endpoints rather than looking up a transform per point. Much cheaper, and accurate enough unless the motion within one sweep is strongly non-linear. + +## Degenerate geometry + +This is the failure that matters, and it is not a bug. ICP recovers only the motion the scene constrains, and some scenes do not constrain all of it: + +- **A long featureless corridor** does not constrain motion *along* the corridor. The walls look identical a meter forward, so ICP happily reports no motion while the robot drives. The map then folds the corridor up into a fraction of its length. +- **A large open space** with everything out of range constrains nothing at all. +- **A flat plane** — a warehouse floor to a horizontal 2D lidar — constrains height and tilt but not translation. + +RTAB-Map detects this rather than walking into it, but only on the point-to-plane path. The defences, in order of effectiveness: + +1. **`guess_frame_id` with wheel odometry.** The guess supplies the motion ICP cannot see, and ICP corrects the part it can. This turns the corridor case from a failure into a non-issue, and it is why lidar odometry on a wheeled robot should essentially always have it. +2. **The structural complexity check**, which is the built-in one. With `Icp/PointToPlane` on, a scan whose normals fail to span the space — the definition of a corridor — scores below `Icp/PointToPlaneMinComplexity` (`0.02`) and is handled by `Icp/PointToPlaneLowComplexityStrategy` instead of being trusted: + + | Value | Behaviour | + |---|---| + | `0` | Reject the transform outright: the frame is reported lost. | + | `1` *(default)* | Recompute with point-to-point and constrain the correction to the axes that *are* observable — in a corridor, y and yaw are kept and **x is taken from the guess**. | + | `2` | Recompute with point-to-point and accept the result as is. | + | `3` | Keep the point-to-plane transform, with the same axis-constrained projection as `1`. | + + The default pairs with defence 1: it detects the unobservable axis and hands that axis to the guess. Without a `guess_frame_id` there is nothing to hand it to, which is why the two belong together. +3. **A 3D lidar instead of a 2D one**, which sees ceiling, floor and doorways that a horizontal slice misses. +4. **`Icp/CorrespondenceRatio`** to reject registrations supported by too few correspondences, so a bad frame is reported lost rather than silently accepted. + +With `Icp/PointToPlane` off, none of the complexity machinery runs: there are no normals to measure, so a degenerate scan is registered and trusted like any other. + +## Combining a camera and a lidar + +With both sensors, the usual arrangement is `icp_odometry` for the pose and the camera for appearance: + +```mermaid +flowchart LR + LIDAR["lidar"] + CAM["RGB-D camera"] + ICP["icp_odometry"] + SYNC["rgbd_sync"] + ODOM(["odom + TF"]) + MAP["rtabmap
subscribe_rgbd + subscribe_scan_cloud"] + LIDAR --> ICP --> ODOM --> MAP + LIDAR --> MAP + CAM --> SYNC --> MAP +``` + +Lidar geometry is the more reliable pose source, while loop closure detection is appearance-based and wants the images. `rtabmap` then subscribes to the camera, the scan and this node's odometry together. + +## When it loses track + +`odom_info` carries the ICP result. The numbers to look at are the correspondence count and ratio: too few correspondences means the scans do not overlap enough, whether because the robot moved too far between them, the range filters are too aggressive, or the scene genuinely changed. + +- **Scans too far apart** — the lidar rate is too low for the speed, or `max_update_rate` is throttling too hard. +- **`Icp/MaxCorrespondenceDistance` too small** — ICP never associates the points at all. It has to be larger than the motion between scans; too large and it associates the wrong things. +- **Everything filtered away** — check `scan_range_min`/`scan_range_max` and `scan_voxel_size` against the actual scale of the environment. + +As everywhere else in this package, `Odom/ResetCountdown` recovers automatically from a lost state instead of staying lost. diff --git a/rtabmap_odom/doc/rgbd_odometry.md b/rtabmap_odom/doc/rgbd_odometry.md new file mode 100644 index 00000000..5ba9220f --- /dev/null +++ b/rtabmap_odom/doc/rgbd_odometry.md @@ -0,0 +1,211 @@ +# rgbd_odometry + +Visual odometry from a color image, a registered depth image and a calibration. + +Each frame's visual features are matched against the previous frame — or against a small local map of recent features — and the camera motion that best explains the matches becomes the pose. Depth turns the 2D feature matches into 3D correspondences, which is what makes the scale real rather than arbitrary. + +Use it when an RGB-D camera is the main sensor. For a stereo pair use [stereo_odometry](stereo_odometry.md); for a lidar, [icp_odometry](icp_odometry.md). All three publish the same topics and share the parameters in the [package README](../README.md#conventions), which covers frames, TF, the RTAB-Map parameters, guesses, the IMU and the services. This page covers what is specific to this node. + +## Contents + +- [Pipeline arrangements](#pipeline-arrangements) +- [Usage](#usage) +- [Subscribed Topics](#subscribed-topics) +- [Published Topics](#published-topics) +- [Parameters](#parameters) +- [Synchronization](#synchronization) +- [Several cameras](#several-cameras) +- [Repetitive patterns](#repetitive-patterns) +- [When it loses track](#when-it-loses-track) + +## Pipeline arrangements + +A camera-only pipeline, with the camera synchronized once and fanned out -- the arrangement [Synchronization](#synchronization) recommends: + +```mermaid +flowchart LR + CAM["RGB-D camera"] + SYNC["rgbd_sync"] + ODOM["rgbd_odometry
subscribe_rgbd:=true"] + MAP["rtabmap
subscribe_rgbd:=true"] + CAM -->|rgb/image
depth/image
rgb/camera_info| SYNC + SYNC -->|rgbd_image| ODOM & MAP + ODOM -->|odom + TF| MAP +``` + +Without `rgbd_sync`, both nodes subscribe to the three raw topics and each synchronizes them independently -- which works, but lets the two settle on different pairings. + +Another arrangement drops `rgbd_sync` altogether and feeds `rtabmap` from **this node's own output** instead: + +```mermaid +flowchart LR + CAM["RGB-D camera"] + ODOM["rgbd_odometry"] + MAP["rtabmap
subscribe_rgbd or subscribe_sensor_data"] + CAM -->|rgb/image
depth/image
rgb/camera_info| ODOM + ODOM -->|odom_rgbd_image
or odom_sensor_data| MAP + ODOM -->|odom + TF| MAP +``` + +Remap `rtabmap`'s `rgbd_image` to `odom_rgbd_image`, or set `subscribe_sensor_data` and remap to `odom_sensor_data/raw`. Two things come for free: + +- **No separate synchronization.** This node already matched the three topics to register the frame, and republishes the result, so there is no `rgbd_sync` to run and no second synchronizer to agree with. +- **The features are reused.** `odom_sensor_data` carries the keypoints, their 3D positions and their descriptors that odometry extracted; they survive the conversion back into RTAB-Map on the other side, so `rtabmap` does not redo feature detection and descriptor extraction. + +## Usage + +Against a camera's raw topics: + +```bash +ros2 run rtabmap_odom rgbd_odometry --ros-args \ + -r rgb/image:=/camera/color/image_raw \ + -r depth/image:=/camera/depth/image_rect_raw \ + -r rgb/camera_info:=/camera/color/camera_info \ + -p frame_id:=base_link +``` + +Against an [`rgbd_sync`](https://github.com/introlab/rtabmap_ros/tree/ros2/rtabmap_sync) output, which is the better arrangement when anything else consumes the same camera: + +```bash +ros2 run rtabmap_odom rgbd_odometry --ros-args \ + -p subscribe_rgbd:=true \ + -r rgbd_image:=/camera/rgbd_image \ + -p frame_id:=base_link +``` + +```python +ComposableNode( + package='rtabmap_odom', + plugin='rtabmap_odom::RGBDOdometry', + name='rgbd_odometry', + parameters=[{'frame_id': 'base_link', 'subscribe_rgbd': True}], + remappings=[('rgbd_image', '/camera/rgbd_image')]) +``` + +## Subscribed Topics + +Which topics are used depends on `subscribe_rgbd` and `rgbd_cameras`. + +**Default** — `subscribe_rgbd:=false`: + +| Topic | Type | Description | +|---|---|---| +| `rgb/image` | [`sensor_msgs/msg/Image`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/Image.html) | Color or mono image. Goes through `image_transport`. | +| `depth/image` | [`sensor_msgs/msg/Image`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/Image.html) | Depth registered to the color camera. `16UC1` in millimeters or `32FC1` in meters. | +| `rgb/camera_info` | [`sensor_msgs/msg/CameraInfo`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/CameraInfo.html) | Calibration of the color camera. | + +**With `subscribe_rgbd:=true`**, one pre-synchronized message instead of three topics: + +| `rgbd_cameras` | Topic | Type | +|---|---|---| +| `1` (default) | `rgbd_image` | [`rtabmap_msgs/msg/RGBDImage`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/RGBDImage.html) | +| `2`–`6` | `rgbd_image0` … `rgbd_image5` | [`rtabmap_msgs/msg/RGBDImage`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/RGBDImage.html) | +| `0` | `rgbd_images` | [`rtabmap_msgs/msg/RGBDImages`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/RGBDImages.html) | + +`rgbd_cameras:=0` takes any number of cameras in a single message, which is what [`rgbdx_sync`](https://github.com/introlab/rtabmap_ros/tree/ros2/rtabmap_sync) produces — the route that needs no rebuild and the only one that goes past six. + +| Topic | Type | Description | +|---|---|---| +| `imu` | [`sensor_msgs/msg/Imu`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/Imu.html) | Optional. Constrains roll and pitch; see [the README](../README.md#imu). | + +## Published Topics + +`odom`, `odom_info`, `odom_local_map`, `odom_last_frame`, `odom_rgbd_image` and the rest are common to all three nodes and documented in [the README](../README.md#published-topics). + +## Parameters + +Specific to this node. The shared ones — `frame_id`, `publish_tf`, `guess_frame_id`, `max_update_rate`, all of RTAB-Map's own — are in [the README](../README.md#conventions). + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `subscribe_rgbd` | `bool` | `false` | Take a pre-synchronized `RGBDImage` instead of three raw topics. | +| `rgbd_cameras` | `int` | `1` | Number of `RGBDImage` topics. `0` means one `RGBDImages` topic carrying any number. Only with `subscribe_rgbd:=true`. | +| `approx_sync` | `bool` | `true` | Match the raw topics by nearest stamp. See [Synchronization](#synchronization). | +| `approx_sync_max_interval` | `double` | `0.0` | Reject sets spanning more than this many seconds. `0` disables. | +| `topic_queue_size` | `int` | `10` | Queue depth of each input subscription. | +| `sync_queue_size` | `int` | `5` | Queue depth of the synchronizer. | +| `queue_size` | `int` | — | **Deprecated**, renamed to `sync_queue_size`. Copied to it with a warning. | +| `qos_camera_info` | `int` | value of `qos` | Reliability of the `rgb/camera_info` subscription alone. | +| `keep_color` | `bool` | `false` | Keep the color image in the data handed to the odometry instead of converting to grayscale. Registration is grayscale either way; this only matters for what downstream consumers of `odom_rgbd_image` receive. | +| `image_transport` | `string` | `"raw"` | Transport for `rgb/image`, e.g. `compressed`. | +| `depth_transport` | `string` | `"raw"` | Transport for `depth/image`, e.g. `compressedDepth`. | +| `rgb_transport` | `string` | — | **Deprecated**, renamed to `image_transport`. | + +## Synchronization + +With `subscribe_rgbd:=false` this node runs its own synchronizer over the three raw topics, and the same trade-off applies as everywhere else in this stack: **exact matching is cheaper and cannot mismatch, but publishes nothing at all if the stamps differ by a nanosecond**. The default here is approximate because many RGB-D cameras do not stamp color and depth identically. + +When several nodes consume the same camera — odometry and `rtabmap`, usually — **synchronize once with `rgbd_sync` and set `subscribe_rgbd:=true` on both**. Two independent approximate synchronizers over the same three topics can settle on different pairings, and then `rtabmap` maps a frame at a pose computed from a different one. Feeding both from one `RGBDImage` removes the possibility. + +`approx_sync_max_interval` is worth setting whenever approximate matching stays on: without it, a camera that stalls and resumes silently pairs a fresh color frame with a stale depth frame. A tenth of the frame period is a reasonable start. + +## Several cameras + +More cameras means more of the scene is textured enough to track, which is the usual reason visual odometry fails indoors. Point them in different directions rather than overlapping. + +Two routes, both requiring the frames to be synchronized and each camera to be in TF: + +- **`rgbd_cameras:=2..6`** subscribes to `rgbd_image0`…`rgbd_imageN` and synchronizes them here. +- **`rgbd_cameras:=0`** takes one `rgbd_images` topic from `rgbdx_sync`, which has no upper limit and needs no rebuild. + +**RTAB-Map has to be built with OpenGV for this.** The default motion estimation is PnP (`Vis/EstimationType=1`, 3D→2D), and the multi-camera version of it lives in OpenGV. Without that dependency the registration refuses to run and says so: + +``` +Multi-camera 2D-3D PnP registration is only available if rtabmap is built with +OpenGV dependency. Use 3D-3D registration approach instead for multi-camera. +``` + +Check with `rtabmap --version`, which prints a `With OpenGV:` line. If it says `false`, either rebuild RTAB-Map against OpenGV or switch to `Vis/EstimationType:='0'` (3D→3D), which needs no extra dependency but registers point cloud to point cloud rather than reprojecting, and is the weaker estimator when depth is noisy. + +**Hardware-synchronize the cameras if you can.** The node treats the set as one rigid observation at one timestamp: features from every camera are registered together, with the extrinsics from TF held fixed. There is no equivalent of lidar deskewing here — it cannot estimate the motion that happened *within* the rig between one camera's exposure and the next. If the cameras fire at different instants while the robot moves, that motion is absorbed as though the rig had flexed, and the registration is pulled off by however far the robot travelled in between. + +Synchronizing the topics is not the same thing: `approx_sync` only decides which frames are grouped, it cannot undo an exposure that happened 20 ms later than its neighbour's. + +Calibration matters more with several cameras than with one for the same reason: the extrinsics between them come from TF, and an error there shows up as a constant bias in the estimated motion rather than as an obvious failure. + +## Features computed elsewhere + +`RGBDImage` has fields for local features — `key_points`, `points` and `descriptors` — and when a frame arrives with them filled, this node hands them to the odometry as they are instead of detecting and describing anything. RTAB-Map extracts features only from a frame that brought none, so nothing is recomputed. + +This is for a camera, or a driver, that already does the extraction: on a multi-camera rig it is most of the per-frame work, and it can be done once and shared with `rtabmap` rather than repeated in each node. + +What a publisher has to get right: + +- **One entry per camera**, in the same order as the images, for `rgbd_cameras:=0` as well as the numbered topics. +- **Keypoints in their own camera's image coordinates.** The node stitches the images side by side and shifts each camera's keypoints by the images that precede it. +- **3D points in that camera's optical frame.** They are brought into `frame_id` with the camera's transform from TF, the same one used for the calibration. +- **Descriptors compressed** with `rtabmap::compressData()`, one row per keypoint, the same type for every camera. +- **Equal counts.** `points` and `descriptors` may be left empty, but if they are there, they must have as many entries as there are keypoints. A frame whose three disagree has its features dropped with an error rather than used out of step. + +Both images may be left out entirely: the depth image's job was to give the keypoints their depth and they arrive with it, and the color image's was to have features found in it. A frame is then its calibration and its features, which is the whole point — the images are nearly all of the bandwidth. The `camera_info` of each camera has to be there either way, as it is what says how big the image would have been and, through its `frame_id`, where the camera is. + +What stops applying, since nothing is extracted: `Vis/MaxFeatures`, `Vis/DepthAsMask`, the detector chosen with `Kp/DetectorStrategy`, and the depth bounds `Vis/MinDepth` and `Vis/MaxDepth`. Whatever is published is what gets registered, so the publisher owns those decisions. `Vis/CorType` must stay at `0` (feature matching); optical flow (`1`) reads the images themselves and has nothing to work with here. + +## Repetitive patterns + +Not every failure announces itself. A scene full of identical detail — a tiled floor, rows of identical shelving, a patterned carpet, a brick wall — hands the matcher plenty of features and plenty of confident matches, just not always the *right* ones. One tile matched to its neighbour looks like a perfectly good inlier, and the pose comes out shifted by exactly one tile. Inlier counts stay healthy, nothing is reported lost, and the trajectory drifts in steps. + +The defence is to constrain **where** a match is allowed to come from: + +- **`guess_frame_id`** gives each feature a predicted image position, from wheel odometry or another external source. +- **`Vis/CorGuessWinSize`** bounds the search around that prediction — 40 pixels by default. Reducing it, to 10 or 20, means a feature can only match something close to where the guess says it should be, so the identical neighbour one tile away is never a candidate. + +## When it loses track + +Visual odometry fails when there is nothing to match: a blank wall, a dark room, motion blur, or a scene where everything moved. The node then publishes a null pose (see [the README](../README.md#lost-frames-resets-and-new-maps)) and `odom_info` says why. + +Look at `odom_info` first — `inliers` is the number that matters: + +```bash +ros2 topic echo /odom_info --field inliers +``` + +Inliers falling below `Vis/MinInliers` (default 20) is the definition of a lost frame. Whether the fix is more features, a better guess or a different strategy depends on which part is short: + +- **Few features detected at all** — the scene is untextured or too dark. Lowering `Vis/MinInliers` lets frames register on fewer matches, but a pose resting on a handful of inliers is poorly constrained and drifts badly — it buys continuity at the price of accuracy. The real fixes are physical. If it is dark, add a light — a spotlight on the robot restores texture the camera can track, and costs far less than changing sensor. Otherwise it is **more field of view**: a wider lens, or [several cameras](#several-cameras) pointed in different directions. It only takes one textured patch somewhere in view to track, so a blank wall filling a narrow FOV stops being a problem the moment the rig can also see the ceiling or a doorway. Failing that, the camera is the wrong sensor here — a lidar if the scene has geometry, wheel odometry if it has neither. See [Choosing a sensor modality for the environment](../README.md#choosing-a-sensor-modality-for-the-environment). +- **Features detected but few matched** — motion is too fast for the search window, or the frame rate is too low. A `guess_frame_id` from wheel odometry is what helps most here. +- **Matched but rejected as outliers** — usually a moving scene, or depth that does not agree with the color image. Check that depth really is registered to color. + +`Odom/ResetCountdown` gets the node out of a lost state automatically instead of leaving it lost until something calls `reset_odom`. + +**A camera alone is not a great odometry source on a wheeled robot.** If the base publishes wheel odometry, feeding it in through `guess_frame_id` is worth more than any amount of tuning here. diff --git a/rtabmap_odom/doc/stereo_odometry.md b/rtabmap_odom/doc/stereo_odometry.md new file mode 100644 index 00000000..d39a7062 --- /dev/null +++ b/rtabmap_odom/doc/stereo_odometry.md @@ -0,0 +1,219 @@ +# stereo_odometry + +Visual odometry from a stereo pair. + +Features are found in the left image and matched into the right one to get their depth by disparity, then matched against the previous frame to get the motion. It is the same registration as [rgbd_odometry](rgbd_odometry.md); only the source of depth differs — computed here from the pair rather than measured by the sensor. + +That difference is the reason to choose it. A stereo pair works outdoors and at range, where the projected-pattern depth of an RGB-D camera returns nothing, and its accuracy degrades gracefully with distance instead of cutting off. The cost is that depth is only available where there is texture to match, and that it depends on a good stereo calibration. + +The shared parameters — frames, TF, guesses, the IMU, RTAB-Map's own parameters, the services — are in the [package README](../README.md#conventions). This page covers what is specific to this node. + +## Contents + +- [Pipeline arrangements](#pipeline-arrangements) +- [Usage](#usage) +- [Subscribed Topics](#subscribed-topics) +- [Published Topics](#published-topics) +- [Parameters](#parameters) +- [Synchronization](#synchronization) +- [Getting the scale right](#getting-the-scale-right) +- [Features computed elsewhere](#features-computed-elsewhere) +- [Repetitive patterns](#repetitive-patterns) +- [When it loses track](#when-it-loses-track) + +## Pipeline arrangements + +A typical stereo pipeline, rectification included: + +```mermaid +flowchart LR + CAM["stereo driver"] + PROC["stereo_image_proc"] + SYNC["stereo_sync"] + ODOM["stereo_odometry"] + MAP["rtabmap"] + CAM -->|left/image_raw
right/image_raw
camera_info x2| PROC + PROC -->|left/image_rect
right/image_rect
camera_info x2| SYNC + SYNC -->|rgbd_image| ODOM & MAP + ODOM -->|odom + TF| MAP +``` + +`stereo_image_proc` can be dropped when the driver already publishes rectified images: + +```mermaid +flowchart LR + CAM["stereo driver
publishing rectified images"] + SYNC["stereo_sync"] + ODOM["stereo_odometry"] + MAP["rtabmap"] + CAM -->|left/image_rect
right/image_rect
camera_info x2| SYNC + SYNC -->|rgbd_image| ODOM & MAP + ODOM -->|odom + TF| MAP +``` + +The same shortcut as on the RGB-D side is available here: drop `stereo_sync` and feed `rtabmap` from **this node's own output**. + +```mermaid +flowchart LR + CAM["stereo driver
publishing rectified images"] + ODOM["stereo_odometry"] + MAP["rtabmap
subscribe_rgbd or subscribe_sensor_data"] + CAM -->|left/image_rect
right/image_rect
camera_info x2| ODOM + ODOM -->|odom_rgbd_image
or odom_sensor_data| MAP + ODOM -->|odom + TF| MAP +``` + +Remap `rtabmap`'s `rgbd_image` to `odom_rgbd_image`, or set `subscribe_sensor_data` and remap to `odom_sensor_data/raw`. The stereo pair survives the trip intact -- the left image, the right image and both calibrations travel in the one message, exactly as `stereo_sync` would have packed them -- and the features this node extracted come with it, so `rtabmap` does not redo feature detection and descriptor extraction. + +Everything above hands the node rectified images. It can also take the raw pair, straight from the driver: + +```mermaid +flowchart LR + CAM["stereo driver"] + ODOM["stereo_odometry
Rtabmap/ImagesAlreadyRectified:=false"] + MAP["rtabmap
subscribe_rgbd or subscribe_sensor_data"] + CAM -->|left/image_raw
right/image_raw
camera_info x2| ODOM + ODOM -->|odom_rgbd_image
or odom_sensor_data| MAP + ODOM -->|odom + TF| MAP +``` + +Two different things can make that work: + +- **The odometry rectifies the pair itself.** With `Rtabmap/ImagesAlreadyRectified:=false` it builds a rectification map from the calibration and applies it to every frame, saying so once: + + ``` + Rtabmap/ImagesAlreadyRectified parameter is set to false but the selected odometry + approach cannot process raw stereo images. We will rectify them for convenience. + ``` + + It needs the geometry between the two cameras to do that — the right `camera_info` carrying `P(0,3)`, or TF between the two camera frames. If a rectification map cannot be built from what the calibration says, the frame is refused rather than registered wrong. + +- **The odometry takes them raw.** A few approaches do their own undistortion and want the unrectified images: `Odom/Strategy` `6` (OKVIS), `8` (MSCKF), `9` (VINS-Fusion) and `10` (OpenVINS), each available only if RTAB-Map was built against that library. Nothing rectifies anything then, and `Rtabmap/ImagesAlreadyRectified:=false` simply tells the pipeline to leave the images alone. + +Which of the two applies decides what `rtabmap` needs when it is fed from this node's output. If the odometry rectified the pair, the rectified images are what travels on -- they replace the raw ones in the frame -- and `rtabmap` keeps `Rtabmap/ImagesAlreadyRectified` at its default `true`. If the odometry took them raw, they arrive raw, and `rtabmap` needs `Rtabmap/ImagesAlreadyRectified:=false` of its own to rectify them again on its side. Set `Mem/UseOdomFeatures:=false` along with it, since it defaults to `true`: `rtabmap` rectifies a stereo pair but does not currently map features that travelled with the frame into the rectified image, so any it reused would be read against the wrong one. + +Rectifying here costs what `stereo_image_proc` would have cost, but only on the frames the odometry actually registers. When it runs slower than the camera — throttled by `max_update_rate`, or dropping frames that arrive while a registration is still running — the rectification happens at the odometry's rate instead of the camera's, and every frame `stereo_image_proc` would have rectified for nothing is saved. Where something else needs the whole stream rectified, the first arrangement is still the one to use. + +## Usage + +```bash +ros2 run rtabmap_odom stereo_odometry --ros-args \ + -r left/image_rect:=/stereo/left/image_rect \ + -r right/image_rect:=/stereo/right/image_rect \ + -r left/camera_info:=/stereo/left/camera_info \ + -r right/camera_info:=/stereo/right/camera_info \ + -p frame_id:=base_link +``` + +```python +ComposableNode( + package='rtabmap_odom', + plugin='rtabmap_odom::StereoOdometry', + name='stereo_odometry', + parameters=[{'frame_id': 'base_link'}], + remappings=[('left/image_rect', '/stereo/left/image_rect'), + ('right/image_rect', '/stereo/right/image_rect'), + ('left/camera_info', '/stereo/left/camera_info'), + ('right/camera_info', '/stereo/right/camera_info')]) +``` + +**The images are normally rectified**, which is what the `image_rect` topic names assume, and what [`stereo_image_proc`](https://docs.ros.org/en/jazzy/p/stereo_image_proc/) produces when the driver does not. + +They do not have to be. RTAB-Map can rectify them itself from the calibration -- set `Rtabmap/ImagesAlreadyRectified:=false` and feed it the raw pair with distortion coefficients in the `camera_info`. What does not work is the silent middle case: **unrectified images with that parameter left at its default of `true`**. Nothing fails loudly; disparity is computed across rows that no longer correspond, giving depths that are wrong in a smoothly varying way and a trajectory that is wrong without looking broken. + +## Subscribed Topics + +**Default** — `subscribe_rgbd:=false`: + +| Topic | Type | Description | +|---|---|---| +| `left/image_rect` | [`sensor_msgs/msg/Image`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/Image.html) | Rectified left image, color or mono. | +| `right/image_rect` | [`sensor_msgs/msg/Image`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/Image.html) | Rectified right image, color or mono. | +| `left/camera_info` | [`sensor_msgs/msg/CameraInfo`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/CameraInfo.html) | Left calibration. | +| `right/camera_info` | [`sensor_msgs/msg/CameraInfo`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/CameraInfo.html) | Right calibration. Its `P` matrix carries the baseline, which sets the scale of the whole trajectory. | + +**With `subscribe_rgbd:=true`**, one pre-synchronized message from [`stereo_sync`](https://github.com/introlab/rtabmap_ros/tree/ros2/rtabmap_sync): + +| `rgbd_cameras` | Topic | Type | +|---|---|---| +| `1` (default) | `rgbd_image` | [`rtabmap_msgs/msg/RGBDImage`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/RGBDImage.html) | +| `2`–`6` | `rgbd_image0` … `rgbd_image5` | [`rtabmap_msgs/msg/RGBDImage`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/RGBDImage.html) | +| `0` | `rgbd_images` | [`rtabmap_msgs/msg/RGBDImages`](https://docs.ros.org/en/jazzy/p/rtabmap_msgs/msg/RGBDImages.html) | + +| Topic | Type | Description | +|---|---|---| +| `imu` | [`sensor_msgs/msg/Imu`](https://docs.ros.org/en/jazzy/p/sensor_msgs/msg/Imu.html) | Optional. See [the README](../README.md#imu). | + +## Published Topics + +Common to all three nodes; see [the README](../README.md#published-topics). + +## Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `subscribe_rgbd` | `bool` | `false` | Take a pre-synchronized `RGBDImage` from `stereo_sync` instead of four raw topics. | +| `rgbd_cameras` | `int` | `1` | Number of `RGBDImage` topics. `0` means one `RGBDImages` topic. Only with `subscribe_rgbd:=true`. More than one needs RTAB-Map built with OpenGV, and the cameras hardware-synchronized — see [Several cameras](rgbd_odometry.md#several-cameras). | +| `approx_sync` | `bool` | `false` | Match the raw topics by nearest stamp. **Defaults to exact**, unlike `rgbd_odometry` — see [Synchronization](#synchronization). | +| `approx_sync_max_interval` | `double` | `0.0` | Reject sets spanning more than this many seconds. `0` disables. Only used when `approx_sync` is on. | +| `topic_queue_size` | `int` | `10` | Queue depth of each input subscription. | +| `sync_queue_size` | `int` | `5` | Queue depth of the synchronizer. | +| `queue_size` | `int` | — | **Deprecated**, renamed to `sync_queue_size`. | +| `qos_camera_info` | `int` | value of `qos` | Reliability of the two `camera_info` subscriptions. | +| `keep_color` | `bool` | `false` | Keep the left image in color in the data passed on, rather than converting to grayscale. Registration is grayscale either way. | +| `image_transport` | `string` | `"raw"` | Transport for both image topics. | + +## Synchronization + +**This node defaults to exact matching**, because a stereo pair is normally hardware-triggered and the two images therefore carry identical stamps. That is the right default: exact matching is cheaper and cannot pair the left image with the wrong right one — and a mismatched stereo pair does not produce an error, it produces wrong disparities and a wrong trajectory. + +The failure mode to recognize is the other one: if the stamps are *not* identical, **nothing is ever published and nothing says why**. Check before assuming the node is broken: + +```bash +ros2 topic echo --once /stereo/left/image_rect --field header.stamp +ros2 topic echo --once /stereo/right/image_rect --field header.stamp +``` + +If they differ, set `approx_sync:=true` and set `approx_sync_max_interval` to something tight — a stereo pair whose images are more than a fraction of a frame apart is not usable for disparity regardless. + +## Getting the scale right + +Everything about a stereo trajectory's scale comes from the **baseline**, which this node reads from the right camera's `P` matrix (`P[3] = -fx * baseline`). Two consequences: + +- A `right/camera_info` whose `P` matrix is all zeros — which some drivers publish before calibration is loaded — gives a zero baseline and no usable depth at all. +- A calibration whose baseline is off by a few percent produces a trajectory off by the same few percent, consistently, with nothing else looking wrong. + +If the map comes out uniformly too large or too small, check the baseline before anything else. + +## Features computed elsewhere + +`RGBDImage` has fields for local features — `key_points`, `points` and `descriptors` — and when a frame arrives with them filled, this node hands them to the odometry as they are instead of detecting and describing anything. RTAB-Map extracts features only from a frame that brought none, so nothing is recomputed, and neither is the disparity search that would otherwise give each feature its depth. + +This is for a camera, or a driver, that already does the extraction: on a multi-camera rig it is most of the per-frame work, and it can be done once and shared with `rtabmap` rather than repeated in each node. + +What a publisher has to get right: + +- **One entry per camera**, in the same order as the images, for `rgbd_cameras:=0` as well as the numbered topics. +- **Keypoints in their own camera's left image.** The node stitches the left images side by side and shifts each camera's keypoints by the images that precede it. A stereo pair's features belong to the left image; nothing is expected in the right one. +- **3D points in that camera's left optical frame.** They are brought into `frame_id` with the camera's transform from TF, the same one used for the calibration. +- **Descriptors compressed** with `rtabmap::compressData()`, one row per keypoint, the same type for every camera. +- **Equal counts.** `points` and `descriptors` may be left empty, but if they are there, they must have as many entries as there are keypoints. A frame whose three disagree has its features dropped with an error rather than used out of step. + +Both images may be left out entirely — the left image's job was to have features found in it, the right one's to give them their disparity, and they arrive with their 3D positions already. A frame is then its two calibrations and its features, which is the whole point: the images are nearly all of the bandwidth. Both `camera_info` still have to be there, the left one saying how big the image would have been and where the camera is, the right one carrying the baseline in `P(0,3)` — without it there is no scale, features or not. + +What stops applying, since nothing is extracted: `Vis/MaxFeatures`, the detector chosen with `Kp/DetectorStrategy`, and the depth bounds `Vis/MinDepth` and `Vis/MaxDepth`. Whatever is published is what gets registered, so the publisher owns those decisions. `Vis/CorType` must stay at `0` (feature matching); optical flow (`1`) reads the images themselves and has nothing to work with here. + +## Repetitive patterns + +Identical detail repeated across the scene — a tiled floor, rows of shelving, a brick wall — lets the matcher pair a feature with the wrong copy of itself, which drifts the trajectory by exactly one repeat while the inlier count stays healthy and nothing is reported lost. It bites stereo twice over, since the same ambiguity also misplaces the left/right match that sets the depth. + +The fix is the same as for [rgbd_odometry](rgbd_odometry.md#repetitive-patterns): an external guess through `guess_frame_id`, with `Vis/CorGuessWinSize` reduced so a match has to come from close to where the guess predicts. `Stereo/WinWidth` and `Stereo/WinHeight` matter here too — a correlation window smaller than the repeating pattern has nothing unique to lock onto. + +## When it loses track + +The same diagnosis as [rgbd_odometry](rgbd_odometry.md#when-it-loses-track) — `odom_info`'s `inliers` is the number to watch — with two failure modes specific to stereo: + +- **Poor rectification.** Matched features should lie on the same image row. If they do not, either the pair is unrectified while `Rtabmap/ImagesAlreadyRectified` is `true`, or the calibration itself is off. +- **Untextured scene.** With no texture there is nothing to match *between* left and right either, so there is no depth at all — worse than the RGB-D case, where the sensor still measures wrong depth on a blank wall. + +`Stereo/*` parameters tune the disparity matching itself: `Stereo/MaxDisparity` bounds how close a point can be, `Stereo/WinWidth` and `Stereo/WinHeight` the correlation window. They are listed by `ros2 param list` like every other RTAB-Map parameter. diff --git a/rtabmap_odom/include/rtabmap_odom/OdometryROS.h b/rtabmap_odom/include/rtabmap_odom/OdometryROS.h index ca0df5bc..b0e8328c 100644 --- a/rtabmap_odom/include/rtabmap_odom/OdometryROS.h +++ b/rtabmap_odom/include/rtabmap_odom/OdometryROS.h @@ -58,57 +58,144 @@ namespace rtabmap { class Odometry; } +/** + * @file + * @brief The node the three odometry nodes of this package are built on. + */ + namespace rtabmap_odom { +/** + * @brief Runs RTAB-Map's odometry as a ROS node: everything but the subscriptions. + * + * `rgbd_odometry`, `stereo_odometry` and `icp_odometry` differ only in what they listen + * to. Each turns its own topics into a rtabmap::SensorData and hands it to processData(); + * from there on this class does the work -- registration, pose integration, the `odom` + * topic and its TF, the IMU intake, the services, the diagnostics, the reset policy when + * tracking is lost. That is why the three nodes share nearly all of their parameters and + * publish the same topics. + * + * A subclass is expected to: + * - call init() from its constructor, saying which families of RTAB-Map parameters it + * accepts, which decides both the defaults and what the node will accept being set; + * - create its subscriptions in onOdomInit() and describe them with initDiagnosticMsg(); + * - call tick() when a message arrives and processData() once a frame is complete; + * - implement flushCallbacks(), so that a reset can drop whatever its synchronizer holds. + * + * The class is also a UThread. By default the frame handed to processData() is passed to + * that thread and the callback returns at once, so a slow registration cannot block the + * executor; a frame arriving while the thread is busy is dropped rather than queued. With + * `always_process_most_recent_frame:=false` it is registered on the calling thread + * instead, which keeps every frame at the cost of holding up the executor. + * + * @see the package README for the parameters and topics these nodes have in common. + */ class OdometryROS : public rclcpp::Node, public UThread { public: + /// Constructs the node under its default name. explicit OdometryROS(const rclcpp::NodeOptions & options); + /// Constructs the node under @p name, which is what the three nodes use. explicit OdometryROS(const std::string & name, const rclcpp::NodeOptions & options); virtual ~OdometryROS(); + /** + * @brief Hands a complete frame to the odometry; called by a subclass's callback. + * @param[in,out] data the frame to register, which comes back carrying the + * features the odometry ended up using + * @param[in] header stamp and frame of the data, used to publish the result + * + * The frame is either queued for the worker thread or registered right here, + * depending on `always_process_most_recent_frame`. Either way, a frame that arrives + * while the previous one is still being registered is dropped: the odometry stays on + * the newest data rather than falling behind. + */ void processData(rtabmap::SensorData & data, const std_msgs::msg::Header & header); + /// `reset_odom` service: starts a new map at the origin, or at the guess frame's pose. void resetOdom(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// `reset_odom_to_pose` service: starts a new map at the pose given in the request. void resetToPose(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// `pause_odom` service: keeps the subscriptions but stops registering what arrives. void pause(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// `resume_odom` service: registers again, starting from the next frame. void resume(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// `log_debug` service: raises RTAB-Map's own log level to debug at runtime. void setLogDebug(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// `log_info` service; see setLogDebug(). void setLogInfo(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// `log_warning` service; see setLogDebug(). void setLogWarn(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// `log_error` service; see setLogDebug(). void setLogError(const std::shared_ptr, const std::shared_ptr, std::shared_ptr); + /// The robot frame the odometry is computed for, `frame_id`. const std::string & frameId() const {return frameId_;} + /// The frame the estimated poses are expressed in, `odom_frame_id`. const std::string & odomFrameId() const {return odomFrameId_;} + /// The frame an external motion guess is read from, `guess_frame_id`; empty if unused. const std::string & guessFrameId() const {return guessFrameId_;} + /// The RTAB-Map parameters this node was configured with, defaults included. const rtabmap::ParametersMap & parameters() const {return parameters_;} + /// Whether the `pause_odom` service has been called and not resumed since. bool isPaused() const {return paused_;} protected: + /** + * @brief Declares the node's parameters and creates the odometry; call it last in the + * subclass constructor. + * @param[in] stereoParams true if the node takes RTAB-Map's stereo parameters + * @param[in] visParams true if it takes the visual registration ones + * @param[in] icpParams true if it takes the scan matching ones + * + * The three flags decide which RTAB-Map parameters the node declares, and so which + * ones it accepts being set: `icp_odometry` refuses a `Vis/` parameter and the other + * two refuse an `Icp/` one. onOdomInit() is called at the end, for the subclass to + * create its subscriptions. + */ void init(bool stereoParams, bool visParams, bool icpParams); + /// The reliability the subclass should give its own subscriptions, from `qos`. rmw_qos_reliability_policy_t qos() const {return qos_;} + /** + * @brief Starts the diagnostics, once the subclass knows what it subscribed to. + * @param[in] subscribedTopicsMsg the human readable list logged at startup and + * repeated in the "no data received" warning + * @param[in] approxSync whether the subclass matches stamps approximately, + * which that warning mentions as a likely cause + * @param[in] subscribedTopic the one topic whose rate is watched, if any + */ void initDiagnosticMsg(const std::string & subscribedTopicsMsg, bool approxSync, const std::string & subscribedTopic = ""); + /// Drops whatever the subclass's synchronizer holds; called when the odometry resets. virtual void flushCallbacks() {}; + /// The node's TF buffer, for the subclass to look up its sensors' frames. tf2_ros::Buffer & tfBuffer() {return *tfBuffer_;} + /// How long a TF lookup may block, from `wait_for_transform`. const double & waitForTransform() const {return waitForTransform_;} + /// The velocity of the last registered frame, null when there is no estimate yet. rtabmap::Transform velocityGuess() const; + /// Stamp of the last registered frame, 0 before the first one. double previousStamp() const {return previousStamp_;} + /// Called after a frame has been registered and published, for a subclass to add to it. virtual void postProcessData(const rtabmap::SensorData & /*data*/, const std_msgs::msg::Header & /*header*/) const {} private: void processData(); virtual void mainLoop(); virtual void mainLoopKill(); + /// Lets a subclass adjust the RTAB-Map parameters before the odometry is created. virtual void updateParameters(rtabmap::ParametersMap &) {} + /// Called at the end of init(), where a subclass creates its subscriptions. virtual void onOdomInit() {} void callbackIMU(const sensor_msgs::msg::Imu::SharedPtr msg); void reset(const rtabmap::Transform & pose = rtabmap::Transform::getIdentity()); protected: + /// The callback group the subclass's sensor subscriptions belong to. rclcpp::CallbackGroup::SharedPtr dataCallbackGroup_; + /// Reports the arrival of an input message to the diagnostics, before anything else. void tick(const rclcpp::Time & stamp); private: diff --git a/rtabmap_odom/include/rtabmap_odom/icp_odometry.hpp b/rtabmap_odom/include/rtabmap_odom/icp_odometry.hpp index dc50b450..589866b1 100644 --- a/rtabmap_odom/include/rtabmap_odom/icp_odometry.hpp +++ b/rtabmap_odom/include/rtabmap_odom/icp_odometry.hpp @@ -44,6 +44,17 @@ using namespace rtabmap; namespace rtabmap_odom { +/** + * @brief Odometry from a laser scanner, 2D or 3D, by scan matching. + * + * Takes a sensor_msgs::msg::LaserScan or a sensor_msgs::msg::PointCloud2 and registers + * each scan against the previous ones with ICP. A cloud whose points carry their own + * timestamps is deskewed first, using TF or the last known velocity, since a scan taken + * while the robot moves is not one rigid observation. + * + * @see doc/icp_odometry.md for the topics, the parameters and the shapes it can and + * cannot constrain. + */ class ICPOdometry : public rtabmap_odom::OdometryROS { public: diff --git a/rtabmap_odom/include/rtabmap_odom/rgbd_odometry.hpp b/rtabmap_odom/include/rtabmap_odom/rgbd_odometry.hpp index 52649daf..b6046da0 100644 --- a/rtabmap_odom/include/rtabmap_odom/rgbd_odometry.hpp +++ b/rtabmap_odom/include/rtabmap_odom/rgbd_odometry.hpp @@ -38,6 +38,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include +#include +#include #include #include @@ -50,6 +52,18 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. namespace rtabmap_odom { +/** + * @brief Odometry from an RGB-D camera, or from several on one rig. + * + * Takes either the three raw topics of a camera (`rgb/image`, `depth/image`, + * `rgb/camera_info`) or pre-synchronized rtabmap_msgs::msg::RGBDImage messages, one per + * camera, and registers each frame's visual features against a local feature map. A + * frame that arrives with its own keypoints, 3D points and descriptors is registered + * with those rather than having them extracted again. + * + * @see doc/rgbd_odometry.md for the topics, the parameters and what to do when it loses + * tracking. + */ class RGBDOdometry : public rtabmap_odom::OdometryROS { public: @@ -61,10 +75,19 @@ private: virtual void updateParameters(rtabmap::ParametersMap & parameters); virtual void onOdomInit(); + /** + * Local features, when the input topic carries them, are indexed per camera like + * the images are: one entry per camera, in the same order. They are optional, and + * a frame that comes without them is processed exactly as before, the features + * being extracted from the images downstream. + */ void commonCallback( const std::vector & rgbImages, const std::vector & depthImages, - const std::vector& cameraInfos); + const std::vector& cameraInfos, + const std::vector > & localKeyPointsMsgs = {}, + const std::vector > & localPoints3dMsgs = {}, + const std::vector & localDescriptorsMsgs = {}); void callback( const sensor_msgs::msg::Image::ConstSharedPtr image, diff --git a/rtabmap_odom/include/rtabmap_odom/stereo_odometry.hpp b/rtabmap_odom/include/rtabmap_odom/stereo_odometry.hpp index 5b47c8d0..96cdfbf5 100644 --- a/rtabmap_odom/include/rtabmap_odom/stereo_odometry.hpp +++ b/rtabmap_odom/include/rtabmap_odom/stereo_odometry.hpp @@ -43,12 +43,25 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #endif #include +#include +#include #include #include namespace rtabmap_odom { +/** + * @brief Odometry from a stereo pair, or from several on one rig. + * + * Takes either the four raw topics of a stereo camera (left and right image plus their + * calibrations) or pre-synchronized rtabmap_msgs::msg::RGBDImage messages carrying the + * pair, one per camera. The right camera's `P(0,3)` is what gives the trajectory its + * scale. A frame that arrives with its own keypoints, 3D points and descriptors is + * registered with those rather than having them extracted again. + * + * @see doc/stereo_odometry.md for the topics, the parameters and the scale it depends on. + */ class StereoOdometry : public rtabmap_odom::OdometryROS { public: @@ -60,11 +73,20 @@ private: virtual void updateParameters(rtabmap::ParametersMap & parameters); virtual void onOdomInit(); + /** + * Local features, when the input topic carries them, are indexed per camera like + * the images are: one entry per camera, in the same order, and placed in that + * camera's left image. They are optional, and a frame that comes without them is + * processed exactly as before, the features being extracted downstream. + */ void commonCallback( const std::vector & leftImages, const std::vector & rightImages, const std::vector& leftCameraInfos, - const std::vector& rightCameraInfos); + const std::vector& rightCameraInfos, + const std::vector > & localKeyPointsMsgs = {}, + const std::vector > & localPoints3dMsgs = {}, + const std::vector & localDescriptorsMsgs = {}); void callback( const sensor_msgs::msg::Image::ConstSharedPtr imageRectLeft, diff --git a/rtabmap_odom/package.xml b/rtabmap_odom/package.xml index 14870d32..292eeec8 100644 --- a/rtabmap_odom/package.xml +++ b/rtabmap_odom/package.xml @@ -30,8 +30,14 @@ rtabmap_util rtabmap_sync + ament_cmake_gtest + + rosbag2_cpp + rosbag2_storage_mcap + ament_cmake + rosdoc2.yaml diff --git a/rtabmap_odom/rosdoc2.yaml b/rtabmap_odom/rosdoc2.yaml new file mode 100644 index 00000000..6e3ef7d9 --- /dev/null +++ b/rtabmap_odom/rosdoc2.yaml @@ -0,0 +1,35 @@ +## Configuration for rosdoc2, the documentation generator used by docs.ros.org. +## Regenerate the annotated default with: +## rosdoc2 default_config --package-path rtabmap_odom +## Build the docs locally with: +## rosdoc2 build --package-path rtabmap_odom --output-directory doc_output + +## This 'attic section' self-documents this file's type and version. +type: 'rosdoc2 config' +version: 1 + +--- + +settings: + ## Generate the standard index page from package.xml (description, maintainer, + ## license, links) and a table of contents for the builders below. + generate_package_index: true + + ## This is an ament_cmake package, so doxygen runs on the public headers by + ## default and there are no Python modules to document. + always_run_doxygen: false + always_run_sphinx_apidoc: false + +builders: + ## Doxygen parses the public C++ API out of include/. + - doxygen: { + name: 'rtabmap_odom Public C/C++ API', + output_dir: 'generated/doxygen' + } + ## Sphinx renders the landing page and pulls the Doxygen XML in through + ## breathe/exhale so the API is browsable alongside the narrative docs. + - sphinx: { + name: 'rtabmap_odom', + doxygen_xml_directory: 'generated/doxygen/xml', + output_dir: '' + } diff --git a/rtabmap_odom/src/OdometryROS.cpp b/rtabmap_odom/src/OdometryROS.cpp index 5a270c75..40ec056f 100644 --- a/rtabmap_odom/src/OdometryROS.cpp +++ b/rtabmap_odom/src/OdometryROS.cpp @@ -25,6 +25,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include #include "rtabmap_odom/OdometryROS.h" #include @@ -61,6 +62,44 @@ using namespace rtabmap; namespace rtabmap_odom { +namespace { + +/** + * @brief The covariance of a pose that came from the guess frame instead of registration. + * + * Used wherever the guess is what the published pose rests on: a frame the odometry did + * not update because it had not moved enough, and the frame that restarts the map after + * a reset. Nothing was measured in either case, so the confidence is the one the guess + * was declared to have rather than anything the registration computed. + */ +cv::Mat guessCovariance(double linearVariance, double angularVariance) +{ + cv::Mat covariance = cv::Mat::zeros(6,6,CV_64FC1); + covariance.at(0,0) = linearVariance; // xx + covariance.at(1,1) = linearVariance; // yy + covariance.at(2,2) = linearVariance; // zz + covariance.at(3,3) = angularVariance; // rr + covariance.at(4,4) = angularVariance; // pp + covariance.at(5,5) = angularVariance; // yawyaw + return covariance; +} + +/** + * @brief The velocity a motion implies, for a frame with no registration to measure one. + * + * Named apart from the guess itself so that it can be called where a `guessVelocity` + * variable is in scope. + */ +rtabmap::Transform velocityFrom(const rtabmap::Transform & motion, double dt) +{ + UASSERT(dt > 0.0); + float x,y,z,roll,pitch,yaw; + motion.getTranslationAndEulerAngles(x,y,z,roll,pitch,yaw); + return rtabmap::Transform(x/dt, y/dt, z/dt, roll/dt, pitch/dt, yaw/dt); +} + +} // namespace + OdometryROS::OdometryROS(const rclcpp::NodeOptions & options) : OdometryROS("odometry", options) {} @@ -83,6 +122,7 @@ OdometryROS::OdometryROS(const std::string & name, const rclcpp::NodeOptions & o publishNullWhenLost_(true), publishCompressedSensorData_(false), qos_(RMW_QOS_POLICY_RELIABILITY_SYSTEM_DEFAULT), + bufferedDataToProcess_(false), paused_(false), resetCountdown_(0), resetCurrentCount_(0), @@ -194,6 +234,17 @@ OdometryROS::OdometryROS(const std::string & name, const rclcpp::NodeOptions & o "are the same frame (value=\"%s\"). \"guess_frame_id\" is disabled.", odomFrameId_.c_str()); guessFrameId_.clear(); } + if(!publishNullWhenLost_ && guessFrameId_.empty() && publishTf_) + { + RCLCPP_ERROR(this->get_logger(), "\"publish_null_when_lost\" is false, but nothing can " + "say where odometry restarts after being lost: \"guess_frame_id\" is not set and " + "\"publish_tf\" is true, so the %s->%s fallback returns this node's own pose. " + "Whatever the robot did while lost will be silently dropped from the trajectory " + "and mapped across. Set \"guess_frame_id\", or set \"publish_tf\" to false if " + "another node (e.g. robot_localization) publishes %s->%s, or leave " + "\"publish_null_when_lost\" true.", + odomFrameId_.c_str(), frameId_.c_str(), odomFrameId_.c_str(), frameId_.c_str()); + } RCLCPP_INFO(this->get_logger(), "Odometry: frame_id = %s", frameId_.c_str()); RCLCPP_INFO(this->get_logger(), "Odometry: odom_frame_id = %s", odomFrameId_.c_str()); RCLCPP_INFO(this->get_logger(), "Odometry: publish_tf = %s", publishTf_?"true":"false"); @@ -628,8 +679,18 @@ void OdometryROS::processData() imuProcessed_ = true; } + // Whether this is a frame at all, as opposed to an IMU-only update. Neither the image + // nor the features answer that on their own: a frame that brings its own features has + // no image, and a frame of an empty scene has no feature. The calibration does, being + // there whenever a camera produced the data -- the same rule RTAB-Map's own + // Odometry::process() applies before registering anything. + const bool isFrame = !data.imageRaw().empty() || + !data.cameraModels().empty() || + !data.stereoCameraModels().empty() || + !data.laserScanRaw().isEmpty(); + Transform groundTruth; - if(!data.imageRaw().empty() || !data.laserScanRaw().isEmpty()) + if(isFrame) { // Detect time jump in the past double clockNow = now().seconds(); @@ -688,7 +749,7 @@ void OdometryROS::processData() { groundTruth = rtabmap_conversions::getTransform(groundTruthFrameId_, groundTruthBaseFrameId_, header.stamp, *tfBuffer_, waitForTransform_); - if(!data.imageRaw().empty() || !data.laserScanRaw().isEmpty()) + if(isFrame) { // Use only XYZ to handle the case odometry was previously initialized with IMU, // we assume that the ground truth contains also a real initial orientation @@ -716,39 +777,6 @@ void OdometryROS::processData() } } - bool tooOldPreviousData = minUpdateRate_ > 0 && previousStamp_ > 0 && rtabmap_conversions::timestampFromROS(header.stamp)-previousStamp_ > 1.0/minUpdateRate_; - if(tooOldPreviousData) - { - RCLCPP_WARN(this->get_logger(), "Odometry lost! Odometry will be reset because last update " - "is %fs too old (>%fs, min_update_rate = %f Hz). Previous data stamp is %f while new data stamp is %f.", - rtabmap_conversions::timestampFromROS(header.stamp) - previousStamp_, 1.0/minUpdateRate_, minUpdateRate_, previousStamp_, rtabmap_conversions::timestampFromROS(header.stamp)); - - if(!guess_.isNull()) - { - RCLCPP_WARN(this->get_logger(), "Odometry automatically reset based on latest guess available from TF (%s->%s, moved %s since got lost)!", - guessFrameId_.c_str(), frameId_.c_str(), guess_.prettyPrint().c_str()); - odometry_->reset(odometry_->getPose() * guess_); - guess_.setNull(); - guessPreviousPose_.setNull(); - } - else - { - // Check TF to see if sensor fusion is used (e.g., the output of robot_localization) - Transform tfPose = rtabmap_conversions::getTransform(odomFrameId_, frameId_, header.stamp, *tfBuffer_, waitForTransform_); - if(tfPose.isNull()) - { - RCLCPP_WARN(this->get_logger(), "Odometry automatically reset to latest computed pose!"); - odometry_->reset(odometry_->getPose()); - } - else - { - RCLCPP_WARN(this->get_logger(), "Odometry automatically reset to latest odometry pose available from TF (%s->%s)!", - odomFrameId_.c_str(), frameId_.c_str()); - odometry_->reset(tfPose); - } - } - } - bool skipOdometryUpdate = false; rtabmap::Transform pose; @@ -756,23 +784,36 @@ void OdometryROS::processData() rtabmap::Transform guessVelocity; Transform guessCurrentPose; + // Whether the guess has a previous pose to be relative to, which decides how the pose + // is seeded from it further down. It is cleared by reset(), so a reset asked for + // through a service restarts at the guess frame while an automatic one continues from + // the pose it has just carried forward. + bool guessIsTheFirstOne = false; if(!guessFrameId_.empty()) { guessCurrentPose = rtabmap_conversions::getTransform(guessFrameId_, frameId_, header.stamp, *tfBuffer_, waitForTransform_); Transform previousPose = guessPreviousPose_; - if(guessPreviousPose_.isNull()) + guessIsTheFirstOne = guessPreviousPose_.isNull(); + if(guessIsTheFirstOne) { previousPose = guessCurrentPose; - if(!guessCurrentPose.isNull() && odometry_->getPose().isIdentity()) - { - RCLCPP_INFO(get_logger(), "Odometry: init pose with guess %s", guessCurrentPose.prettyPrint().c_str()); - odometry_->reset(guessCurrentPose); - } } if(!previousPose.isNull() && !guessCurrentPose.isNull()) { + // What the guess frame says the robot is doing. This is what gets published + // for a frame with no registration behind it -- one skipped for not having + // moved enough, or one starting a new map, whose twist would otherwise be + // unknown although its pose comes from the guess. It is dropped further down + // as soon as the registration has a velocity of its own to report. + if(previousStamp_ > 0.0 && + rtabmap_conversions::timestampFromROS(header.stamp) > previousStamp_) + { + guessVelocity = velocityFrom(previousPose.inverse() * guessCurrentPose, + rtabmap_conversions::timestampFromROS(header.stamp) - previousStamp_); + } + if(guess_.isNull()) { guess_ = previousPose.inverse() * guessCurrentPose; @@ -791,20 +832,7 @@ void OdometryROS::processData() { // Ignore odometry update, we didn't move enough pose = odometry_->getPose() * guess_; - info.reg.covariance = cv::Mat::zeros(6,6,CV_64FC1); - info.reg.covariance.at(0,0) = guessLinearVariance_; // xx - info.reg.covariance.at(1,1) = guessLinearVariance_; // yy - info.reg.covariance.at(2,2) = guessLinearVariance_; // zz - info.reg.covariance.at(3,3) = guessAngularVariance_; // rr - info.reg.covariance.at(4,4) = guessAngularVariance_; // pp - info.reg.covariance.at(5,5) = guessAngularVariance_; // yawyaw - //set velocity - double dt = rtabmap_conversions::timestampFromROS(header.stamp)-previousStamp_; - UASSERT(dt>0.0); - // use part of guess matching dt - (previousPose.inverse() * guessCurrentPose).getTranslationAndEulerAngles(x,y,z,roll,pitch,yaw); - guessVelocity = rtabmap::Transform(x/dt, y/dt, z/dt, roll/dt, pitch/dt, yaw/dt); skipOdometryUpdate = true; } } @@ -817,16 +845,117 @@ void OdometryROS::processData() } } + // Handled here rather than before the guess is computed: guess_ only holds the motion + // since the previous frame once the block above has run, and resetting without it + // throws away everything the guess source measured across the gap -- which is exactly + // what this reset is supposed to carry over. + bool tooOldPreviousData = minUpdateRate_ > 0 && previousStamp_ > 0 && rtabmap_conversions::timestampFromROS(header.stamp)-previousStamp_ > 1.0/minUpdateRate_; + if(tooOldPreviousData) + { + RCLCPP_WARN(this->get_logger(), "Odometry lost! Odometry will be reset because last update " + "is %fs too old (>%fs, min_update_rate = %f Hz). Previous data stamp is %f while new data stamp is %f.", + rtabmap_conversions::timestampFromROS(header.stamp) - previousStamp_, 1.0/minUpdateRate_, minUpdateRate_, previousStamp_, rtabmap_conversions::timestampFromROS(header.stamp)); + + if(!guess_.isNull()) + { + RCLCPP_WARN(this->get_logger(), "Odometry automatically reset based on latest guess available from TF (%s->%s, moved %s since got lost)!", + guessFrameId_.c_str(), frameId_.c_str(), guess_.prettyPrint().c_str()); + odometry_->reset(odometry_->getPose() * guess_); + // Cleared because it has just been applied: the odometry now starts from a + // pose that already includes it, and leaving it would have the registration + // apply it a second time on the frame that initialises the new map. + // guessPreviousPose_ is kept, so the next frame measures its motion from this + // one rather than starting over and losing a frame of it. + guess_.setNull(); + } + else + { + // Check TF to see if sensor fusion is used (e.g., the output of robot_localization) + Transform tfPose = rtabmap_conversions::getTransform(odomFrameId_, frameId_, header.stamp, *tfBuffer_, waitForTransform_); + if(tfPose.isNull()) + { + RCLCPP_WARN(this->get_logger(), "Odometry automatically reset to latest computed pose!"); + odometry_->reset(odometry_->getPose()); + } + else + { + RCLCPP_WARN(this->get_logger(), "Odometry automatically reset to latest odometry pose available from TF (%s->%s)!", + odomFrameId_.c_str(), frameId_.c_str()); + odometry_->reset(tfPose); + } + } + } + // process data rclcpp::Time timeStart = rclcpp::Clock().now(); if(!groundTruth.isNull()) { data.setGroundTruth(groundTruth); } + // Set when the guess has already been folded into the pose below, so that a reset + // later in this frame does not go looking for a fallback that is no longer needed. + bool poseCarriedByGuess = false; + // Set when this frame starts a new map and the guess frame says where, which is what + // makes the trajectory it starts continuous with the one before it. + bool initialisedOnGuess = false; if(!skipOdometryUpdate) { + // This frame will initialise the odometry's map whenever no frame has been + // registered since the last reset -- at startup, after a service reset, or on + // recovery from an automatic one. Registration then returns no motion, so the pose + // has to be put where the guess says the robot is *before* the frame is processed: + // afterwards the map is already anchored in the wrong place, and the next + // registration measures the difference against that anchor and takes the correction + // straight back out. Resetting here costs nothing, the map being empty either way. + // + // There are two ways to be right, depending on what the guess can say: + initialisedOnGuess = odometry_->framesProcessed() == 0 && !guessCurrentPose.isNull(); + if(initialisedOnGuess) + { + if(guessIsTheFirstOne) + { + // Nothing to be relative to. Adopt the guess source's own coordinates, so + // that odometry restarts where the guess says it is rather than at the + // origin. A pose asked for explicitly through reset_odom_to_pose is left + // alone: only an odometry still sitting at the identity is seeded this way. + if(odometry_->getPose().isIdentity()) + { + RCLCPP_INFO(get_logger(), "Odometry: init pose with guess %s", + guessCurrentPose.prettyPrint().c_str()); + odometry_->reset(guessCurrentPose); + } + } + else if(!guess_.isNull() && !guess_.isIdentity()) + { + // There is a previous guess pose, so the guess describes real motion since + // the frame before this one -- which an automatic reset has just carried the + // pose through. Advance by it and the trajectory stays continuous; drop it + // and the new map is anchored a frame behind, once per reset, accumulating. + RCLCPP_DEBUG(this->get_logger(), "Odometry: advancing the pose by the guess " + "(%s) before the map is initialised, so the motion measured since the " + "previous frame is not lost.", guess_.prettyPrint().c_str()); + odometry_->reset(odometry_->getPose() * guess_); + guess_.setNull(); + poseCarriedByGuess = true; + } + } pose = odometry_->process(data, guess_, &info); } + + // 9999 on both covariances is how rtabmap is told a frame starts a new map. When the + // guess frame says where it starts, and publish_null_when_lost says this consumer + // wants poses rather than the news of a reset, it goes out as a continuation instead. + const bool publishAsContinuation = initialisedOnGuess && !publishNullWhenLost_ && !pose.isNull(); + if(skipOdometryUpdate || publishAsContinuation) + { + // Both rest on the guess rather than on a registration: its confidence, its velocity. + info.reg.covariance = guessCovariance(guessLinearVariance_, guessAngularVariance_); + } + else + { + // The registration measured this one, so its velocity is the one to publish. + guessVelocity.setNull(); + } if(!pose.isNull()) { if(!skipOdometryUpdate) { @@ -909,11 +1038,12 @@ void OdometryROS::processData() if(setTwist) { float x,y,z,roll,pitch,yaw; - if(skipOdometryUpdate) { - UASSERT(!guessVelocity.isNull()); - guessVelocity.getTranslationAndEulerAngles(x,y,z,roll,pitch,yaw); - } else { + // Whatever is left of the two: the registration's own velocity, or the + // guess's where the frame had no registration to give one. + if(guessVelocity.isNull()) { odometry_->getVelocityGuess().getTranslationAndEulerAngles(x,y,z,roll,pitch,yaw); + } else { + guessVelocity.getTranslationAndEulerAngles(x,y,z,roll,pitch,yaw); } odom.twist.twist.linear.x = x; odom.twist.twist.linear.y = y; @@ -931,7 +1061,7 @@ void OdometryROS::processData() odom.twist.covariance.at(35) = setTwist?info.reg.covariance.at(5,5):BAD_COVARIANCE; // yawyaw //publish the message - if(setTwist || publishNullWhenLost_) + if(setTwist || publishNullWhenLost_ || publishAsContinuation) { odomPub_->publish(odom); } @@ -953,7 +1083,7 @@ void OdometryROS::processData() cloud.push_back(pt); } sensor_msgs::msg::PointCloud2 cloudMsg; - pcl::toROSMsg(cloud, cloudMsg); + rtabmap_conversions::toPointCloud2Msg(cloud, cloudMsg); cloudMsg.header.stamp = header.stamp; // use corresponding time stamp to image cloudMsg.header.frame_id = odomFrameId_; odomLocalMap_->publish(cloudMsg); @@ -976,7 +1106,7 @@ void OdometryROS::processData() } sensor_msgs::msg::PointCloud2 cloudMsg; - pcl::toROSMsg(cloud, cloudMsg); + rtabmap_conversions::toPointCloud2Msg(cloud, cloudMsg); cloudMsg.header.stamp = header.stamp; // use corresponding time stamp to image cloudMsg.header.frame_id = odomFrameId_; odomLastFrame_->publish(cloudMsg); @@ -996,7 +1126,7 @@ void OdometryROS::processData() cloud.push_back(pcl::PointXYZ(pt.x, pt.y, pt.z)); } sensor_msgs::msg::PointCloud2 cloudMsg; - pcl::toROSMsg(cloud, cloudMsg); + rtabmap_conversions::toPointCloud2Msg(cloud, cloudMsg); cloudMsg.header.stamp = header.stamp; // use corresponding time stamp to image cloudMsg.header.frame_id = odomFrameId_; odomLastFrame_->publish(cloudMsg); @@ -1010,22 +1140,22 @@ void OdometryROS::processData() if(info.localScanMap.hasNormals() && info.localScanMap.hasIntensity()) { pcl::PointCloud::Ptr cloud = util3d::laserScanToPointCloudINormal(info.localScanMap, info.localScanMap.localTransform()); - pcl::toROSMsg(*cloud, cloudMsg); + rtabmap_conversions::toPointCloud2Msg(*cloud, cloudMsg); } else if(info.localScanMap.hasNormals()) { pcl::PointCloud::Ptr cloud = util3d::laserScanToPointCloudNormal(info.localScanMap, info.localScanMap.localTransform()); - pcl::toROSMsg(*cloud, cloudMsg); + rtabmap_conversions::toPointCloud2Msg(*cloud, cloudMsg); } else if(info.localScanMap.hasIntensity()) { pcl::PointCloud::Ptr cloud = util3d::laserScanToPointCloudI(info.localScanMap, info.localScanMap.localTransform()); - pcl::toROSMsg(*cloud, cloudMsg); + rtabmap_conversions::toPointCloud2Msg(*cloud, cloudMsg); } else { pcl::PointCloud::Ptr cloud = util3d::laserScanToPointCloud(info.localScanMap, info.localScanMap.localTransform()); - pcl::toROSMsg(*cloud, cloudMsg); + rtabmap_conversions::toPointCloud2Msg(*cloud, cloudMsg); } cloudMsg.header.stamp = header.stamp; // use corresponding time stamp to image @@ -1106,6 +1236,17 @@ void OdometryROS::processData() odometry_->reset(odometry_->getPose() * guess_); guess_.setNull(); } + else if(poseCarriedByGuess) + { + // The guess was folded into the pose before this frame was processed, so the + // pose already covers the motion since the last one. Going to TF for a + // fallback here would block for wait_for_transform on every lost frame and + // answer a question that has already been answered. + RCLCPP_WARN(this->get_logger(), "Odometry automatically reset, carrying the " + "latest guess from TF (%s->%s) that was already applied to the pose!", + guessFrameId_.c_str(), frameId_.c_str()); + odometry_->reset(odometry_->getPose()); + } else { // Check TF to see if sensor fusion is used (e.g., the output of robot_localization) @@ -1319,6 +1460,11 @@ void OdometryROS::reset(const Transform & pose) UScopeMutex lock(dataMutex_); odometry_->reset(pose); guess_.setNull(); + // Clearing this is what tells the next frame to restart from the guess frame rather + // than continue from here: the seeding step below cannot tell a reset asked for + // through a service from one the node decided on its own, and reads this instead. The + // automatic resets deliberately leave it alone, so that they carry on from the pose + // they just moved. guessPreviousPose_.setNull(); previousStamp_ = 0.0; previousClockTime_ = 0.0; diff --git a/rtabmap_odom/src/nodelets/icp_odometry.cpp b/rtabmap_odom/src/nodelets/icp_odometry.cpp index 2281e7fb..f67453c8 100644 --- a/rtabmap_odom/src/nodelets/icp_odometry.cpp +++ b/rtabmap_odom/src/nodelets/icp_odometry.cpp @@ -25,6 +25,7 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +#include #include #include @@ -69,6 +70,7 @@ ICPOdometry::ICPOdometry(const rclcpp::NodeOptions & options) : ICPOdometry::~ICPOdometry() { + this->join(true); } void ICPOdometry::onOdomInit() @@ -399,12 +401,12 @@ void ICPOdometry::callbackScan(const sensor_msgs::msg::LaserScan::SharedPtr scan if(hasIntensity) { - pcl::fromROSMsg(scanOut, *pclScanI); + rtabmap_conversions::fromPointCloud2Msg(scanOut, *pclScanI); pclScanI->is_dense = true; } else { - pcl::fromROSMsg(scanOut, *pclScan); + rtabmap_conversions::fromPointCloud2Msg(scanOut, *pclScan); pclScan->is_dense = true; } @@ -519,7 +521,7 @@ void ICPOdometry::callbackScan(const sensor_msgs::msg::LaserScan::SharedPtr scan void ICPOdometry::callbackCloud(const sensor_msgs::msg::PointCloud2::SharedPtr pointCloudMsg) { UASSERT_MSG(pointCloudMsg->data.size() == pointCloudMsg->row_step*pointCloudMsg->height, - uFormat("data=%d row_step=%d height=%d", pointCloudMsg->data.size(), pointCloudMsg->row_step, pointCloudMsg->height).c_str()); + uFormat("data=%d row_step=%d height=%d", (int)pointCloudMsg->data.size(), (int)pointCloudMsg->row_step, (int)pointCloudMsg->height).c_str()); if(scanReceived_) { @@ -668,7 +670,7 @@ void ICPOdometry::callbackCloud(const sensor_msgs::msg::PointCloud2::SharedPtr p if(hasNormals && hasIntensity) { pcl::PointCloud::Ptr pclScan(new pcl::PointCloud); - pcl::fromROSMsg(*cloudMsg, *pclScan); + rtabmap_conversions::fromPointCloud2Msg(*cloudMsg, *pclScan); if(pclScan->size() && scanDownsamplingStep_ > 1) { pclScan = util3d::downsample(pclScan, scanDownsamplingStep_); @@ -686,7 +688,7 @@ void ICPOdometry::callbackCloud(const sensor_msgs::msg::PointCloud2::SharedPtr p else if(hasNormals) { pcl::PointCloud::Ptr pclScan(new pcl::PointCloud); - pcl::fromROSMsg(*cloudMsg, *pclScan); + rtabmap_conversions::fromPointCloud2Msg(*cloudMsg, *pclScan); if(pclScan->size() && scanDownsamplingStep_ > 1) { pclScan = util3d::downsample(pclScan, scanDownsamplingStep_); @@ -704,7 +706,7 @@ void ICPOdometry::callbackCloud(const sensor_msgs::msg::PointCloud2::SharedPtr p else if(hasIntensity) { pcl::PointCloud::Ptr pclScan(new pcl::PointCloud); - pcl::fromROSMsg(*cloudMsg, *pclScan); + rtabmap_conversions::fromPointCloud2Msg(*cloudMsg, *pclScan); if(pclScan->size() && scanDownsamplingStep_ > 1) { pclScan = util3d::downsample(pclScan, scanDownsamplingStep_); @@ -750,7 +752,7 @@ void ICPOdometry::callbackCloud(const sensor_msgs::msg::PointCloud2::SharedPtr p else { pcl::PointCloud::Ptr pclScan(new pcl::PointCloud); - pcl::fromROSMsg(*cloudMsg, *pclScan); + rtabmap_conversions::fromPointCloud2Msg(*cloudMsg, *pclScan); if(pclScan->size() && scanDownsamplingStep_ > 1) { pclScan = util3d::downsample(pclScan, scanDownsamplingStep_); diff --git a/rtabmap_odom/src/nodelets/rgbd_odometry.cpp b/rtabmap_odom/src/nodelets/rgbd_odometry.cpp index 97d48f6e..5f43acab 100644 --- a/rtabmap_odom/src/nodelets/rgbd_odometry.cpp +++ b/rtabmap_odom/src/nodelets/rgbd_odometry.cpp @@ -38,6 +38,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "rtabmap_conversions/MsgConversion.h" #include +#include #include #include #include @@ -73,6 +74,8 @@ RGBDOdometry::RGBDOdometry(const rclcpp::NodeOptions & options) : RGBDOdometry::~RGBDOdometry() { + this->join(true); + delete approxSync_; delete exactSync_; delete approxSync2_; @@ -435,40 +438,58 @@ void RGBDOdometry::updateParameters(ParametersMap & parameters) void RGBDOdometry::commonCallback( const std::vector & rgbImages, const std::vector & depthImages, - const std::vector& cameraInfos) + const std::vector& cameraInfos, + const std::vector > & localKeyPointsMsgs, + const std::vector > & localPoints3dMsgs, + const std::vector & localDescriptorsMsgs) { UASSERT(rgbImages.size() > 0 && rgbImages.size() == depthImages.size() && rgbImages.size() == cameraInfos.size()); rclcpp::Time higherStamp; UASSERT_MSG(rgbImages[0], "RGB image is null!"); - int imageWidth = rgbImages[0]->image.cols; - int imageHeight = rgbImages[0]->image.rows; UASSERT_MSG(depthImages[0], "Depth image is null!"); + + // The images are what the local features would otherwise be extracted from, so a frame + // that brings its own can leave them out -- which is nearly all of the bandwidth. It + // then describes itself with its calibration alone: how big the image would have been, + // where the camera is, what it sees. (An RGB-D message with no depth image at all also + // used to divide by zero below.) + const bool hasRgb = !rgbImages[0]->image.empty(); + const bool hasDepth = !depthImages[0]->image.empty(); + + int imageWidth = hasRgb?rgbImages[0]->image.cols:(int)cameraInfos[0].width; + int imageHeight = hasRgb?rgbImages[0]->image.rows:(int)cameraInfos[0].height; int depthWidth = depthImages[0]->image.cols; int depthHeight = depthImages[0]->image.rows; - UASSERT_MSG( - imageWidth/depthWidth == imageHeight/depthHeight, - uFormat("rgb=%dx%d depth=%dx%d", imageWidth, imageHeight, depthWidth, depthHeight).c_str()); + if(hasDepth) + { + UASSERT_MSG( + imageWidth/depthWidth == imageHeight/depthHeight, + uFormat("rgb=%dx%d depth=%dx%d", imageWidth, imageHeight, depthWidth, depthHeight).c_str()); + } int cameraCount = rgbImages.size(); cv::Mat rgb; cv::Mat depth; std::vector cameraModels; + std::vector keypoints; + std::vector points3d; + cv::Mat descriptors; for(unsigned int i=0; iencoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) ==0 || + if((hasRgb && !(rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) ==0 || rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) ==0 || rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) ==0 || rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::BGR8) == 0 || rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::RGB8) == 0 || rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::BGRA8) == 0 || rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::RGBA8) == 0 || - rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::BAYER_GRBG8) == 0) || - !(depthImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_16UC1) == 0 || + rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::BAYER_GRBG8) == 0)) || + (hasDepth && !(depthImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_16UC1) == 0 || depthImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_32FC1) == 0 || - depthImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) == 0)) + depthImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) == 0))) { RCLCPP_ERROR(this->get_logger(), "Input type must be image=mono8,mono16,rgb8,bgr8,bgra8,rgba8 and " "image_depth=32FC1,16UC1,mono16. Current rgb=%s and depth=%s", @@ -476,20 +497,33 @@ void RGBDOdometry::commonCallback( depthImages[i]->encoding.c_str()); return; } - UASSERT_MSG(rgbImages[i]->image.cols == imageWidth && rgbImages[i]->image.rows == imageHeight, - uFormat("imageWidth=%d vs %d imageHeight=%d vs %d", - imageWidth, - rgbImages[i]->image.cols, - imageHeight, - rgbImages[i]->image.rows).c_str()); - UASSERT_MSG(depthImages[i]->image.cols == depthWidth && depthImages[i]->image.rows == depthHeight, - uFormat("depthWidth=%d vs %d depthHeight=%d vs %d", - depthWidth, - depthImages[i]->image.cols, - depthHeight, - depthImages[i]->image.rows).c_str()); + if(hasRgb) + { + UASSERT_MSG(rgbImages[i]->image.cols == imageWidth && rgbImages[i]->image.rows == imageHeight, + uFormat("imageWidth=%d vs %d imageHeight=%d vs %d", + imageWidth, + rgbImages[i]->image.cols, + imageHeight, + rgbImages[i]->image.rows).c_str()); + } + if(hasDepth) + { + UASSERT_MSG(depthImages[i]->image.cols == depthWidth && depthImages[i]->image.rows == depthHeight, + uFormat("depthWidth=%d vs %d depthHeight=%d vs %d", + depthWidth, + depthImages[i]->image.cols, + depthHeight, + depthImages[i]->image.rows).c_str()); + } - rclcpp::Time stamp = rtabmap_conversions::timestampFromROS(rgbImages[i]->header.stamp)>rtabmap_conversions::timestampFromROS(depthImages[i]->header.stamp)?rgbImages[i]->header.stamp:depthImages[i]->header.stamp; + // An image that is not there carries no header either, so a frame that has none is + // stamped and placed by its calibration, which is all it has. + const std::string & cameraFrameId = hasRgb?rgbImages[i]->header.frame_id:cameraInfos[i].header.frame_id; + rclcpp::Time stamp = cameraInfos[i].header.stamp; + if(hasRgb || hasDepth) + { + stamp = rtabmap_conversions::timestampFromROS(rgbImages[i]->header.stamp)>rtabmap_conversions::timestampFromROS(depthImages[i]->header.stamp)?rgbImages[i]->header.stamp:depthImages[i]->header.stamp; + } if(i == 0) { @@ -500,7 +534,7 @@ void RGBDOdometry::commonCallback( higherStamp = stamp; } - Transform localTransform = rtabmap_conversions::getTransform(this->frameId(), rgbImages[i]->header.frame_id, stamp, tfBuffer(), waitForTransform()); + Transform localTransform = rtabmap_conversions::getTransform(this->frameId(), cameraFrameId, stamp, tfBuffer(), waitForTransform()); if(localTransform.isNull()) { return; @@ -528,53 +562,75 @@ void RGBDOdometry::commonCallback( } } - cv_bridge::CvImageConstPtr ptrImage = rgbImages[i]; - if(rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) !=0 && - rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) != 0) + if(hasRgb) { - if(keepColor_ && rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) != 0) + cv_bridge::CvImageConstPtr ptrImage = rgbImages[i]; + if(rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) !=0 && + rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) != 0) { - ptrImage = cv_bridge::cvtColor(rgbImages[i], "bgr8"); + if(keepColor_ && rgbImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) != 0) + { + ptrImage = cv_bridge::cvtColor(rgbImages[i], "bgr8"); + } + else + { + ptrImage = cv_bridge::cvtColor(rgbImages[i], "mono8"); + } + } + + // initialize + if(rgb.empty()) + { + rgb = cv::Mat(imageHeight, imageWidth*cameraCount, ptrImage->image.type()); + } + + if(ptrImage->image.type() == rgb.type()) + { + ptrImage->image.copyTo(cv::Mat(rgb, cv::Rect(i*imageWidth, 0, imageWidth, imageHeight))); } else { - ptrImage = cv_bridge::cvtColor(rgbImages[i], "mono8"); + RCLCPP_ERROR(this->get_logger(), "Some RGB images are not the same type! %d vs %d", ptrImage->image.type(), rgb.type()); + return; } } - cv_bridge::CvImageConstPtr ptrDepth = depthImages[i]; + if(hasDepth) + { + cv_bridge::CvImageConstPtr ptrDepth = depthImages[i]; + if(depth.empty()) + { + depth = cv::Mat(depthHeight, depthWidth*cameraCount, ptrDepth->image.type()); + } - // initialize - if(rgb.empty()) - { - rgb = cv::Mat(imageHeight, imageWidth*cameraCount, ptrImage->image.type()); - } - if(depth.empty()) - { - depth = cv::Mat(depthHeight, depthWidth*cameraCount, ptrDepth->image.type()); - } - - if(ptrImage->image.type() == rgb.type()) - { - ptrImage->image.copyTo(cv::Mat(rgb, cv::Rect(i*imageWidth, 0, imageWidth, imageHeight))); - } - else - { - RCLCPP_ERROR(this->get_logger(), "Some RGB images are not the same type! %d vs %d", ptrImage->image.type(), rgb.type()); - return; - } - - if(ptrDepth->image.type() == depth.type()) - { - ptrDepth->image.copyTo(cv::Mat(depth, cv::Rect(i*depthWidth, 0, depthWidth, depthHeight))); - } - else - { - RCLCPP_ERROR(this->get_logger(), "Some Depth images are not the same type! %d vs %d", ptrDepth->image.type(), depth.type()); - return; + if(ptrDepth->image.type() == depth.type()) + { + ptrDepth->image.copyTo(cv::Mat(depth, cv::Rect(i*depthWidth, 0, depthWidth, depthHeight))); + } + else + { + RCLCPP_ERROR(this->get_logger(), "Some Depth images are not the same type! %d vs %d", ptrDepth->image.type(), depth.type()); + return; + } } cameraModels.push_back(rtabmap_conversions::cameraModelFromROS(cameraInfos[i], localTransform)); + + // The images of all cameras are stitched side by side above, so the keypoints of + // camera i are shifted by as many images as come before it, and their 3D points, + // which arrive in that camera's optical frame, are brought back to the base frame. + if(localKeyPointsMsgs.size() == rgbImages.size()) + { + rtabmap_conversions::keypointsFromROS(localKeyPointsMsgs[i], keypoints, imageWidth*i); + } + if(localPoints3dMsgs.size() == rgbImages.size()) + { + rtabmap_conversions::points3fFromROS(localPoints3dMsgs[i], points3d, localTransform); + } + if(localDescriptorsMsgs.size() == rgbImages.size()) + { + descriptors.push_back(localDescriptorsMsgs[i]); + } } rtabmap::SensorData data( @@ -584,9 +640,30 @@ void RGBDOdometry::commonCallback( 0, rtabmap_conversions::timestampFromROS(higherStamp)); + // Features that came with the frame are used as they are: the odometry then skips + // detection, description and the depth lookup that would otherwise rebuild them + // (see RegistrationVis, which extracts only when the frame carries no keypoints). + // They are dropped rather than trusted if the three of them disagree, as using them + // out of step would silently mismatch keypoints with their descriptors or 3D points. + if(!keypoints.empty()) + { + if((!points3d.empty() && points3d.size() != keypoints.size()) || + (!descriptors.empty() && descriptors.rows != (int)keypoints.size())) + { + RCLCPP_ERROR(this->get_logger(), "Ignoring the local features received with this frame: " + "%d keypoints, %d 3D points and %d descriptors, which should be the same count " + "(or none at all for the 3D points and the descriptors).", + (int)keypoints.size(), (int)points3d.size(), descriptors.rows); + } + else + { + data.setFeatures(keypoints, points3d, descriptors); + } + } + std_msgs::msg::Header header; header.stamp = higherStamp; - header.frame_id = rgbImages.size()==1?rgbImages[0]->header.frame_id:""; + header.frame_id = rgbImages.size()==1?(hasRgb?rgbImages[0]->header.frame_id:cameraInfos[0].header.frame_id):""; this->processData(data, header); } @@ -627,6 +704,27 @@ void RGBDOdometry::callback( } } +namespace { + +/** + * @brief Collects the local features one camera's image carries, cameras in order. + * + * An image that carries none pushes empty entries rather than nothing, so that the + * per-camera indexing still lines up with the images. + */ +void appendLocalFeatures( + const rtabmap_msgs::msg::RGBDImage & image, + std::vector > & keyPoints, + std::vector > & points3d, + std::vector & descriptors) +{ + keyPoints.push_back(image.key_points); + points3d.push_back(image.points); + descriptors.push_back(rtabmap::uncompressData(image.descriptors)); +} + +} // namespace + void RGBDOdometry::callbackRGBDX( const rtabmap_msgs::msg::RGBDImages::ConstSharedPtr images) { @@ -642,13 +740,17 @@ void RGBDOdometry::callbackRGBDX( std::vector imageMsgs(images->rgbd_images.size()); std::vector depthMsgs(images->rgbd_images.size()); std::vector infoMsgs; + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; for(size_t i=0; irgbd_images.size(); ++i) { rtabmap_conversions::toCvShare(images->rgbd_images[i], images, imageMsgs[i], depthMsgs[i]); infoMsgs.push_back(images->rgbd_images[i].rgb_camera_info); + appendLocalFeatures(images->rgbd_images[i], localKeyPoints, localPoints3d, localDescriptors); } - this->commonCallback(imageMsgs, depthMsgs, infoMsgs); + this->commonCallback(imageMsgs, depthMsgs, infoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -665,7 +767,12 @@ void RGBDOdometry::callbackRGBD( rtabmap_conversions::toCvShare(image, imageMsgs[0], depthMsgs[0]); infoMsgs.push_back(image->rgb_camera_info); - this->commonCallback(imageMsgs, depthMsgs, infoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(imageMsgs, depthMsgs, infoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -685,7 +792,13 @@ void RGBDOdometry::callbackRGBD2( infoMsgs.push_back(image->rgb_camera_info); infoMsgs.push_back(image2->rgb_camera_info); - this->commonCallback(imageMsgs, depthMsgs, infoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(imageMsgs, depthMsgs, infoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -708,7 +821,14 @@ void RGBDOdometry::callbackRGBD3( infoMsgs.push_back(image2->rgb_camera_info); infoMsgs.push_back(image3->rgb_camera_info); - this->commonCallback(imageMsgs, depthMsgs, infoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(imageMsgs, depthMsgs, infoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -734,7 +854,15 @@ void RGBDOdometry::callbackRGBD4( infoMsgs.push_back(image3->rgb_camera_info); infoMsgs.push_back(image4->rgb_camera_info); - this->commonCallback(imageMsgs, depthMsgs, infoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image4, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(imageMsgs, depthMsgs, infoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -763,7 +891,16 @@ void RGBDOdometry::callbackRGBD5( infoMsgs.push_back(image4->rgb_camera_info); infoMsgs.push_back(image5->rgb_camera_info); - this->commonCallback(imageMsgs, depthMsgs, infoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image4, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image5, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(imageMsgs, depthMsgs, infoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -795,7 +932,17 @@ void RGBDOdometry::callbackRGBD6( infoMsgs.push_back(image5->rgb_camera_info); infoMsgs.push_back(image6->rgb_camera_info); - this->commonCallback(imageMsgs, depthMsgs, infoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image4, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image5, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image6, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(imageMsgs, depthMsgs, infoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } diff --git a/rtabmap_odom/src/nodelets/stereo_odometry.cpp b/rtabmap_odom/src/nodelets/stereo_odometry.cpp index f56b2cc3..72cd9cb0 100644 --- a/rtabmap_odom/src/nodelets/stereo_odometry.cpp +++ b/rtabmap_odom/src/nodelets/stereo_odometry.cpp @@ -42,6 +42,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include #include #include +#include #include using namespace rtabmap; @@ -73,6 +74,8 @@ StereoOdometry::StereoOdometry(const rclcpp::NodeOptions & options) : StereoOdometry::~StereoOdometry() { + this->join(true); + delete approxSync_; delete exactSync_; delete approxSync2_; @@ -407,29 +410,46 @@ void StereoOdometry::commonCallback( const std::vector & leftImages, const std::vector & rightImages, const std::vector& leftCameraInfos, - const std::vector& rightCameraInfos) + const std::vector& rightCameraInfos, + const std::vector > & localKeyPointsMsgs, + const std::vector > & localPoints3dMsgs, + const std::vector & localDescriptorsMsgs) { UASSERT(leftImages.size() > 0 && leftImages.size() == rightImages.size() && leftImages.size() == leftCameraInfos.size() && rightImages.size() == rightCameraInfos.size()); rclcpp::Time higherStamp; - int leftWidth = leftImages[0]->image.cols; - int leftHeight = leftImages[0]->image.rows; + + // The images are what the local features would otherwise be extracted from, so a frame + // that brings its own can leave them out -- which is nearly all of the bandwidth. It + // then describes itself with its calibration alone: how big the left image would have + // been, where the rig is, how far apart the two cameras are. + const bool hasImages = !leftImages[0]->image.empty() && !rightImages[0]->image.empty(); + + int leftWidth = hasImages?leftImages[0]->image.cols:(int)leftCameraInfos[0].width; + int leftHeight = hasImages?leftImages[0]->image.rows:(int)leftCameraInfos[0].height; int rightWidth = rightImages[0]->image.cols; int rightHeight = rightImages[0]->image.rows; - UASSERT_MSG( - leftWidth == rightWidth && leftHeight == rightHeight, - uFormat("left=%dx%d right=%dx%d", leftWidth, leftHeight, rightWidth, rightHeight).c_str()); + if(hasImages) + { + UASSERT_MSG( + leftWidth == rightWidth && leftHeight == rightHeight, + uFormat("left=%dx%d right=%dx%d", leftWidth, leftHeight, rightWidth, rightHeight).c_str()); + } int cameraCount = leftImages.size(); cv::Mat left; cv::Mat right; std::vector cameraModels; + std::vector keypoints; + std::vector points3d; + cv::Mat descriptors; for(unsigned int i=0; iencoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) ==0 || + if(hasImages && + (!(leftImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) ==0 || leftImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) ==0 || leftImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) ==0 || leftImages[i]->encoding.compare(sensor_msgs::image_encodings::BGR8) == 0 || @@ -442,14 +462,21 @@ void StereoOdometry::commonCallback( rightImages[i]->encoding.compare(sensor_msgs::image_encodings::BGR8) == 0 || rightImages[i]->encoding.compare(sensor_msgs::image_encodings::RGB8) == 0 || rightImages[i]->encoding.compare(sensor_msgs::image_encodings::BGRA8) == 0 || - rightImages[i]->encoding.compare(sensor_msgs::image_encodings::RGBA8) == 0)) + rightImages[i]->encoding.compare(sensor_msgs::image_encodings::RGBA8) == 0))) { RCLCPP_ERROR(this->get_logger(), "Input type must be image=mono8,mono16,rgb8,bgr8,rgba8,bgra8 (mono8 recommended), received types are %s (left) and %s (right)", leftImages[i]->encoding.c_str(), rightImages[i]->encoding.c_str()); return; } - rclcpp::Time stamp = rtabmap_conversions::timestampFromROS(leftImages[i]->header.stamp)>rtabmap_conversions::timestampFromROS(rightImages[i]->header.stamp)?leftImages[i]->header.stamp:rightImages[i]->header.stamp; + // An image that is not there carries no header either, so a frame that has none is + // stamped and placed by its calibration, which is all it has. + const std::string & cameraFrameId = hasImages?leftImages[i]->header.frame_id:leftCameraInfos[i].header.frame_id; + rclcpp::Time stamp = leftCameraInfos[i].header.stamp; + if(hasImages) + { + stamp = rtabmap_conversions::timestampFromROS(leftImages[i]->header.stamp)>rtabmap_conversions::timestampFromROS(rightImages[i]->header.stamp)?leftImages[i]->header.stamp:rightImages[i]->header.stamp; + } if(i == 0) { @@ -460,7 +487,7 @@ void StereoOdometry::commonCallback( higherStamp = stamp; } - Transform localTransform = rtabmap_conversions::getTransform(this->frameId(), leftImages[i]->header.frame_id, stamp, tfBuffer(), waitForTransform()); + Transform localTransform = rtabmap_conversions::getTransform(this->frameId(), cameraFrameId, stamp, tfBuffer(), waitForTransform()); if(localTransform.isNull()) { return; @@ -488,7 +515,13 @@ void StereoOdometry::commonCallback( } } - if(!leftImages[i]->image.empty() && !rightImages[i]->image.empty()) + if(hasImages != (!leftImages[i]->image.empty() && !rightImages[i]->image.empty())) + { + RCLCPP_ERROR(this->get_logger(), "Odom: camera %d of this frame has images while " + "another one doesn't (or the other way around)?!?", i); + return; + } + { bool alreadyRectified = true; Parameters::parse(parameters(), Parameters::kRtabmapImagesAlreadyRectified(), alreadyRectified); @@ -602,62 +635,76 @@ void StereoOdometry::commonCallback( shown = true; } } - cv_bridge::CvImageConstPtr ptrLeft = leftImages[i]; - if(leftImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) !=0 && - leftImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) != 0) + if(hasImages) { - if(keepColor_ && leftImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) != 0) + cv_bridge::CvImageConstPtr ptrLeft = leftImages[i]; + if(leftImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) !=0 && + leftImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) != 0) { - ptrLeft = cv_bridge::cvtColor(leftImages[i], "bgr8"); + if(keepColor_ && leftImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO16) != 0) + { + ptrLeft = cv_bridge::cvtColor(leftImages[i], "bgr8"); + } + else + { + ptrLeft = cv_bridge::cvtColor(leftImages[i], "mono8"); + } + } + cv_bridge::CvImageConstPtr ptrRight = rightImages[i]; + if(rightImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) !=0 && + rightImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) != 0) + { + ptrRight = cv_bridge::cvtColor(rightImages[i], "mono8"); + } + + // initialize + if(left.empty()) + { + left = cv::Mat(leftHeight, leftWidth*cameraCount, ptrLeft->image.type()); + } + if(right.empty()) + { + right = cv::Mat(rightHeight, rightWidth*cameraCount, ptrRight->image.type()); + } + + if(ptrLeft->image.type() == left.type()) + { + ptrLeft->image.copyTo(cv::Mat(left, cv::Rect(i*leftWidth, 0, leftWidth, leftHeight))); } else { - ptrLeft = cv_bridge::cvtColor(leftImages[i], "mono8"); + RCLCPP_ERROR(this->get_logger(), "Some left images are not the same type! %d vs %d", ptrLeft->image.type(), left.type()); + return; } - } - cv_bridge::CvImageConstPtr ptrRight = rightImages[i]; - if(rightImages[i]->encoding.compare(sensor_msgs::image_encodings::TYPE_8UC1) !=0 && - rightImages[i]->encoding.compare(sensor_msgs::image_encodings::MONO8) != 0) - { - ptrRight = cv_bridge::cvtColor(rightImages[i], "mono8"); - } - // initialize - if(left.empty()) - { - left = cv::Mat(leftHeight, leftWidth*cameraCount, ptrLeft->image.type()); - } - if(right.empty()) - { - right = cv::Mat(rightHeight, rightWidth*cameraCount, ptrRight->image.type()); - } - - if(ptrLeft->image.type() == left.type()) - { - ptrLeft->image.copyTo(cv::Mat(left, cv::Rect(i*leftWidth, 0, leftWidth, leftHeight))); - } - else - { - RCLCPP_ERROR(this->get_logger(), "Some left images are not the same type! %d vs %d", ptrLeft->image.type(), left.type()); - return; - } - - if(ptrRight->image.type() == right.type()) - { - ptrRight->image.copyTo(cv::Mat(right, cv::Rect(i*rightWidth, 0, rightWidth, rightHeight))); - } - else - { - RCLCPP_ERROR(this->get_logger(), "Some right images are not the same type! %d vs %d", ptrRight->image.type(), right.type()); - return; + if(ptrRight->image.type() == right.type()) + { + ptrRight->image.copyTo(cv::Mat(right, cv::Rect(i*rightWidth, 0, rightWidth, rightHeight))); + } + else + { + RCLCPP_ERROR(this->get_logger(), "Some right images are not the same type! %d vs %d", ptrRight->image.type(), right.type()); + return; + } } cameraModels.push_back(stereoModel); } - else + + // The left images of all cameras are stitched side by side above, so the keypoints + // of camera i are shifted by as many images as come before it, and their 3D points, + // which arrive in that camera's optical frame, are brought back to the base frame. + if(localKeyPointsMsgs.size() == leftImages.size()) { - RCLCPP_ERROR(this->get_logger(), "Odom: input images empty?!?"); - return; + rtabmap_conversions::keypointsFromROS(localKeyPointsMsgs[i], keypoints, leftWidth*i); + } + if(localPoints3dMsgs.size() == leftImages.size()) + { + rtabmap_conversions::points3fFromROS(localPoints3dMsgs[i], points3d, localTransform); + } + if(localDescriptorsMsgs.size() == leftImages.size()) + { + descriptors.push_back(localDescriptorsMsgs[i]); } } @@ -669,9 +716,30 @@ void StereoOdometry::commonCallback( 0, rtabmap_conversions::timestampFromROS(higherStamp)); + // Features that came with the frame are used as they are: the odometry then skips + // detection, description and the disparity search that would otherwise rebuild them + // (see RegistrationVis, which extracts only when the frame carries no keypoints). + // They are dropped rather than trusted if the three of them disagree, as using them + // out of step would silently mismatch keypoints with their descriptors or 3D points. + if(!keypoints.empty()) + { + if((!points3d.empty() && points3d.size() != keypoints.size()) || + (!descriptors.empty() && descriptors.rows != (int)keypoints.size())) + { + RCLCPP_ERROR(this->get_logger(), "Ignoring the local features received with this frame: " + "%d keypoints, %d 3D points and %d descriptors, which should be the same count " + "(or none at all for the 3D points and the descriptors).", + (int)keypoints.size(), (int)points3d.size(), descriptors.rows); + } + else + { + data.setFeatures(keypoints, points3d, descriptors); + } + } + std_msgs::msg::Header header; header.stamp = higherStamp; - header.frame_id = leftImages.size()==1?leftImages[0]->header.frame_id:""; + header.frame_id = leftImages.size()==1?(hasImages?leftImages[0]->header.frame_id:leftCameraInfos[0].header.frame_id):""; this->processData(data, header); } @@ -715,6 +783,27 @@ void StereoOdometry::callback( } } +namespace { + +/** + * @brief Collects the local features one camera's frame carries, cameras in order. + * + * A frame that carries none pushes empty entries rather than nothing, so that the + * per-camera indexing still lines up with the images. + */ +void appendLocalFeatures( + const rtabmap_msgs::msg::RGBDImage & image, + std::vector > & keyPoints, + std::vector > & points3d, + std::vector & descriptors) +{ + keyPoints.push_back(image.key_points); + points3d.push_back(image.points); + descriptors.push_back(rtabmap::uncompressData(image.descriptors)); +} + +} // namespace + void StereoOdometry::callbackRGBD( const rtabmap_msgs::msg::RGBDImage::ConstSharedPtr image) { @@ -730,7 +819,12 @@ void StereoOdometry::callbackRGBD( leftInfoMsgs.push_back(image->rgb_camera_info); rightInfoMsgs.push_back(image->depth_camera_info); - this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -750,14 +844,18 @@ void StereoOdometry::callbackRGBDX( std::vector rightMsgs(images->rgbd_images.size()); std::vector leftInfoMsgs; std::vector rightInfoMsgs; + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; for(size_t i=0; irgbd_images.size(); ++i) { rtabmap_conversions::toCvShare(images->rgbd_images[i], images, leftMsgs[i], rightMsgs[i]); leftInfoMsgs.push_back(images->rgbd_images[i].rgb_camera_info); rightInfoMsgs.push_back(images->rgbd_images[i].depth_camera_info); + appendLocalFeatures(images->rgbd_images[i], localKeyPoints, localPoints3d, localDescriptors); } - this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs); + this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -780,7 +878,13 @@ void StereoOdometry::callbackRGBD2( rightInfoMsgs.push_back(image->depth_camera_info); rightInfoMsgs.push_back(image2->depth_camera_info); - this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -807,7 +911,14 @@ void StereoOdometry::callbackRGBD3( rightInfoMsgs.push_back(image2->depth_camera_info); rightInfoMsgs.push_back(image3->depth_camera_info); - this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -838,7 +949,15 @@ void StereoOdometry::callbackRGBD4( rightInfoMsgs.push_back(image3->depth_camera_info); rightInfoMsgs.push_back(image4->depth_camera_info); - this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image4, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -873,7 +992,16 @@ void StereoOdometry::callbackRGBD5( rightInfoMsgs.push_back(image4->depth_camera_info); rightInfoMsgs.push_back(image5->depth_camera_info); - this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image4, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image5, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } @@ -912,7 +1040,17 @@ void StereoOdometry::callbackRGBD6( rightInfoMsgs.push_back(image5->depth_camera_info); rightInfoMsgs.push_back(image6->depth_camera_info); - this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs); + std::vector > localKeyPoints; + std::vector > localPoints3d; + std::vector localDescriptors; + appendLocalFeatures(*image, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image2, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image3, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image4, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image5, localKeyPoints, localPoints3d, localDescriptors); + appendLocalFeatures(*image6, localKeyPoints, localPoints3d, localDescriptors); + + this->commonCallback(leftMsgs, rightMsgs, leftInfoMsgs, rightInfoMsgs, localKeyPoints, localPoints3d, localDescriptors); } } diff --git a/rtabmap_odom/test/bag_playback.hpp b/rtabmap_odom/test/bag_playback.hpp new file mode 100644 index 00000000..aa636031 --- /dev/null +++ b/rtabmap_odom/test/bag_playback.hpp @@ -0,0 +1,79 @@ +/* +Copyright (c) 2010-2026, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. (BSD-3-Clause, see the repository root.) +*/ + +#ifndef RTABMAP_ODOM_BAG_PLAYBACK_HPP_ +#define RTABMAP_ODOM_BAG_PLAYBACK_HPP_ + +#include +#include + +#include +#include + +#include "test_data.hpp" + +/** + * @file + * @brief Reads recorded messages out of a bag, for tests that need real sensor input. + * + * The messages are read and replayed by the test itself rather than by `ros2 bag play`: + * no second process, no wall-clock pacing, and the test controls exactly when each + * message reaches the node. What the recording provides is the part that cannot be + * written by hand -- a real driver's cloud layout and a dense TF history around it. + */ + +namespace rtabmap_odom_test { + +/** + * @brief The Ouster recording in test/data/lidar; see that directory's README. + * + * Two sweeps half a mast turn apart, from a platform that never moves: the only thing + * between them is the mast's rotation, which TF describes in full. + */ +inline std::string ousterHalfTurnBag() +{ + return testDataRoot() + "/lidar/ouster_half_turn"; +} + +/** + * @brief Every message recorded on @p topic, deserialized. + * + * Returns an empty vector if the bag or the topic is missing, which the caller is + * expected to assert on -- a silently empty fixture would make a test pass for the wrong + * reason. + */ +template +std::vector readBagMessages(const std::string & bagPath, const std::string & topic) +{ + std::vector messages; + rosbag2_cpp::Reader reader; + try + { + reader.open(bagPath); + } + catch(const std::exception & e) + { + return messages; + } + + rclcpp::Serialization serialization; + while(reader.has_next()) + { + const std::shared_ptr message = reader.read_next(); + if(message->topic_name != topic) + { + continue; + } + rclcpp::SerializedMessage serialized(*message->serialized_data); + MsgT deserialized; + serialization.deserialize_message(&serialized, &deserialized); + messages.push_back(deserialized); + } + return messages; +} + +} // namespace rtabmap_odom_test + +#endif /* RTABMAP_ODOM_BAG_PLAYBACK_HPP_ */ diff --git a/rtabmap_odom/test/camera_rig.hpp b/rtabmap_odom/test/camera_rig.hpp new file mode 100644 index 00000000..77979a07 --- /dev/null +++ b/rtabmap_odom/test/camera_rig.hpp @@ -0,0 +1,319 @@ +/* +Copyright (c) 2010-2026, Mathieu Labbe - IntRoLab - Universite de Sherbrooke +All rights reserved. (BSD-3-Clause, see the repository root.) +*/ + +#ifndef RTABMAP_ODOM_CAMERA_RIG_HPP_ +#define RTABMAP_ODOM_CAMERA_RIG_HPP_ + +#include + +#include + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "msg_builders.hpp" + +/** + * @file + * @brief A camera rig in a world of points, for the multi-camera odometry tests. + * + * Several cameras looking outward from one body is the case that cannot be covered with + * recorded frames: it needs a calibrated rig, a scene all of them can see, and ground + * truth for where the body went. So the scene is made rather than recorded -- points + * scattered around the rig, projected into each camera at each pose along a known + * trajectory, which is the approach of RTAB-Map's own multi-camera tests + * (`EstimateMotion3DTo2DMultiCam*` in corelib/test/test_util3d_motion_estimation.cpp and + * the four-camera rig of test_optimizer.cpp). + * + * What comes out is the *features*, not imagery: keypoints, their 3D positions and a + * descriptor per point, which is what a camera driver doing its own feature extraction + * would publish on `rgbd_images`, and all it needs to publish. The frames carry no image + * at all, so a run that recovers the trajectory can only have done it from them. + */ + +namespace rtabmap_odom_test { + +/// Descriptors are matched between frames, so the same point has to keep the same one. +const int kRigDescriptorSize = 32; + +/** + * @brief Cameras looking outward from one body, and the points they see. + * + * The cameras are spread evenly around the body and mounted on its rim, so opposite + * cameras are a real distance apart rather than sharing an optical centre. + */ +struct CameraRig +{ + int width = 0; + int height = 0; + double fx = 0.0; + + /// base_link -> camera i, optical frame, in the order the cameras are published. + std::vector localTransforms; + std::vector frameIds; + + /// The world: points in the odometry frame, and one descriptor row per point. + std::vector points; + cv::Mat descriptors; + + size_t cameras() const { return localTransforms.size(); } + + /// Camera @p i as RTAB-Map sees it, intrinsics and mounting together. + rtabmap::CameraModel model(size_t i) const + { + return rtabmap::CameraModel(fx, fx, width/2.0, height/2.0, + localTransforms[i], 0, cv::Size(width, height)); + } +}; + +/** + * @brief A rig of @p cameras cameras in a world of @p numPoints points. + * + * The horizontal field of view is 360/cameras degrees, up to 90, so the cameras tile as + * much of the circle as they can without overlapping: a point is then seen by at most one + * of them, which keeps every descriptor unique within a frame. Two cameras seeing the same + * point would put two identical descriptors in the same frame, and the ratio test that + * accepts a match only when the best candidate is clearly better than the second would + * throw both away. + * + * The points sit in a box wider than the trajectory, minus a hole around it: something + * closer than @p minRange would swing through a camera's field of view, or behind it, + * over the course of the run. + */ +inline CameraRig makeCameraRig( + int cameras = 4, + int numPoints = 300, + float boxXY = 8.0f, + float boxZ = 1.5f, + float minRange = 2.5f, + int width = 160, + int height = 120, + float rimRadius = 0.175f, + float rimHeight = 0.05f, + uint32_t seed = 7) +{ + CameraRig rig; + rig.width = width; + rig.height = height; + // Half the horizontal field of view spans half the angle between two cameras, capped + // at 45 degrees: one or two cameras would otherwise be asked for a 360 or 180 degree + // view, which no pinhole model has. The cap only widens the gaps between cameras, so + // a point is still seen by at most one of them. + rig.fx = (width/2.0) / std::tan(std::min(M_PI/double(cameras), M_PI/4.0)); + + for(int i=0; i distXY(-boxXY, boxXY); + std::uniform_real_distribution distZ(-boxZ, boxZ); + std::normal_distribution distDescriptor(0.0f, 1.0f); + + rig.descriptors = cv::Mat(numPoints, kRigDescriptorSize, CV_32FC1); + for(int i=0; i(i, c) = distDescriptor(rng); + } + ++i; + } + return rig; +} + +/// The mounting of each camera, as the rig's driver would publish it on /tf_static. +inline std::vector cameraRigTransforms( + const CameraRig & rig, const rclcpp::Time & stamp, + const std::string & baseFrame = "base_link") +{ + std::vector transforms; + for(size_t i=0; i > keyPoints; + std::vector > points; + std::vector descriptors; +}; + +/** + * @brief What the rig sees from @p pose, one entry per camera. + * + * Each point is given to the first camera that has it in view, so no point is reported + * twice. The keypoints are in their own camera's image, the 3D points in their own + * camera's optical frame, and the descriptors in the order of the keypoints -- which is + * how a driver publishes them, and what the node has to reassemble. + */ +inline RigObservations observeCameraRig(const CameraRig & rig, const rtabmap::Transform & pose) +{ + const size_t cameras = rig.cameras(); + std::vector models; + std::vector worldToCamera; + for(size_t i=0; i= float(rig.width) || v >= float(rig.height)) + { + continue; + } + + rtabmap_msgs::msg::KeyPoint keyPoint; + keyPoint.pt.x = u; + keyPoint.pt.y = v; + keyPoint.size = 3; + keyPoint.response = 1.0f; + seen.keyPoints[i].push_back(keyPoint); + + rtabmap_msgs::msg::Point3f point; + point.x = inCamera.x; + point.y = inCamera.y; + point.z = inCamera.z; + seen.points[i].push_back(point); + + seen.descriptors[i].push_back(rig.descriptors.row(int(p))); + break; + } + } + return seen; +} + +/** + * @brief The rig's observations from @p pose as RGB-D frames, one per camera. + * + * @p withImages attaches a blank image to each camera. There is nothing to find in it, + * but the odometry only takes the paths that touch images when one is there. + */ +inline rtabmap_msgs::msg::RGBDImages cameraRigFrame( + const CameraRig & rig, const rtabmap::Transform & pose, double stamp, + bool withImages = false) +{ + const RigObservations seen = observeCameraRig(rig, pose); + + rtabmap_msgs::msg::RGBDImages msg; + msg.header.stamp = stampOf(stamp); + msg.header.frame_id = rig.frameIds[0]; + for(size_t i=0; i + `box_link` does not translate by a single millimetre over the whole recording -- the + answer is known: the transform between the two is the identity. That is what the test + measures against, rather than a value taken from a previous run. + + TF runs from 0.1 s before each sweep to 0.1 s past its end, with nothing in between: the + gap holds transforms nobody looks up, and keeping them would have tripled the file. +- **`rgbd`** -- `17` and `154`, two frames of a hand-held Kinect sequence, far enough apart + that losing tracking between them is a legitimate outcome. + +In every set the left image is color and the right one grayscale, as the cameras recorded +them. + +Both stereo sets come from the same 640x480 rig, but **not** from the same calibration, and +the two are not interchangeable: + +| | `stereo/rect` | `stereo/raw` | +| --- | --- | --- | +| `distortion_coefficients` | zeros | the lens's real plumb_bob values (~-0.34) | +| `rectification_matrix` | identity | the rotation into the rectified frame | +| `projection_matrix` fx | 487.61 | 500.22 | +| baseline (`-Tx/fx`) | 0.1197 m | 0.1197 m | + +Rectification is what leaves a calibration with no distortion and an identity rotation, so +the rectified file describes the output of `stereo_image_proc`, not what the camera +produced. Handing it to a raw pair claims a distortion-free lens the images do not have: +doing that costs roughly a third of the inliers (110 against 214) and triples the reported +standard deviation. `stereo_pose.yaml` holds the rig's measured extrinsics -- ~12 cm along +x plus a few milliradians of rotation -- which the tests publish as the TF between +`camera_left` and `camera_right`, the transform the node looks up when it has to rectify +the pair itself. + +## File formats + +The images are copied as they are. The calibration files differ from their originals by one +line: the format directive is commented out (`#%YAML:1.0`, with no `---`), which is the ROS +flavour of the same file. `%YAML:1.0` is not a valid YAML directive, so plain YAML parsers +-- `camera_info_manager`, `rosparam`, PyYAML -- reject the original form. +`test/test_data.hpp` puts the directive back in memory before handing the text to +`cv::FileStorage`, so the files stay readable by both. + +Note that they carry no OpenCV `dt` field either, so `>> cv::Mat` cannot read them; +`rows`/`cols`/`data` are read element by element, as RTAB-Map's own `CameraModel::load` +does. + +The depth images are 16-bit millimetres. `rgbd/calib/*.yaml` also carries a +`local_transform` (the optical-frame-to-robot transform RTAB-Map stores with the camera +model); the ROS nodes take that from TF instead, so the tests publish it as a `base_link` +-> `camera` static transform rather than reading it here. + +## Using them + +`test/test_data.hpp` loads these into `cv::Mat`, `sensor_msgs/CameraInfo` and +`geometry_msgs/Transform`. CMake passes the directory as `RTABMAP_ODOM_TEST_DATA_ROOT`, +pointing into the source tree: the test binaries are not installed, and neither are these +files. diff --git a/rtabmap_odom/test/data/lidar/ouster_half_turn/metadata.yaml b/rtabmap_odom/test/data/lidar/ouster_half_turn/metadata.yaml new file mode 100644 index 00000000..c24a12b9 --- /dev/null +++ b/rtabmap_odom/test/data/lidar/ouster_half_turn/metadata.yaml @@ -0,0 +1,52 @@ +rosbag2_bagfile_information: + compression_format: '' + compression_mode: '' + custom_data: null + duration: + nanoseconds: 5066021600 + files: + - duration: + nanoseconds: 5066021600 + message_count: 120 + path: ouster_half_turn.mcap + starting_time: + nanoseconds_since_epoch: 1613418430206475184 + message_count: 120 + relative_file_paths: + - ouster_half_turn.mcap + ros_distro: rosbags + starting_time: + nanoseconds_since_epoch: 1613418430206475184 + storage_identifier: mcap + topics_with_message_count: + - message_count: 2 + topic_metadata: + name: /tf_static + offered_qos_profiles: "- avoid_ros_namespace_conventions: false\n deadline: + {nsec: 0, sec: 0}\n depth: 10\n durability: 1\n history: 1\n lifespan: + {nsec: 0, sec: 0}\n liveliness: 1\n liveliness_lease_duration: {nsec: 0, + sec: 0}\n reliability: 1" + serialization_format: cdr + type: tf2_msgs/msg/TFMessage + type_description_hash: RIHS01_e369d0f05a23ae52508854b66f6aa0437f3449d652e8cbf22d5abe85d020f087 + - message_count: 116 + topic_metadata: + name: /tf + offered_qos_profiles: "- avoid_ros_namespace_conventions: false\n deadline: + {nsec: 0, sec: 0}\n depth: 10\n durability: 2\n history: 1\n lifespan: + {nsec: 0, sec: 0}\n liveliness: 1\n liveliness_lease_duration: {nsec: 0, + sec: 0}\n reliability: 1" + serialization_format: cdr + type: tf2_msgs/msg/TFMessage + type_description_hash: RIHS01_e369d0f05a23ae52508854b66f6aa0437f3449d652e8cbf22d5abe85d020f087 + - message_count: 2 + topic_metadata: + name: /os_cloud_node/points + offered_qos_profiles: "- avoid_ros_namespace_conventions: false\n deadline: + {nsec: 0, sec: 0}\n depth: 10\n durability: 2\n history: 1\n lifespan: + {nsec: 0, sec: 0}\n liveliness: 1\n liveliness_lease_duration: {nsec: 0, + sec: 0}\n reliability: 1" + serialization_format: cdr + type: sensor_msgs/msg/PointCloud2 + type_description_hash: RIHS01_9198cabf7da3796ae6fe19c4cb3bdd3525492988c70522628af5daa124bae2b5 + version: 8 diff --git a/rtabmap_odom/test/data/lidar/ouster_half_turn/ouster_half_turn.mcap b/rtabmap_odom/test/data/lidar/ouster_half_turn/ouster_half_turn.mcap new file mode 100644 index 0000000000000000000000000000000000000000..7e25ec581d27bb02cc8c8fa6f3bde5748f531fd9 GIT binary patch literal 857414 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

lTOhD5u== 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