rtabmap_odom tests and doc (#1456)

* rtabmap_odom tests and doc

* opengv note

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

* Added real data tests for rgbd_odom and stereo_odom

* added real data for icp_odometry's deskewing test

* fixing json cmake error on lyrical/rolling

* test 2d icp odom deskewing branch

* first review of existing OdometryROS tests

* testing with imu used as guess

* tested imu arrivals sync

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

* fixing header errors in ci >=lyrical

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

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

* forcing latest rtabmap version

* updated OdometryROS API

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

* splitting docker jobs

* doc edit

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

* updated stereo doc

* ficing rolling ci (rviz Ogre header)

* Added test coverage of alll rgbd_image callbacks

* fixing rolling ci

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

* fixing ros2 ci testing

* improved sync callback coverage

* improving stereo_odometry test coverage

* improved icp_odometry test coverage

* lyrical voxel_grid ptr error

* make multicam tests working as well without opengv

* removing deps of missing packages on rolling

* PCL empty cloud  conversion compiler errors fix

* fixing icp_odometry test failure on ci witohut libpointmatcher

* fixing nav2 costmap plugin build on lyrical

* joining thread when exiting

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

* Fix parallel tests seg fault

---------

Co-authored-by: mathieu86 <[email protected]>
This commit is contained in:
matlabbe
2026-09-21 17:02:45 -07:00
committed by GitHub
co-authored by mathieu86
parent 73c98f87a8
commit 11edc01d6a
91 changed files with 9736 additions and 350 deletions
@@ -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<rmw_request_id_t>, const std::shared_ptr<std_srvs::srv::Empty::Request>, std::shared_ptr<std_srvs::srv::Empty::Response>);
/// `reset_odom_to_pose` service: starts a new map at the pose given in the request.
void resetToPose(const std::shared_ptr<rmw_request_id_t>, const std::shared_ptr<rtabmap_msgs::srv::ResetPose::Request>, std::shared_ptr<rtabmap_msgs::srv::ResetPose::Response>);
/// `pause_odom` service: keeps the subscriptions but stops registering what arrives.
void pause(const std::shared_ptr<rmw_request_id_t>, const std::shared_ptr<std_srvs::srv::Empty::Request>, std::shared_ptr<std_srvs::srv::Empty::Response>);
/// `resume_odom` service: registers again, starting from the next frame.
void resume(const std::shared_ptr<rmw_request_id_t>, const std::shared_ptr<std_srvs::srv::Empty::Request>, std::shared_ptr<std_srvs::srv::Empty::Response>);
/// `log_debug` service: raises RTAB-Map's own log level to debug at runtime.
void setLogDebug(const std::shared_ptr<rmw_request_id_t>, const std::shared_ptr<std_srvs::srv::Empty::Request>, std::shared_ptr<std_srvs::srv::Empty::Response>);
/// `log_info` service; see setLogDebug().
void setLogInfo(const std::shared_ptr<rmw_request_id_t>, const std::shared_ptr<std_srvs::srv::Empty::Request>, std::shared_ptr<std_srvs::srv::Empty::Response>);
/// `log_warning` service; see setLogDebug().
void setLogWarn(const std::shared_ptr<rmw_request_id_t>, const std::shared_ptr<std_srvs::srv::Empty::Request>, std::shared_ptr<std_srvs::srv::Empty::Response>);
/// `log_error` service; see setLogDebug().
void setLogError(const std::shared_ptr<rmw_request_id_t>, const std::shared_ptr<std_srvs::srv::Empty::Request>, std::shared_ptr<std_srvs::srv::Empty::Response>);
/// 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:
@@ -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:
@@ -38,6 +38,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <image_transport/subscriber_filter.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <rtabmap_msgs/msg/key_point.hpp>
#include <rtabmap_msgs/msg/point3f.hpp>
#include <rtabmap_msgs/msg/rgbd_image.hpp>
#include <rtabmap_msgs/msg/rgbd_images.hpp>
@@ -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<cv_bridge::CvImageConstPtr> & rgbImages,
const std::vector<cv_bridge::CvImageConstPtr> & depthImages,
const std::vector<sensor_msgs::msg::CameraInfo>& cameraInfos);
const std::vector<sensor_msgs::msg::CameraInfo>& cameraInfos,
const std::vector<std::vector<rtabmap_msgs::msg::KeyPoint> > & localKeyPointsMsgs = {},
const std::vector<std::vector<rtabmap_msgs::msg::Point3f> > & localPoints3dMsgs = {},
const std::vector<cv::Mat> & localDescriptorsMsgs = {});
void callback(
const sensor_msgs::msg::Image::ConstSharedPtr image,
@@ -43,12 +43,25 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <cv_bridge/cv_bridge.hpp>
#endif
#include <sensor_msgs/msg/image.hpp>
#include <rtabmap_msgs/msg/key_point.hpp>
#include <rtabmap_msgs/msg/point3f.hpp>
#include <rtabmap_msgs/msg/rgbd_image.hpp>
#include <rtabmap_msgs/msg/rgbd_images.hpp>
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<cv_bridge::CvImageConstPtr> & leftImages,
const std::vector<cv_bridge::CvImageConstPtr> & rightImages,
const std::vector<sensor_msgs::msg::CameraInfo>& leftCameraInfos,
const std::vector<sensor_msgs::msg::CameraInfo>& rightCameraInfos);
const std::vector<sensor_msgs::msg::CameraInfo>& rightCameraInfos,
const std::vector<std::vector<rtabmap_msgs::msg::KeyPoint> > & localKeyPointsMsgs = {},
const std::vector<std::vector<rtabmap_msgs::msg::Point3f> > & localPoints3dMsgs = {},
const std::vector<cv::Mat> & localDescriptorsMsgs = {});
void callback(
const sensor_msgs::msg::Image::ConstSharedPtr imageRectLeft,