Files
rtabmap_ros/rtabmap_odom/test/test_data.hpp
T
matlabbeandmathieu86 11edc01d6a rtabmap_odom tests and doc (#1456)
* rtabmap_odom tests and doc

* opengv note

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

* Added real data tests for rgbd_odom and stereo_odom

* added real data for icp_odometry's deskewing test

* fixing json cmake error on lyrical/rolling

* test 2d icp odom deskewing branch

* first review of existing OdometryROS tests

* testing with imu used as guess

* tested imu arrivals sync

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

* fixing header errors in ci >=lyrical

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

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

* forcing latest rtabmap version

* updated OdometryROS API

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

* splitting docker jobs

* doc edit

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

* updated stereo doc

* ficing rolling ci (rviz Ogre header)

* Added test coverage of alll rgbd_image callbacks

* fixing rolling ci

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

* fixing ros2 ci testing

* improved sync callback coverage

* improving stereo_odometry test coverage

* improved icp_odometry test coverage

* lyrical voxel_grid ptr error

* make multicam tests working as well without opengv

* removing deps of missing packages on rolling

* PCL empty cloud  conversion compiler errors fix

* fixing icp_odometry test failure on ci witohut libpointmatcher

* fixing nav2 costmap plugin build on lyrical

* joining thread when exiting

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

* Fix parallel tests seg fault

---------

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

269 lines
9.1 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_TEST_DATA_HPP_
#define RTABMAP_ODOM_TEST_DATA_HPP_
#include <geometry_msgs/msg/transform.hpp>
#include <sensor_msgs/msg/camera_info.hpp>
#include <tf2/LinearMath/Matrix3x3.hpp>
#include <tf2/LinearMath/Quaternion.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include "msg_builders.hpp"
/**
* @file
* @brief Real frames for the odometry node tests, from test/data.
*
* Visual odometry needs imagery it can actually track: synthetic noise gives a detector
* corners that match nothing between frames, so a generated "motion" tells us only that
* the node did not crash. These loaders hand the tests the same stereo pairs and RGB-D
* frames that RTAB-Map registers in corelib/test/test_odometry.cpp, which lets a ROS test
* assert that a plausible transform came out the other end.
*
* See test/data/README.md for where the files come from and why they are vendored.
*/
namespace rtabmap_odom_test {
/// Root of the vendored fixtures, set by CMake to test/data in the source tree.
inline std::string testDataRoot()
{
return std::string(RTABMAP_ODOM_TEST_DATA_ROOT);
}
/**
* @brief Opens a ROS camera calibration file with OpenCV's parser.
*
* The files are the ROS flavour: the format line is commented out (`#%YAML:1.0`) so that
* a plain YAML parser -- camera_info_manager, rosparam, PyYAML -- accepts them, since
* `%YAML:1.0` is not a valid YAML directive and makes those parsers fail. cv::FileStorage
* wants the directive, so it is put back here, in memory, and the file on disk stays
* loadable by both.
*
* @return a closed FileStorage if the file is missing
*/
inline cv::FileStorage openCalibration(const std::string & path)
{
std::ifstream file(path.c_str());
if(!file.is_open())
{
return cv::FileStorage();
}
std::ostringstream buffer;
buffer << file.rdbuf();
std::string text = buffer.str();
const std::string rosHeader = "#%YAML:1.0";
if(text.compare(0, rosHeader.size(), rosHeader) == 0)
{
text = "%YAML:1.0\n---" + text.substr(rosHeader.size());
}
return cv::FileStorage(text, cv::FileStorage::READ | cv::FileStorage::MEMORY);
}
/**
* @brief Reads a rows/cols/data matrix from a calibration file node.
*
* ROS calibration files have no OpenCV `dt` field, so `>> cv::Mat` cannot read them;
* the elements are taken one by one instead, as RTAB-Map's CameraModel::load does.
*
* @return an empty vector if the node is missing or malformed
*/
inline std::vector<double> readCalibrationMatrix(
cv::FileStorage & fs, const std::string & name, int rows, int cols)
{
const cv::FileNode node = fs[name];
if(node.empty())
{
return std::vector<double>();
}
std::vector<double> data;
node["data"] >> data;
if((int)node["rows"] != rows || (int)node["cols"] != cols ||
data.size() != size_t(rows * cols))
{
return std::vector<double>();
}
return data;
}
/**
* @brief A CameraInfo from a ROS calibration file.
*
* The RGB-D files carry only camera_matrix, so R falls back to identity and P to [K|0] --
* which is what a driver publishes for an already-rectified monocular camera anyway.
*/
inline sensor_msgs::msg::CameraInfo cameraInfoFromCalibration(
const std::string & path, const std::string & frameId, double stamp)
{
sensor_msgs::msg::CameraInfo info;
info.header.frame_id = frameId;
info.header.stamp = stampOf(stamp);
cv::FileStorage fs = openCalibration(path);
if(!fs.isOpened())
{
return info;
}
info.width = uint32_t((int)fs["image_width"]);
info.height = uint32_t((int)fs["image_height"]);
info.distortion_model = fs["distortion_model"].isString() ?
(std::string)fs["distortion_model"] : std::string("plumb_bob");
const std::vector<double> k = readCalibrationMatrix(fs, "camera_matrix", 3, 3);
const std::vector<double> r = readCalibrationMatrix(fs, "rectification_matrix", 3, 3);
const std::vector<double> p = readCalibrationMatrix(fs, "projection_matrix", 3, 4);
const std::vector<double> d = readCalibrationMatrix(fs, "distortion_coefficients", 1, 5);
info.d.assign(5, 0.0);
for(size_t i=0; i<d.size() && i<info.d.size(); ++i)
{
info.d[i] = d[i];
}
for(size_t i=0; i<9; ++i)
{
info.k[i] = k.empty() ? 0.0 : k[i];
info.r[i] = r.empty() ? (i%4 == 0 ? 1.0 : 0.0) : r[i];
}
for(size_t i=0; i<12; ++i)
{
info.p[i] = p.empty() ? (i%4 == 3 ? 0.0 : info.k[(i/4)*3 + i%4]) : p[i];
}
return info;
}
// ---------------------------------------------------------------------------
// Stereo pairs: data/stereo, from one 640x480 rig at 20 Hz.
//
// "rect" holds rectified pairs (frames 50 and 60), what the nodes expect on
// left/image_rect by default. "raw" holds pairs straight off the camera (frames 420 and
// 425), for the Rtabmap/ImagesAlreadyRectified path.
//
// Each set carries the calibration that describes it, and the two are not
// interchangeable: the rectified one has no distortion and an identity rectification
// matrix, because that is what rectification leaves behind, while the raw one carries the
// lens's real plumb_bob coefficients and the rotation into the rectified frame. Handing
// the rectified calibration to a raw pair claims a distortion-free lens the images do not
// have.
// ---------------------------------------------------------------------------
/// "rect" or "raw"; a rectified pair unless a test says otherwise.
enum StereoSet { kRectified, kRaw };
inline std::string stereoSetDir(StereoSet set)
{
return testDataRoot() + (set == kRaw ? "/stereo/raw" : "/stereo/rect");
}
/// The left image of a pair, in color as a stereo driver publishes it (bgr8).
inline cv::Mat stereoLeftImage(const std::string & name, StereoSet set = kRectified)
{
return cv::imread(stereoSetDir(set) + "/left/" + name + ".jpg", cv::IMREAD_COLOR);
}
/// The right image of a pair, grayscale (mono8), which is all the matcher uses.
inline cv::Mat stereoRightImage(const std::string & name, StereoSet set = kRectified)
{
return cv::imread(stereoSetDir(set) + "/right/" + name + ".jpg", cv::IMREAD_GRAYSCALE);
}
inline sensor_msgs::msg::CameraInfo stereoLeftInfo(
const std::string & frameId, double stamp, StereoSet set = kRectified)
{
return cameraInfoFromCalibration(stereoSetDir(set) + "/stereo_left.yaml", frameId, stamp);
}
/// The right camera's P(0,3) is -fx * baseline, which is where the stereo scale comes from.
inline sensor_msgs::msg::CameraInfo stereoRightInfo(
const std::string & frameId, double stamp, StereoSet set = kRectified)
{
return cameraInfoFromCalibration(stereoSetDir(set) + "/stereo_right.yaml", frameId, stamp);
}
/**
* @brief The right camera's pose in the left camera's frame, from the rig's stereo
* calibration (data/stereo/raw/stereo_pose.yaml).
*
* Stereo calibration stores the transform the other way round -- a point in left
* coordinates maps to the right camera as X_right = R * X_left + T -- so this inverts it
* into what TF wants to publish, camera_left -> camera_right. Feeding it to TF is what
* lets Rtabmap/ImagesAlreadyRectified:=false rectify the pair against the rig's real
* extrinsics, small inter-camera rotation included, rather than an assumed ideal baseline.
*
* @return an identity transform if the file is missing
*/
inline geometry_msgs::msg::Transform stereoRightInLeftFrame()
{
geometry_msgs::msg::Transform transform;
transform.rotation.w = 1.0;
cv::FileStorage fs = openCalibration(stereoSetDir(kRaw) + "/stereo_pose.yaml");
if(!fs.isOpened())
{
return transform;
}
const std::vector<double> r = readCalibrationMatrix(fs, "rotation_matrix", 3, 3);
const std::vector<double> t = readCalibrationMatrix(fs, "translation_matrix", 3, 1);
if(r.empty() || t.empty())
{
return transform;
}
// (R, T) -> (R', -R'T)
const tf2::Matrix3x3 rotation(
r[0], r[1], r[2],
r[3], r[4], r[5],
r[6], r[7], r[8]);
const tf2::Matrix3x3 inverse = rotation.transpose();
const tf2::Vector3 translation = inverse * -tf2::Vector3(t[0], t[1], t[2]);
tf2::Quaternion q;
inverse.getRotation(q);
transform.rotation.x = q.x();
transform.rotation.y = q.y();
transform.rotation.z = q.z();
transform.rotation.w = q.w();
transform.translation.x = translation.x();
transform.translation.y = translation.y();
transform.translation.z = translation.z();
return transform;
}
// ---------------------------------------------------------------------------
// RGB-D frames: data/rgbd, frames "17" and "154".
// ---------------------------------------------------------------------------
inline cv::Mat rgbdColorImage(const std::string & name)
{
return cv::imread(testDataRoot() + "/rgbd/rgb/" + name + ".jpg", cv::IMREAD_COLOR);
}
/// 16-bit millimetres, the encoding the RGB-D drivers publish (16UC1).
inline cv::Mat rgbdDepthImage(const std::string & name)
{
return cv::imread(testDataRoot() + "/rgbd/depth/" + name + ".png", cv::IMREAD_UNCHANGED);
}
inline sensor_msgs::msg::CameraInfo rgbdInfo(
const std::string & name, const std::string & frameId, double stamp)
{
return cameraInfoFromCalibration(
testDataRoot() + "/rgbd/calib/" + name + ".yaml", frameId, stamp);
}
} // namespace rtabmap_odom_test
#endif /* RTABMAP_ODOM_TEST_DATA_HPP_ */