rtabmap_conversions tests and doc (#1449)

* Initial tests

* more tests

* More in-depth deskew() testing

* slightly less verbose clamping corruption warning

* added tf buffer related tests

* added remaining tests

* Added rosdoc2, improve tests when we require sync of odom stamp and sensor stamp

* cleanup doc

* fixing ci
This commit is contained in:
matlabbe
2026-09-06 17:23:42 -07:00
committed by GitHub
parent 1e6edb4579
commit f77dda2b58
11 changed files with 4456 additions and 77 deletions
@@ -71,76 +71,364 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#define RCLCPP_QOS(queueSize, qos) rclcpp::QoS(queueSize).reliability((rmw_qos_reliability_policy_t)qos)
#endif
/**
* @namespace rtabmap_conversions
* @brief Conversions between RTAB-Map library types and ROS 2 messages.
*
* Naming is uniform throughout: `xxxFromROS()` converts a message into an RTAB-Map
* type and returns it by value, `xxxToROS()` writes an RTAB-Map type into a message
* passed by reference so the message can be reused.
*
* @note RTAB-Map distinguishes a *null* transform (unknown) from an identity one. On
* the wire a null transform is encoded as an all-zero quaternion, so results of
* the `transformFromXxx()` functions should be checked with
* rtabmap::Transform::isNull() before use.
*/
namespace rtabmap_conversions {
void transformToTF(const rtabmap::Transform & transform, tf2::Transform & tfTransform);
//============================================================================
// Transforms
// Conversions between rtabmap::Transform and the tf2 / geometry_msgs representations.
//============================================================================
/**
* @brief Convert a rtabmap::Transform into a tf2::Transform.
* @param[in] transform the transform to convert
* @param[out] tfTransform the converted transform, or filled with NaN if @p transform is null
* @return false if @p transform is null, true otherwise
*
* @note tf2::Transform stores its rotation as a basis matrix and so cannot represent
* the all-zero quaternion used elsewhere to mean "null". The null case is
* reported through the return value instead, and the output is poisoned with
* NaN so that ignoring that return value fails loudly rather than silently
* proceeding with a plausible-looking identity.
* @see transformFromTF()
*/
bool transformToTF(const rtabmap::Transform & transform, tf2::Transform & tfTransform);
/**
* @brief Convert a tf2::Transform into a rtabmap::Transform.
* @param transform the transform to convert
* @return the converted transform, or a null transform if @p transform contains NaN
* (which is how transformToTF() reports a null transform)
* @see transformToTF()
*/
rtabmap::Transform transformFromTF(const tf2::Transform & transform);
/**
* @brief Convert a rtabmap::Transform into a geometry_msgs Transform.
*
* The quaternion is normalized. A null @p transform is encoded as an all-zero
* quaternion, which transformFromGeometryMsg() decodes back to null.
*
* @param[in] transform the transform to convert
* @param[out] msg the converted message
*/
void transformToGeometryMsg(const rtabmap::Transform & transform, geometry_msgs::msg::Transform & msg);
/**
* @brief Convert a geometry_msgs Transform into a rtabmap::Transform.
* @param msg the message to convert
* @return the converted transform, or a null transform if the quaternion is all zeros
*/
rtabmap::Transform transformFromGeometryMsg(const geometry_msgs::msg::Transform & msg);
/**
* @brief Convert a rtabmap::Transform into a geometry_msgs Pose.
* @param[in] transform the transform to convert
* @param[out] msg the converted message; a null @p transform gives an all-zero orientation
*/
void transformToPoseMsg(const rtabmap::Transform & transform, geometry_msgs::msg::Pose & msg);
/**
* @brief Convert a geometry_msgs Pose into a rtabmap::Transform.
* @param msg the message to convert
* @param ignoreRotationIfNotSet if true, an all-zero orientation yields a
* translation-only transform instead of a null one
* @return the converted transform, or a null transform if the orientation is all zeros
* and @p ignoreRotationIfNotSet is false
*
* @warning geometry_msgs::msg::Quaternion defaults to `w = 1`, not all zeros, so a
* default-constructed Pose is a valid identity rotation rather than "unset".
*/
rtabmap::Transform transformFromPoseMsg(const geometry_msgs::msg::Pose & msg, bool ignoreRotationIfNotSet = false);
//============================================================================
// Images
// Extracting OpenCV images from RGBDImage messages, and building them back.
//============================================================================
/**
* @brief Extract the RGB and depth images of an RGBDImage message, copying the pixels.
*
* Handles both the raw (`rgb`, `depth`) and compressed (`rgb_compressed`,
* `depth_compressed`) fields. Both output pointers are always valid; they hold an
* empty image when the corresponding field is not set.
*
* @param[in] image the message to read
* @param[out] rgb the RGB image
* @param[out] depth the depth image
* @see toCvShare() to avoid the copy
*/
void toCvCopy(const rtabmap_msgs::msg::RGBDImage & image, cv_bridge::CvImagePtr & rgb, cv_bridge::CvImagePtr & depth);
/**
* @brief Extract the RGB and depth images of an RGBDImage message without copying.
*
* The returned images alias the message's buffers, so @p image must outlive them.
*
* @param[in] image the message to read; its shared pointer keeps the buffers alive
* @param[out] rgb the RGB image
* @param[out] depth the depth image
*/
void toCvShare(const rtabmap_msgs::msg::RGBDImage::ConstSharedPtr & image, cv_bridge::CvImageConstPtr & rgb, cv_bridge::CvImageConstPtr & depth);
/**
* @brief Extract the RGB and depth images of an RGBDImage message without copying.
* @param[in] image the message to read
* @param[in] trackedObject object whose lifetime keeps the message buffers alive
* @param[out] rgb the RGB image
* @param[out] depth the depth image
*/
void toCvShare(const rtabmap_msgs::msg::RGBDImage & image, const std::shared_ptr<void const>& trackedObject, cv_bridge::CvImageConstPtr & rgb, cv_bridge::CvImageConstPtr & depth);
/**
* @brief Fill an RGBDImage message from a SensorData.
*
* Supports a single RGB-D camera or a single stereo pair; multi-camera data cannot be
* represented by this message and is rejected with an error.
*
* @param[in] data the sensor data to convert
* @param[out] msg the converted message, stamped with @p data's stamp
* @param[in] sensorFrameId frame id stamped on the message and its sub-messages
*
* @note rtabmap::SensorData holds its stamp as a double, so the stamp written here is
* only accurate to a few hundred nanoseconds at current epoch times and will not
* compare equal to the ROS stamp the data originally came from. Callers that need
* the exact original stamp assign `msg.header` after this call.
* @note Unlike infoToROS(), an already-stamped `msg.header` is overwritten rather than
* kept: the same header is applied to every sub-message here, so preserving only
* the top-level one would leave the message internally inconsistent.
*/
void rgbdImageToROS(const rtabmap::SensorData & data, rtabmap_msgs::msg::RGBDImage & msg, const std::string & sensorFrameId);
/**
* @brief Build a SensorData from an RGBDImage message.
*
* The stamp is taken from the top-level `image->header`, and the camera's local
* transform is not carried by the message (callers resolve it from TF).
*
* @param image the message to convert
* @return the converted sensor data
*
* @warning The returned SensorData does **not** copy the pixels: it points into the
* message's own buffers. @p image must therefore outlive it and must not be
* modified meanwhile. Deep-copy the images before letting the SensorData
* escape a subscription callback, because the queue recycles the message as
* soon as the callback returns.
*/
rtabmap::SensorData rgbdImageFromROS(const rtabmap_msgs::msg::RGBDImage::ConstSharedPtr & image);
// copy data
//============================================================================
// Compressed data
//============================================================================
/**
* @brief Copy an already-compressed cv::Mat into a byte vector.
* @param[in] compressed a 1xN CV_8UC1 matrix of compressed bytes, or an empty matrix
* @param[out] bytes the bytes; cleared when @p compressed is empty
*/
void compressedMatToBytes(const cv::Mat & compressed, std::vector<unsigned char> & bytes);
/**
* @brief Wrap a byte vector as a 1xN CV_8UC1 cv::Mat of compressed data.
* @param bytes the bytes to wrap
* @param copy if false, the returned matrix aliases @p bytes, which must then outlive it
* @return the matrix, empty when @p bytes is empty
*/
cv::Mat compressedMatFromBytes(const std::vector<unsigned char> & bytes, bool copy = true);
//============================================================================
// Statistics
//============================================================================
/**
* @brief Read an Info message into RTAB-Map statistics.
* @param[in] info the message to convert
* @param[out] stat the statistics, marked as extended
* @note The stamp comes from `info.header`, which infoToROS() does not set.
*/
void infoFromROS(const rtabmap_msgs::msg::Info & info, rtabmap::Statistics & stat);
/**
* @brief Fill an Info message from RTAB-Map statistics.
* @param[in] stats the statistics to convert
* @param[out] info the converted message
* @note If the caller left `info.header.stamp` unset it is filled from @p stats, so that
* infoFromROS() recovers a stamp. An already-stamped header is never overwritten:
* rtabmap::Statistics holds its stamp as a double, so the value derived from it is
* only accurate to a few hundred nanoseconds at current epoch times and will not
* compare equal to the ROS stamp the data came from. Callers wanting the exact
* input stamp — or a publication time unrelated to the data — stamp the header
* themselves before or after this call.
* @warning The frame id is never set: rtabmap::Statistics does not carry one, so the
* caller must always fill `info.header.frame_id` itself.
*/
void infoToROS(const rtabmap::Statistics & stats, rtabmap_msgs::msg::Info & info);
//============================================================================
// Features and landmarks
// Keypoints, 2D/3D points, descriptors and environmental sensors.
//============================================================================
/** @brief Convert a Link message into a rtabmap::Link, including its 6x6 information matrix. */
rtabmap::Link linkFromROS(const rtabmap_msgs::msg::Link & msg);
/** @brief Fill a Link message from a rtabmap::Link. */
void linkToROS(const rtabmap::Link & link, rtabmap_msgs::msg::Link & msg);
/** @brief Convert a KeyPoint message into a cv::KeyPoint. */
cv::KeyPoint keypointFromROS(const rtabmap_msgs::msg::KeyPoint & msg);
/** @brief Fill a KeyPoint message from a cv::KeyPoint. */
void keypointToROS(const cv::KeyPoint & kpt, rtabmap_msgs::msg::KeyPoint & msg);
/** @brief Convert keypoint messages into a new vector of cv::KeyPoint. */
std::vector<cv::KeyPoint> keypointsFromROS(const std::vector<rtabmap_msgs::msg::KeyPoint> & msg);
/**
* @brief Append keypoint messages to an existing vector.
* @param[in] msg the messages to convert
* @param[in,out] kpts vector the keypoints are appended to; existing content is kept
* @param[in] xShift offset added to the x coordinate of every appended keypoint,
* used when several camera images are laid out side by side
*/
void keypointsFromROS(const std::vector<rtabmap_msgs::msg::KeyPoint> & msg, std::vector<cv::KeyPoint> & kpts, int xShift=0);
/** @brief Fill keypoint messages from a vector of cv::KeyPoint. */
void keypointsToROS(const std::vector<cv::KeyPoint> & kpts, std::vector<rtabmap_msgs::msg::KeyPoint> & msg);
/** @brief Convert a GlobalDescriptor message, decompressing its data and info matrices. */
rtabmap::GlobalDescriptor globalDescriptorFromROS(const rtabmap_msgs::msg::GlobalDescriptor & msg);
/** @brief Fill a GlobalDescriptor message, compressing its data and info matrices. */
void globalDescriptorToROS(const rtabmap::GlobalDescriptor & desc, rtabmap_msgs::msg::GlobalDescriptor & msg);
/** @brief Convert global descriptor messages into RTAB-Map descriptors. */
std::vector<rtabmap::GlobalDescriptor> globalDescriptorsFromROS(const std::vector<rtabmap_msgs::msg::GlobalDescriptor> & msg);
/** @brief Fill global descriptor messages; @p msg is cleared first. */
void globalDescriptorsToROS(const std::vector<rtabmap::GlobalDescriptor> & desc, std::vector<rtabmap_msgs::msg::GlobalDescriptor> & msg);
/** @brief Convert an EnvSensor message into a rtabmap::EnvSensor. */
rtabmap::EnvSensor envSensorFromROS(const rtabmap_msgs::msg::EnvSensor & msg);
/** @brief Fill an EnvSensor message from a rtabmap::EnvSensor. */
void envSensorToROS(const rtabmap::EnvSensor & sensor, rtabmap_msgs::msg::EnvSensor & msg);
/** @brief Convert EnvSensor messages into a map keyed by sensor type. */
rtabmap::EnvSensors envSensorsFromROS(const std::vector<rtabmap_msgs::msg::EnvSensor> & msg);
/** @brief Fill EnvSensor messages from a map of sensors; @p msg is cleared first. */
void envSensorsToROS(const rtabmap::EnvSensors & sensors, std::vector<rtabmap_msgs::msg::EnvSensor> & msg);
/** @brief Convert a Point2f message into a cv::Point2f. */
cv::Point2f point2fFromROS(const rtabmap_msgs::msg::Point2f & msg);
/** @brief Fill a Point2f message from a cv::Point2f. */
void point2fToROS(const cv::Point2f & kpt, rtabmap_msgs::msg::Point2f & msg);
/** @brief Convert Point2f messages into a vector of cv::Point2f. */
std::vector<cv::Point2f> points2fFromROS(const std::vector<rtabmap_msgs::msg::Point2f> & msg);
/** @brief Fill Point2f messages from a vector of cv::Point2f. */
void points2fToROS(const std::vector<cv::Point2f> & kpts, std::vector<rtabmap_msgs::msg::Point2f> & msg);
/** @brief Convert a Point3f message into a cv::Point3f. */
cv::Point3f point3fFromROS(const rtabmap_msgs::msg::Point3f & msg);
/** @brief Fill a Point3f message from a cv::Point3f. */
void point3fToROS(const cv::Point3f & kpt, rtabmap_msgs::msg::Point3f & msg);
/**
* @brief Convert Point3f messages into a vector of cv::Point3f.
* @param msg the messages to convert
* @param transform applied to every point; ignored when null or identity
* @return the converted points
*/
std::vector<cv::Point3f> points3fFromROS(const std::vector<rtabmap_msgs::msg::Point3f> & msg, const rtabmap::Transform & transform = rtabmap::Transform());
/**
* @brief Append Point3f messages to an existing vector.
* @param[in] msg the messages to convert
* @param[in,out] points3 vector the points are appended to; existing content is kept
* @param[in] transform applied to every appended point; ignored when null or identity
*/
void points3fFromROS(const std::vector<rtabmap_msgs::msg::Point3f> & msg, std::vector<cv::Point3f> & points3, const rtabmap::Transform & transform = rtabmap::Transform());
/**
* @brief Fill Point3f messages from a vector of cv::Point3f.
* @param[in] kpts the points to convert
* @param[out] msg the converted messages
* @param[in] transform applied to every point; ignored when null or identity
*/
void points3fToROS(const std::vector<cv::Point3f> & kpts, std::vector<rtabmap_msgs::msg::Point3f> & msg, const rtabmap::Transform & transform = rtabmap::Transform());
//============================================================================
// Camera models
//============================================================================
/**
* @brief Convert a CameraInfo message into a rtabmap::CameraModel.
*
* Fisheye/equidistant distortion (4 coefficients) is repacked into RTAB-Map's 1x6
* layout. A projection matrix means the model describes an already-rectified image.
*
* @param camInfo the message to convert
* @param localTransform transform from the base frame to the optical frame
* @return the converted model
*
* @note `k`, `r` and `p` are fixed-size arrays and so are never empty. An unset matrix
* is all zeros, which is detected through the focal length (`k[0]` / `p[0]`).
*/
rtabmap::CameraModel cameraModelFromROS(
const sensor_msgs::msg::CameraInfo & camInfo,
const rtabmap::Transform & localTransform = rtabmap::Transform::getIdentity());
/**
* @brief Fill a CameraInfo message from a rtabmap::CameraModel.
*
* A model carrying a projection matrix describes a rectified image, so zero distortion
* is reported for it. Without one, `P` is synthesized as `[K | 0]` and the raw
* distortion coefficients are emitted (`equidistant` for a 1x6 fisheye matrix,
* `rational_polynomial` above 5 coefficients, `plumb_bob` otherwise).
*
* @param[in] model the model to convert
* @param[out] camInfo the converted message; the header is not set
*/
void cameraModelToROS(
const rtabmap::CameraModel & model,
sensor_msgs::msg::CameraInfo & camInfo);
/**
* @brief Build a stereo model from a pair of CameraInfo messages.
* @param leftCamInfo left camera info
* @param rightCamInfo right camera info; the baseline is read from its `P(0,3)`
* @param localTransform transform from the base frame to the left optical frame
* @param stereoTransform explicit left-to-right transform, when not encoded in `P`
* @return the converted model
*/
rtabmap::StereoCameraModel stereoCameraModelFromROS(
const sensor_msgs::msg::CameraInfo & leftCamInfo,
const sensor_msgs::msg::CameraInfo & rightCamInfo,
const rtabmap::Transform & localTransform = rtabmap::Transform::getIdentity(),
const rtabmap::Transform & stereoTransform = rtabmap::Transform());
/**
* @brief Build a stereo model, resolving the local transform from TF.
* @param leftCamInfo left camera info
* @param rightCamInfo right camera info
* @param frameId base frame the model's local transform is expressed in
* @param tfBuffer must contain @p frameId -> the left camera info's frame at
* its stamp
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @return the converted model, invalid if the transform could not be resolved
*/
rtabmap::StereoCameraModel stereoCameraModelFromROS(
const sensor_msgs::msg::CameraInfo & leftCamInfo,
const sensor_msgs::msg::CameraInfo & rightCamInfo,
@@ -148,12 +436,34 @@ rtabmap::StereoCameraModel stereoCameraModelFromROS(
tf2_ros::Buffer & tfBuffer,
double waitForTransform);
//============================================================================
// Map graph
// Poses, links, nodes and sensor data — the map serialization path.
//============================================================================
/**
* @brief Read a MapData message into poses, links and signatures.
* @param[in] msg the message to convert
* @param[out] poses optimized poses by node id
* @param[out] links constraints, keyed by their originating node id
* @param[out] signatures node data by node id
* @param[out] mapToOdom transform from the map frame to the odometry frame
*/
void mapDataFromROS(
const rtabmap_msgs::msg::MapData & msg,
std::map<int, rtabmap::Transform> & poses,
std::multimap<int, rtabmap::Link> & links,
std::map<int, rtabmap::Signature> & signatures,
rtabmap::Transform & mapToOdom);
/**
* @brief Fill a MapData message from poses, links and signatures.
* @param[in] poses optimized poses by node id
* @param[in] links constraints
* @param[in] signatures node data by node id
* @param[in] mapToOdom transform from the map frame to the odometry frame
* @param[out] msg the converted message; the header is not set
*/
void mapDataToROS(
const std::map<int, rtabmap::Transform> & poses,
const std::multimap<int, rtabmap::Link> & links,
@@ -161,40 +471,159 @@ void mapDataToROS(
const rtabmap::Transform & mapToOdom,
rtabmap_msgs::msg::MapData & msg);
/**
* @brief Read a MapGraph message into poses and links.
* @param[in] msg the message to convert
* @param[out] poses optimized poses by node id
* @param[out] links constraints, keyed by their originating node id
* @param[out] mapToOdom transform from the map frame to the odometry frame
*/
void mapGraphFromROS(
const rtabmap_msgs::msg::MapGraph & msg,
std::map<int, rtabmap::Transform> & poses,
std::multimap<int, rtabmap::Link> & links,
rtabmap::Transform & mapToOdom);
/**
* @brief Fill a MapGraph message from poses and links.
* @param[in] poses optimized poses by node id
* @param[in] links constraints
* @param[in] mapToOdom transform from the map frame to the odometry frame
* @param[out] msg the converted message; the header is not set
*/
void mapGraphToROS(
const std::map<int, rtabmap::Transform> & poses,
const std::multimap<int, rtabmap::Link> & links,
const rtabmap::Transform & mapToOdom,
rtabmap_msgs::msg::MapGraph & msg);
/**
* @brief Convert a SensorData message into a rtabmap::SensorData.
* @param msg the message to convert
* @return the converted sensor data
* @note `ground_truth_pose` is not read here; nodeFromROS() owns that field.
*/
rtabmap::SensorData sensorDataFromROS(const rtabmap_msgs::msg::SensorData & msg);
/**
* @brief Fill a SensorData message from a rtabmap::SensorData.
* @param[in] signature the sensor data to convert
* @param[out] msg the converted message
* @param[in] frameId frame id stamped on the message
* @param[in] copyRawData also serialize the uncompressed images and laser scan, which
* is significantly larger on the wire
*/
void sensorDataToROS(const rtabmap::SensorData & signature, rtabmap_msgs::msg::SensorData & msg, const std::string & frameId = "base_link", bool copyRawData = false);
/**
* @brief Convert a Node message into a rtabmap::Signature, with its data and visual words.
* @param msg the message to convert
* @return the converted signature
*/
rtabmap::Signature nodeFromROS(const rtabmap_msgs::msg::Node & msg);
/**
* @brief Fill a Node message from a rtabmap::Signature.
* @param[in] signature the signature to convert
* @param[out] msg the converted message
*/
void nodeToROS(const rtabmap::Signature & signature, rtabmap_msgs::msg::Node & msg);
// DEPRECATED
/** @deprecated Use nodeFromROS() instead. */
rtabmap::Signature nodeDataFromROS(const rtabmap_msgs::msg::Node & msg);
/** @deprecated Use nodeToROS() instead. */
void nodeDataToROS(const rtabmap::Signature & signature, rtabmap_msgs::msg::Node & msg);
/** @brief Convert only the node's metadata (id, map id, weight, stamp, label, pose). */
rtabmap::Signature nodeInfoFromROS(const rtabmap_msgs::msg::Node & msg);
/** @brief Fill only the node's metadata (id, map id, weight, stamp, label, pose). */
void nodeInfoToROS(const rtabmap::Signature & signature, rtabmap_msgs::msg::Node & msg);
//============================================================================
// Odometry
//============================================================================
/**
* @brief Format odometry info as the `Odometry/...` statistics published with the map.
* @param info the odometry info to summarize
* @return statistic name (with its unit) to value
* @note The covariance-derived entries are omitted when `info.reg.covariance` is not a
* 6x6 CV_64FC1 matrix, which is the case for a default-constructed OdometryInfo.
*/
std::map<std::string, float> odomInfoToStatistics(const rtabmap::OdometryInfo & info);
/**
* @brief Convert an OdomInfo message into a rtabmap::OdometryInfo.
* @param msg the message to convert
* @param ignoreData skip the heavy members (words, local map, correspondences)
* @return the converted odometry info
*/
rtabmap::OdometryInfo odomInfoFromROS(const rtabmap_msgs::msg::OdomInfo & msg, bool ignoreData = false);
/**
* @brief Fill an OdomInfo message from a rtabmap::OdometryInfo.
* @param[in] info the odometry info to convert
* @param[out] msg the converted message
* @param[in] ignoreData skip the heavy members (words, local map, correspondences)
*/
void odomInfoToROS(const rtabmap::OdometryInfo & info, rtabmap_msgs::msg::OdomInfo & msg, bool ignoreData = false);
//============================================================================
// User data, IMU and landmarks
//============================================================================
/**
* @brief Extract the payload of a UserData message.
* @param dataMsg the message to read
* @return the payload; still compressed when the message was written with compression,
* in which case the caller applies rtabmap::uncompressData()
*/
cv::Mat userDataFromROS(const rtabmap_msgs::msg::UserData & dataMsg);
/**
* @brief Fill a UserData message.
* @param[in] data the payload
* @param[out] dataMsg the converted message
* @param[in] compress compress the payload, which is then carried as a 1xN byte blob
*/
void userDataToROS(const cv::Mat & data, rtabmap_msgs::msg::UserData & dataMsg, bool compress);
/**
* @brief Convert an Imu message into a rtabmap::IMU.
* @param msg the message to convert
* @param localTransform transform from the base frame to the IMU frame
* @return the converted IMU sample, with its three covariance matrices
*/
rtabmap::IMU imuFromROS(const sensor_msgs::msg::Imu & msg, const rtabmap::Transform & localTransform = rtabmap::Transform::getIdentity());
/**
* @brief Fill an Imu message from a rtabmap::IMU.
* @param[in] imu the IMU sample to convert
* @param[out] msg the converted message; the header is not set
*/
void imuToROS(const rtabmap::IMU & imu, sensor_msgs::msg::Imu & msg);
/**
* @brief Convert tag/landmark detections into RTAB-Map landmarks, expressed in @p frameId.
*
* Each detection is transformed from its own frame into @p frameId, then corrected for
* the odometry motion between @p odomStamp and the detection's stamp.
*
* @param tags detections by landmark id, each paired with its tag size;
* ids must be > 0, others are dropped with an error
* @param frameId base frame the landmarks are expressed in
* @param odomFrameId fixed frame used for the odometry correction; when empty
* no correction is applied
* @param odomStamp stamp the landmarks should be synchronized to
* @param tfBuffer must contain @p frameId -> each detection's frame at that
* detection's stamp, and, when @p odomFrameId is set,
* @p odomFrameId -> @p frameId covering both stamps
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @param defaultLinVariance linear variance used when a detection carries no covariance
* @param defaultAngVariance angular variance used when a detection carries no covariance
* @return the landmarks, keyed by id
*/
rtabmap::Landmarks landmarksFromROS(
const std::map<int, std::pair<geometry_msgs::msg::PoseWithCovarianceStamped, float> > & tags,
const std::string & frameId,
@@ -205,10 +634,43 @@ rtabmap::Landmarks landmarksFromROS(
double defaultLinVariance,
double defaultAngVariance);
inline double timestampFromROS(const rclcpp::Time & stamp) {return stamp.seconds();}
inline rclcpp::Time timestampToROS(const double & t) {int32_t sec= (int32_t)floor(t); return rclcpp::Time(sec, (uint32_t)std::round((t-sec) * 1e9));}
// common stuff
//============================================================================
// Timestamps
//============================================================================
/**
* @brief Convert a ROS time into seconds.
* @note A double holds about 15-16 significant digits, so at current epoch times
* (~1.7e9 s) it resolves to roughly 400 ns. Converting back with timestampToROS()
* therefore does not reproduce the original stamp exactly, and the rounding can
* carry into the seconds field. Compare converted stamps with a tolerance, and
* keep the original rclcpp::Time whenever exactness matters.
*/
inline double timestampFromROS(const rclcpp::Time & stamp) {return stamp.seconds();}
/**
* @brief Convert seconds into a ROS time.
* @note The result uses RCL_ROS_TIME, matching how message header stamps convert. The
* rclcpp::Time(sec, nsec) constructor defaults to RCL_SYSTEM_TIME instead, and
* comparing times of different clock types throws.
*/
inline rclcpp::Time timestampToROS(const double & t) {int32_t sec= (int32_t)floor(t); return rclcpp::Time(sec, (uint32_t)std::round((t-sec) * 1e9), RCL_ROS_TIME);}
//============================================================================
// TF lookups
//============================================================================
/**
* @brief Look a static relationship between two frames up in TF.
* @param fromFrameId the reference frame
* @param toFrameId the target frame
* @param stamp time of the lookup
* @param tfBuffer buffer to query
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @return the transform, or a null transform if the lookup failed (which is logged
* rather than thrown)
*/
rtabmap::Transform getTransform(
const std::string & fromFrameId,
const std::string & toFrameId,
@@ -216,9 +678,19 @@ rtabmap::Transform getTransform(
tf2_ros::Buffer & tfBuffer,
double waitForTransform);
// get moving transform accordingly to a fixed frame. For example get
// transform of /base_link between two stamps accordingly to /odom frame.
/**
* @brief Measure how a frame moved between two stamps, relative to a fixed frame.
*
* For example, the motion of `base_link` between two stamps as seen from `odom`.
*
* @param movingFrame the frame whose motion is measured
* @param fixedFrame the frame the motion is measured against
* @param stampFrom start of the interval
* @param stampTo end of the interval
* @param tfBuffer buffer to query
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @return the motion, or a null transform if the lookup failed
*/
rtabmap::Transform getMovingTransform(
const std::string & movingFrame,
const std::string & fixedFrame,
@@ -227,6 +699,54 @@ rtabmap::Transform getMovingTransform(
tf2_ros::Buffer & tfBuffer,
double waitForTransform);
//============================================================================
// Sensor message conversion
// Assembling RGB-D, stereo and laser scan messages into RTAB-Map inputs.
//============================================================================
/**
* @brief Assemble one or more RGB-D (or RGB + right) camera streams into RTAB-Map inputs.
*
* With several cameras the images are concatenated horizontally into a single wide
* image and one model is produced per camera. Whether the second image is treated as a
* depth map or as the right image of a stereo pair is inferred from its encoding, and
* for `mono16` from whether the camera infos carry a baseline in `P(0,3)`.
*
* @param imageMsgs RGB (or left) images, one per camera; may be empty
* @param depthMsgs depth (or right) images, one per camera; may be empty
* @param cameraInfoMsgs camera infos, one per camera; must not be empty
* @param depthCameraInfoMsgs camera infos of the depth/right cameras; may be empty
* @param frameId base frame the local transforms are expressed in
* @param odomFrameId fixed frame the robot motion is measured against, used to
* re-express each camera pose relative to the base frame at
* @p odomStamp; empty to skip that correction entirely
* @param odomStamp stamp the data is synchronized to
* @param[out] rgb the assembled RGB (or left) image
* @param[out] depth the assembled depth (or right) image
* @param[out] cameraModels one model per camera, when the input is RGB-D
* @param[out] stereoCameraModels one model per camera, when the input is stereo
* @param tfBuffer must contain @p frameId -> each camera's optical frame at
* that camera's stamp, and, when @p odomFrameId is set,
* @p odomFrameId -> @p frameId covering both @p odomStamp
* and the camera stamps
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @param alreadRectifiedImages whether the images are already rectified
* @param localKeyPointsMsgs optional per-camera keypoints to merge
* @param localPoints3dMsgs optional per-camera 3D points to merge
* @param localDescriptorsMsgs optional per-camera descriptors to merge
* @param[out] localKeyPoints merged keypoints, shifted to the concatenated image
* @param[out] localPoints3d merged 3D points
* @param[out] localDescriptors merged descriptors
* @return false on an unsupported encoding or a missing camera local transform
*
* @note The odometry correction is applied per camera, using each camera's own stamp,
* and only when it differs from @p odomStamp. If that lookup fails the function
* warns and carries on with an uncorrected pose — unlike a missing camera local
* transform, which is fatal and returns false.
* @note A camera's RGB and depth stamps are assumed to be equal. Should they differ,
* the depth stamp is the one used, since the geometry is what gets synchronized.
*/
bool convertRGBDMsgs(
const std::vector<cv_bridge::CvImageConstPtr> & imageMsgs,
const std::vector<cv_bridge::CvImageConstPtr> & depthMsgs,
@@ -249,6 +769,34 @@ bool convertRGBDMsgs(
std::vector<cv::Point3f> * localPoints3d = 0,
cv::Mat * localDescriptors = 0);
/**
* @brief Convert a stereo pair into RTAB-Map inputs.
*
* The left image keeps its colour; the right image is always reduced to mono.
*
* @param leftImageMsg left image
* @param rightImageMsg right image
* @param leftCamInfoMsg left camera info
* @param rightCamInfoMsg right camera info; the baseline is read from its `P(0,3)`
* @param frameId base frame the local transform is expressed in
* @param odomFrameId fixed frame the robot motion is measured against, used to
* re-express the camera pose relative to the base frame at
* @p odomStamp; empty to skip that correction entirely
* @param odomStamp stamp the data is synchronized to
* @param[out] left the left image
* @param[out] right the right image, as mono
* @param[out] stereoModel the stereo model
* @param tfBuffer must contain @p frameId -> the left image's frame at the left
* image stamp, and, when @p odomFrameId is set,
* @p odomFrameId -> @p frameId covering both stamps
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @param alreadyRectified whether the images are already rectified
* @return false on an unsupported encoding or a missing local transform
*
* @note The odometry correction is applied only when the left image stamp differs from
* @p odomStamp. A failed correction lookup warns and leaves the pose uncorrected;
* a missing local transform is fatal and returns false.
*/
bool convertStereoMsg(
const cv_bridge::CvImageConstPtr& leftImageMsg,
const cv_bridge::CvImageConstPtr& rightImageMsg,
@@ -264,6 +812,34 @@ bool convertStereoMsg(
double waitForTransform,
bool alreadyRectified);
/**
* @brief Convert a 2D LaserScan into a rtabmap::LaserScan.
* @param scan2dMsg the scan to convert
* @param frameId base frame the scan's local transform is expressed in
* @param odomFrameId fixed frame the robot motion is measured against, used to
* re-express the scan pose relative to the base frame at
* @p odomStamp; empty to skip that correction entirely
* @param odomStamp stamp the scan is synchronized to
* @param[out] scan the converted scan
* @param tfBuffer must contain @p frameId -> the laser frame at the scan stamp,
* and the laser frame relative to @p odomFrameId (or @p frameId
* when that is empty) across the whole sweep, since the points
* are projected through it
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @param outputInFrameId express the points in @p frameId rather than the laser frame
* @return false if the scan is malformed (zero angle increment, inverted range or angle
* bounds) or if a required transform is missing
*
* @note Unlike convertScan3dMsg(), this deskews the scan itself: the points are
* projected with laser_geometry, which transforms each ray at its own time using
* @p scan2dMsg.time_increment. That only corrects for motion if the projection
* target is a fixed frame, i.e. if @p odomFrameId is set — with it empty the
* target is @p frameId, which does not move relative to itself. This is also why
* the laser frame must be known across the whole sweep, which the function checks
* up front.
* @note The odometry correction is applied only when the scan stamp differs from
* @p odomStamp; a failed correction lookup warns and leaves the pose uncorrected.
*/
bool convertScanMsg(
const sensor_msgs::msg::LaserScan & scan2dMsg,
const std::string & frameId,
@@ -274,6 +850,32 @@ bool convertScanMsg(
double waitForTransform,
bool outputInFrameId = false);
/**
* @brief Convert a PointCloud2 into a rtabmap::LaserScan.
* @param scan3dMsg the cloud to convert
* @param frameId base frame the scan's local transform is expressed in
* @param odomFrameId fixed frame the robot motion is measured against, used to
* re-express the scan pose relative to the base frame at
* @p odomStamp; empty to skip that correction entirely
* @param odomStamp stamp the scan is synchronized to
* @param[out] scan the converted scan
* @param tfBuffer must contain @p frameId -> the cloud's frame at the cloud
* stamp, and, when @p odomFrameId is set, @p odomFrameId ->
* @p frameId covering both stamps
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @param maxPoints downsample to at most this many points, 0 for no limit
* @param maxRange drop points beyond this range, 0 for no limit
* @param is2D treat the cloud as planar
* @return false if the local transform could not be resolved
*
* @note The cloud is assumed to be already deskewed. A single rigid transform is applied
* to the whole cloud, so any motion during the sweep is preserved as-is; call
* deskew() on the message first if the sensor was moving. This is unlike
* convertScanMsg(), which deskews 2D scans itself through laser_geometry.
* @note The odometry correction is applied only when the cloud stamp differs from
* @p odomStamp; a failed correction lookup warns and leaves the pose uncorrected.
* @see deskew()
*/
bool convertScan3dMsg(
const sensor_msgs::msg::PointCloud2 & scan3dMsg,
const std::string & frameId,
@@ -286,6 +888,29 @@ bool convertScan3dMsg(
float maxRange = 0.0f,
bool is2D = false);
//============================================================================
// Point cloud utilities
//============================================================================
/**
* @brief Deskew a point cloud using TF.
*
* Corrects each point for the sensor motion during the sweep, using the per-point time
* channel (`t`, `time`, `stamps` or `timestamp`). See the other overload for how that
* channel is interpreted.
*
* @param input the cloud to deskew
* @param[out] output the deskewed cloud, expressed in the frame at input's header stamp
* @param fixedFrameId frame the sensor motion is measured against
* @param tfBuffer must contain the cloud's own frame relative to
* @p fixedFrameId across the whole sweep
* @param waitForTransform seconds to wait for TF, 0 to not wait
* @param slerp interpolate between the sweep's two end poses instead of
* looking TF up for every point; one query instead of N, at the
* cost of linearizing the motion across the sweep
* @return false if the cloud has no usable time channel or a lookup failed
*/
bool deskew(
const sensor_msgs::msg::PointCloud2 & input,
sensor_msgs::msg::PointCloud2 & output,
@@ -294,21 +919,49 @@ bool deskew(
double waitForTransform,
bool slerp = false);
/**
* @brief Deskew a point cloud using a constant velocity model.
*
* The per-point time channel may be named `t`, `time`, `stamps` or `timestamp`. Its
* datatype decides how it is read: `UINT32` (nanoseconds) and `FLOAT32` (seconds) are
* *offsets from the message header stamp*, while `FLOAT64` carries *absolute* stamps,
* with milliseconds/microseconds/nanoseconds detected automatically by magnitude.
*
* On success the channel is zeroed to mark the cloud as deskewed, so calling this again
* on the same cloud is a no-op that returns true rather than an error.
*
* @param input cloud with a per-point time channel
* @param[out] output deskewed cloud, expressed in the frame at input's header stamp
* @param velocity twist of the sensor frame (m/s and rad/s)
* @return false if the cloud has no usable time channel or @p velocity is null
*/
bool deskew(
const sensor_msgs::msg::PointCloud2 & input,
sensor_msgs::msg::PointCloud2 & output,
double previousStamp,
const rtabmap::Transform & velocity);
// Missing function in ros2 (from old pcl_ros)
/**
* @brief Apply a rigid transform to the XYZ fields of a point cloud.
*
* Missing function in ROS 2, taken from the old pcl_ros.
*
* @param transform the transform to apply
* @param in the cloud to transform
* @param[out] out the transformed cloud; all other fields are copied unchanged
*/
void transformPointCloud (
const Eigen::Matrix4f &transform,
const sensor_msgs::msg::PointCloud2 &in,
sensor_msgs::msg::PointCloud2 &out);
/** Return the size of a datatype (which is an enum of sensor_msgs::PointField::) in bytes
* @param datatype one of the enums of sensor_msgs::PointField::
* Note: Missing function in ros2 (from old pcl_ros)
/**
* @brief Return the size in bytes of a PointField datatype.
*
* Missing function in ROS 2, taken from the old pcl_ros.
*
* @param datatype one of the sensor_msgs::msg::PointField enums
* @return the size in bytes
* @throws std::runtime_error if @p datatype is not a known PointField type
*/
inline int sizeOfPointField(int datatype)
{
@@ -330,6 +983,13 @@ inline int sizeOfPointField(int datatype)
return -1;
}
/**
* @brief Find the entry of a map whose key is closest to @p key.
* @param buffer the map to search; must not be empty
* @param key the key to look for
* @return iterator to the closest entry, clamped to the first or last one when @p key
* falls outside the map's range
*/
template <typename K, typename V>
typename std::map<K, V>::const_iterator getClosestIterator(
const std::map<K, V> & buffer,
@@ -365,6 +1025,7 @@ typename std::map<K, V>::const_iterator getClosestIterator(
return iterB;
}
}
#endif /* MSGCONVERSION_H_ */