mirror of
https://github.com/introlab/rtabmap_ros.git
synced 2026-10-04 00:37:46 +08:00
* 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]>
363 lines
11 KiB
C++
363 lines
11 KiB
C++
/*
|
|
Copyright (c) 2010-2026, Mathieu Labbe - IntRoLab - Universite de Sherbrooke
|
|
All rights reserved. (BSD-3-Clause, see the repository root.)
|
|
*/
|
|
|
|
#ifndef RTABMAP_ODOM_MSG_BUILDERS_HPP_
|
|
#define RTABMAP_ODOM_MSG_BUILDERS_HPP_
|
|
|
|
#include <rclcpp/rclcpp.hpp>
|
|
#include <sensor_msgs/msg/camera_info.hpp>
|
|
#include <sensor_msgs/msg/image.hpp>
|
|
#include <sensor_msgs/msg/laser_scan.hpp>
|
|
#include <sensor_msgs/msg/point_cloud2.hpp>
|
|
#include <sensor_msgs/image_encodings.hpp>
|
|
#include <nav_msgs/msg/odometry.hpp>
|
|
#include <rtabmap_msgs/msg/odom_info.hpp>
|
|
#include <rtabmap_msgs/msg/rgbd_image.hpp>
|
|
#include <rtabmap_msgs/msg/scan_descriptor.hpp>
|
|
#include <rtabmap_msgs/msg/sensor_data.hpp>
|
|
#include <rtabmap_msgs/msg/user_data.hpp>
|
|
|
|
#include <opencv2/core/core.hpp>
|
|
#ifdef PRE_ROS_IRON
|
|
#include <cv_bridge/cv_bridge.h>
|
|
#else
|
|
#include <cv_bridge/cv_bridge.hpp>
|
|
#endif
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace rtabmap_odom_test {
|
|
|
|
/// A ROS time from a double, the way sensor stamps are written throughout these tests.
|
|
inline rclcpp::Time stampOf(double seconds)
|
|
{
|
|
return rclcpp::Time(
|
|
int32_t(seconds), uint32_t((seconds - int32_t(seconds)) * 1e9), RCL_ROS_TIME);
|
|
}
|
|
|
|
/// A rectified pinhole CameraInfo; @p tx is P(0,3), non-zero for a stereo right camera.
|
|
inline sensor_msgs::msg::CameraInfo makeCameraInfo(
|
|
const std::string & frameId, double stamp, int width = 8, int height = 8,
|
|
double tx = 0.0, double fx = 100.0)
|
|
{
|
|
sensor_msgs::msg::CameraInfo info;
|
|
info.header.frame_id = frameId;
|
|
info.header.stamp = stampOf(stamp);
|
|
info.width = width;
|
|
info.height = height;
|
|
info.distortion_model = "plumb_bob";
|
|
info.d = {0.0, 0.0, 0.0, 0.0, 0.0};
|
|
info.k = {fx, 0.0, width/2.0, 0.0, fx, height/2.0, 0.0, 0.0, 1.0};
|
|
info.r = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0};
|
|
info.p = {fx, 0.0, width/2.0, tx, 0.0, fx, height/2.0, 0.0, 0.0, 0.0, 1.0, 0.0};
|
|
return info;
|
|
}
|
|
|
|
inline sensor_msgs::msg::Image makeImage(
|
|
const std::string & frameId, double stamp,
|
|
const cv::Mat & image, const std::string & encoding)
|
|
{
|
|
std_msgs::msg::Header header;
|
|
header.frame_id = frameId;
|
|
header.stamp = stampOf(stamp);
|
|
sensor_msgs::msg::Image msg;
|
|
cv_bridge::CvImage(header, encoding, image).toImageMsg(msg);
|
|
return msg;
|
|
}
|
|
|
|
/// A bgr8 color image of a single flat color.
|
|
inline sensor_msgs::msg::Image makeRgbImage(
|
|
const std::string & frameId, double stamp, int width = 8, int height = 8,
|
|
const cv::Scalar & color = cv::Scalar(10, 20, 30))
|
|
{
|
|
return makeImage(frameId, stamp, cv::Mat(height, width, CV_8UC3, color), "bgr8");
|
|
}
|
|
|
|
/// A 16UC1 depth image in millimeters, the encoding the RGB-D drivers publish.
|
|
inline sensor_msgs::msg::Image makeDepthImage(
|
|
const std::string & frameId, double stamp, int width = 8, int height = 8,
|
|
uint16_t millimeters = 1500)
|
|
{
|
|
return makeImage(frameId, stamp,
|
|
cv::Mat(height, width, CV_16UC1, cv::Scalar(millimeters)), "16UC1");
|
|
}
|
|
|
|
/// A mono8 image, used as a stereo left or right frame.
|
|
inline sensor_msgs::msg::Image makeMonoImage(
|
|
const std::string & frameId, double stamp, int width = 8, int height = 8,
|
|
uint8_t value = 60)
|
|
{
|
|
return makeImage(frameId, stamp,
|
|
cv::Mat(height, width, CV_8UC1, cv::Scalar(value)), "mono8");
|
|
}
|
|
|
|
/// An RGB-D message with raw bgr8 color and 16UC1 depth, as rgbd_sync publishes it.
|
|
inline rtabmap_msgs::msg::RGBDImage makeRGBDImage(
|
|
const std::string & frameId, double stamp, int width = 8, int height = 8,
|
|
const cv::Scalar & rgbColor = cv::Scalar(10, 20, 30), uint16_t depthValue = 1500)
|
|
{
|
|
rtabmap_msgs::msg::RGBDImage msg;
|
|
msg.header.frame_id = frameId;
|
|
msg.header.stamp = stampOf(stamp);
|
|
msg.rgb = makeRgbImage(frameId, stamp, width, height, rgbColor);
|
|
msg.depth = makeDepthImage(frameId, stamp, width, height, depthValue);
|
|
msg.rgb_camera_info = makeCameraInfo(frameId, stamp, width, height);
|
|
msg.depth_camera_info = makeCameraInfo(frameId, stamp, width, height);
|
|
return msg;
|
|
}
|
|
|
|
/// A flat LaserScan of @p count equal ranges over 180 degrees.
|
|
inline sensor_msgs::msg::LaserScan makeLaserScan(
|
|
const std::string & frameId, double stamp, size_t count = 10, float range = 2.0f)
|
|
{
|
|
sensor_msgs::msg::LaserScan scan;
|
|
scan.header.frame_id = frameId;
|
|
scan.header.stamp = stampOf(stamp);
|
|
scan.angle_min = -M_PI_2;
|
|
scan.angle_max = M_PI_2;
|
|
scan.angle_increment = count > 1 ? float(M_PI / double(count - 1)) : float(M_PI);
|
|
scan.time_increment = 0.0f;
|
|
scan.scan_time = 0.1f;
|
|
scan.range_min = 0.1f;
|
|
scan.range_max = 10.0f;
|
|
scan.ranges.assign(count, range);
|
|
return scan;
|
|
}
|
|
|
|
/// A dense unorganized XYZ float cloud, the shape a 3D lidar driver publishes.
|
|
inline sensor_msgs::msg::PointCloud2 makeXYZCloud(
|
|
const std::string & frameId, double stamp,
|
|
const std::vector<cv::Point3f> & points)
|
|
{
|
|
sensor_msgs::msg::PointCloud2 cloud;
|
|
cloud.header.frame_id = frameId;
|
|
cloud.header.stamp = stampOf(stamp);
|
|
cloud.height = 1;
|
|
cloud.width = points.size();
|
|
cloud.is_bigendian = false;
|
|
cloud.is_dense = true;
|
|
|
|
cloud.fields.resize(3);
|
|
const char * names[3] = {"x", "y", "z"};
|
|
for(int i=0; i<3; ++i)
|
|
{
|
|
cloud.fields[i].name = names[i];
|
|
cloud.fields[i].offset = 4 * i;
|
|
cloud.fields[i].datatype = sensor_msgs::msg::PointField::FLOAT32;
|
|
cloud.fields[i].count = 1;
|
|
}
|
|
cloud.point_step = 12;
|
|
cloud.row_step = cloud.point_step * cloud.width;
|
|
cloud.data.resize(cloud.row_step * cloud.height);
|
|
|
|
for(size_t i=0; i<points.size(); ++i)
|
|
{
|
|
float * p = reinterpret_cast<float *>(&cloud.data[i * cloud.point_step]);
|
|
p[0] = points[i].x;
|
|
p[1] = points[i].y;
|
|
p[2] = points[i].z;
|
|
}
|
|
return cloud;
|
|
}
|
|
|
|
/**
|
|
* @brief An XYZ cloud carrying the optional fields a 3D lidar may add.
|
|
*
|
|
* `intensity` and the `normal_*`/`curvature` group each change which PCL point type the
|
|
* odometry converts the cloud into, so a driver that sends them takes a different path
|
|
* through the node than one that sends plain XYZ. `t` is the per-point offset from the
|
|
* header stamp that deskewing needs, spread evenly over @p sweep seconds.
|
|
*/
|
|
inline sensor_msgs::msg::PointCloud2 makeCloudWithFields(
|
|
const std::string & frameId, double stamp,
|
|
const std::vector<cv::Point3f> & points,
|
|
bool withIntensity, bool withNormals,
|
|
const cv::Point3f & normal = cv::Point3f(0, 0, 1),
|
|
bool withTime = false, float sweep = 0.01f)
|
|
{
|
|
sensor_msgs::msg::PointCloud2 cloud;
|
|
cloud.header.frame_id = frameId;
|
|
cloud.header.stamp = stampOf(stamp);
|
|
cloud.height = 1;
|
|
cloud.width = points.size();
|
|
cloud.is_bigendian = false;
|
|
cloud.is_dense = true;
|
|
|
|
std::vector<std::string> names = {"x", "y", "z"};
|
|
if(withIntensity)
|
|
{
|
|
names.push_back("intensity");
|
|
}
|
|
if(withNormals)
|
|
{
|
|
names.push_back("normal_x");
|
|
names.push_back("normal_y");
|
|
names.push_back("normal_z");
|
|
names.push_back("curvature");
|
|
}
|
|
if(withTime)
|
|
{
|
|
names.push_back("t");
|
|
}
|
|
cloud.fields.resize(names.size());
|
|
for(size_t i=0; i<names.size(); ++i)
|
|
{
|
|
cloud.fields[i].name = names[i];
|
|
cloud.fields[i].offset = uint32_t(4 * i);
|
|
cloud.fields[i].datatype = sensor_msgs::msg::PointField::FLOAT32;
|
|
cloud.fields[i].count = 1;
|
|
}
|
|
cloud.point_step = uint32_t(4 * names.size());
|
|
cloud.row_step = cloud.point_step * cloud.width;
|
|
cloud.data.resize(size_t(cloud.row_step) * cloud.height);
|
|
|
|
for(size_t i=0; i<points.size(); ++i)
|
|
{
|
|
float * p = reinterpret_cast<float *>(&cloud.data[i * cloud.point_step]);
|
|
size_t f = 0;
|
|
p[f++] = points[i].x;
|
|
p[f++] = points[i].y;
|
|
p[f++] = points[i].z;
|
|
if(withIntensity)
|
|
{
|
|
p[f++] = float(i % 256);
|
|
}
|
|
if(withNormals)
|
|
{
|
|
p[f++] = normal.x;
|
|
p[f++] = normal.y;
|
|
p[f++] = normal.z;
|
|
p[f++] = 0.0f; // curvature
|
|
}
|
|
if(withTime)
|
|
{
|
|
p[f++] = points.size() > 1 ?
|
|
sweep * float(i) / float(points.size() - 1) : 0.0f;
|
|
}
|
|
}
|
|
return cloud;
|
|
}
|
|
|
|
/// A small cloud on a line, enough to tell one scan from another.
|
|
inline sensor_msgs::msg::PointCloud2 makeScanCloud(
|
|
const std::string & frameId, double stamp, size_t count = 4)
|
|
{
|
|
std::vector<cv::Point3f> points;
|
|
points.reserve(count);
|
|
for(size_t i=0; i<count; ++i)
|
|
{
|
|
points.push_back(cv::Point3f(1.0f + float(i), 0.0f, 0.0f));
|
|
}
|
|
return makeXYZCloud(frameId, stamp, points);
|
|
}
|
|
|
|
/// A ScanDescriptor carrying a 2D scan, a 3D scan, or both, and optionally a descriptor.
|
|
inline rtabmap_msgs::msg::ScanDescriptor makeScanDescriptor(
|
|
const std::string & frameId, double stamp,
|
|
bool with2d = true, bool with3d = false, bool withGlobalDescriptor = false)
|
|
{
|
|
rtabmap_msgs::msg::ScanDescriptor msg;
|
|
msg.header.frame_id = frameId;
|
|
msg.header.stamp = stampOf(stamp);
|
|
if(with2d)
|
|
{
|
|
msg.scan = makeLaserScan(frameId, stamp);
|
|
}
|
|
if(with3d)
|
|
{
|
|
msg.scan_cloud = makeScanCloud(frameId, stamp);
|
|
}
|
|
if(withGlobalDescriptor)
|
|
{
|
|
// Only "not empty" matters here: consumers pass the payload straight to
|
|
// RTAB-Map, which is what knows how to decode it.
|
|
msg.global_descriptor.header = msg.header;
|
|
msg.global_descriptor.data = {1, 2, 3, 4};
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
/// An identity-pose odometry message at @p x meters along the x axis.
|
|
inline nav_msgs::msg::Odometry makeOdometry(
|
|
const std::string & frameId, double stamp, double x = 0.0,
|
|
const std::string & childFrameId = "base_link")
|
|
{
|
|
nav_msgs::msg::Odometry msg;
|
|
msg.header.frame_id = frameId;
|
|
msg.header.stamp = stampOf(stamp);
|
|
msg.child_frame_id = childFrameId;
|
|
msg.pose.pose.position.x = x;
|
|
msg.pose.pose.orientation.w = 1.0;
|
|
return msg;
|
|
}
|
|
|
|
inline rtabmap_msgs::msg::OdomInfo makeOdomInfo(
|
|
const std::string & frameId, double stamp, int inliers = 50)
|
|
{
|
|
rtabmap_msgs::msg::OdomInfo msg;
|
|
msg.header.frame_id = frameId;
|
|
msg.header.stamp = stampOf(stamp);
|
|
msg.inliers = inliers;
|
|
msg.matches = inliers;
|
|
return msg;
|
|
}
|
|
|
|
/// A SensorData carrying one RGB-D camera, as rtabmap_odom republishes it.
|
|
inline rtabmap_msgs::msg::SensorData makeSensorData(
|
|
const std::string & frameId, double stamp, int width = 8, int height = 8)
|
|
{
|
|
rtabmap_msgs::msg::SensorData msg;
|
|
msg.header.frame_id = frameId;
|
|
msg.header.stamp = stampOf(stamp);
|
|
msg.left = makeRgbImage(frameId, stamp, width, height);
|
|
msg.right = makeDepthImage(frameId, stamp, width, height);
|
|
msg.left_camera_info.push_back(makeCameraInfo(frameId, stamp, width, height));
|
|
msg.right_camera_info.push_back(makeCameraInfo(frameId, stamp, width, height));
|
|
geometry_msgs::msg::Transform localTransform;
|
|
localTransform.rotation.w = 1.0;
|
|
msg.local_transform.push_back(localTransform);
|
|
return msg;
|
|
}
|
|
|
|
/// An uncompressed user data matrix (several rows, so it is not taken as compressed).
|
|
inline rtabmap_msgs::msg::UserData makeUserData(
|
|
const std::string & frameId, double stamp)
|
|
{
|
|
rtabmap_msgs::msg::UserData msg;
|
|
msg.header.frame_id = frameId;
|
|
msg.header.stamp = stampOf(stamp);
|
|
msg.rows = 2;
|
|
msg.cols = 2;
|
|
msg.type = CV_8UC1;
|
|
msg.data = {1, 2, 3, 4};
|
|
return msg;
|
|
}
|
|
|
|
/**
|
|
* @brief Reads the x/y/z of a point from any FLOAT32 xyz cloud.
|
|
*
|
|
* Looks the offsets up in the field list rather than assuming they are 0/4/8.
|
|
*/
|
|
inline cv::Point3f readXYZ(const sensor_msgs::msg::PointCloud2 & cloud, size_t index)
|
|
{
|
|
uint32_t xOffset = 0, yOffset = 4, zOffset = 8;
|
|
for(size_t i=0; i<cloud.fields.size(); ++i)
|
|
{
|
|
if(cloud.fields[i].name == "x") { xOffset = cloud.fields[i].offset; }
|
|
else if(cloud.fields[i].name == "y") { yOffset = cloud.fields[i].offset; }
|
|
else if(cloud.fields[i].name == "z") { zOffset = cloud.fields[i].offset; }
|
|
}
|
|
const unsigned char * base = &cloud.data[index * cloud.point_step];
|
|
return cv::Point3f(
|
|
*reinterpret_cast<const float *>(base + xOffset),
|
|
*reinterpret_cast<const float *>(base + yOffset),
|
|
*reinterpret_cast<const float *>(base + zOffset));
|
|
}
|
|
|
|
} // namespace rtabmap_odom_test
|
|
|
|
#endif /* RTABMAP_ODOM_MSG_BUILDERS_HPP_ */
|