mirror of
https://github.com/introlab/rtabmap.git
synced 2026-09-03 01:50:24 +08:00
* added doc and tests for util2d.h * updated cmake-ros ci * Added util3d.h doc and tests * util3d_transforms.h: Added doc and tests * util3d_filtering.h: started doc and test * util3d_filtering.h: more tests and doc * Added more doc/tests * finished util3d_filtering doc and tests * added test for util2d::depthBleedingFiltering * Added util3d_registration tests * Added util3d_features.h doc/tests * added doc/tests for util3d_correspondences.h * added doc/gtest for util3d_mapping.h (missing hpp functions) * finished testing util3d_mapping.hpp * Added util3d_motion_estimation.h tests (2D->3D done) * finished util3d_motion_estimation.h tests * minimal util3d_surface.h * Added Transform and VisualWord tests * Added doc for CameraModel and StereoCameraModel * Added more logs in ros ci * Passing tests on fical * improved all devcontainer * added devcontainer kilted, fixed source setup.bash, removed ldconfig in ros-cmake workflow * cleanup * source ros * Added utilite tests * Added testing to appveyor, github actions cancellable on re-commit on same branch * appveyor testing without all targets * appveyor: specifying ALL_BUILD target * Fixed Util2dTest.NMSImageBoundsRespected test * Fixing PCL Indices error on old pcl * Added VWDictionary tests and doc. Fixed LSH not working (fix from https://github.com/flann-lib/flann/pull/472 * fixing some appveyor CI errors, added test to check dictionary serialization against all type * Added StereoDense, StereoBM and StereoSGBM doc and tests * Added Stereo tests * Added CameraModel and StereoCameraModel tests * Added doc and test for Statistics * Added doc/tests for Signature * Added doc/test for SensorEvent, added doc for SensorCaptureInfo * Added doc to SensorData * Added SensorData tests * Added SensorCapture and SensorCaptureThread doc and tests * fixed sensordata test * updated SSC test and doc * Added doc and tests for BayesFilter class * Enabled testing on mac, updated windows testing like on linux * added test_link * fixed unresolved on windows * fixed ThreadHandle error on macos ci * Added GPS and GeodeticCoords tests * Added tests for compression * Added Odometry tests (base class only) * Added DBDriver tests * Added coverage report * uniformized test names * fixing concurancy and coverage ci * dont built tools, examples and app for coverage build * fixed report tool rebuilt without qt compilation error * updated coverage option * updated coverage config * added doc CI job * fixing windows and mac ci errors * Added DBDriverSqlite3 tests * Added IMU tests * Added Graph tests * fixing flaky macos test * Added IMUThread and IMUFilter tests * Added Landmarks tests * Added LASWriter tests * fixing seed flaky test * fixing flaky macos timing tests * Added LocalGrid tests * Added LocalGridMaker tests * fixing ci errors * Added GlobalMap tests * Added doc for EnvSensor * Added Features2D tests * Added Registration tests * Added RegistrationVis tests * Added doc for Rtabmap and Memory classes * Added Memory and Rtabmap tests * making some tests less flaky * lcov 1.14 support * updated compatible tool arguments * Added integration tests (RGB-D, Stereo, Lidar2d, Lidar3d) * More octomap checks * Refactored how/when python interpretor is created to simplify library usage * Added python tests * fixed some flaky tests * suppressed some third party related warnings * fixed ceres tests * more flaky fixes * Fixing tests without libpointmatcher * Added RANSAC rejection filter to PCL ICP * fixing multi platform flakiness * Added test to detect regression * Fixing windows pcl link error * fixed some macos flakiness * bigger 2D2D registration error on opencv 4.6.0 * flakiness * fixing flaky tests on windows and mac * flaky thread test on slow mac VM * windows slow test * fixing more ci erros * fxing temp dir on windows * Added Optimizer tests and discovered some bugs (fixed) * fixing flaky tests in mac and windows * Added Optimizer doc * Added GTSAM BA, updated Ceres to use g2o ba parameters. Renamed g2o's ba related parameters to Optimizer group and used by both gtsam and ceres. * fixing build without gtsam * fixing home dir * fixing python ci isssues * Added multicam ba tests * Added Ceres multicam BA support * Aligned BundleAdjustment parameters with Optimizer/Strategy to avoid confusion in the code * Added BA integration test * Added robust graph optimization integration test * Added loop3it test * Added stereo20Hz test * Added smartfactor gtsam * Fixed bugged check and warn if python didn't return any descriptors * Fixing gtsam version build issues * fixing tilt on windows ci * loosing ceres integration test for ci * mac ci flakiness * updating missing param in gui * updating test bound for mac * added appearance-based tests, set min gftt quality to quality level * testing more stuff * improving features2d tests * ci flakiness * fixing flaky ci * ci fixes * flaky fixes * Added RegistrationIcp tests * Added icp integration test with real-worl corridor like env * intermediate nodes * fixing enum * Updated test to catch #1714 * Fixed 2d corridor failing on pcl * flaky pnp test * flaky brisk test * Set rtabmap_integration test as long * updating loop closure test * flaky ci tests * TEsting roundtrip g2o/toro save/load * loosing test bound * fixed cuda capable checks * flaky tests * Debugging test hanging * more debugging stuff * updating limit * windows: disabled cuda on ci to avoid incompatible driver issue. Fixing a bad test mem allocation * trying fixing cuda hanging issue * fixing ci flakyness * flaky tests * Updated BOW flaky tests by checking min precision/recall instead of recall@100precision. Fixed signature test * CameraModel::load() test initRectificationMap param * test dbdriver load dictionary idsOnly * Memory: test keepLinkedInDb param * added dummyDictionary tests * test intermediate nodes count * Added MarkerDetector tests * reverted breaking change of UMutex and USemaphore * Features2d: fixed compiltion warnings with clang about override * clang warnings * fixing test build with pcl 1.8 * g2o and gtsam build errors on android * opencv5 test fixes * disabled testing for ios and android builds * normalized endline characters for easier diff * added LF CRLF rule * bump 0.23.10. fixing doc version * Publish rtabmap website doc from ci * fixing MSCVC build error * macos icp flaky test * fixing ceres macos test bound * ficing more flaky tests * fixing opencv5 related test errors. Also fixed an actual bug in ENU_WGS84ToGeocentric_WGS84() * added comment about mrpt change * removed rosdoc2 (will add it for rtabmap_ros later) * fixing website style * updated download links * locally deployable website with api * sweep doxygen issues * improved/revised doxygen main pages * removed examples empty page * Updated doxygen style * more concise doxygen groups * added api link on main readme * fixing utilite test error * fixing CommonFilteringGroundNormalsUp test * updated precisionRecall test bounds for Freak and brief descriptors * fixing scale check in ba tests * disabled tests on windows cuda build (missing dlls amd runner cannot test cuda anyway) * ceres: missing suitesparse dep in windows ci * adjusting recall thr for fast/freak * ficing more flaky tests * fixing flaky tests * disabled coverage in ros ci * Enable integration tests for ros ci jobs * loosing up some threshold for failing tests * trigger cache * fixing test data in ros ci. Updated flaky test for mac * slaking some test limit * Fixed rtabmap-detectMoreLoopClosures inverted output value * loosing up sift recall on mac * optimizer re-ordered distribution for reproducible results (mac g2o) * macos dump test crash log * combining all tests to save time on shared library reload. Also fixed Logs with missing arguments. * Added ENABLE_FORMAT_ERRORS cmake option * do test only one time * fixed all format warnings * format security android build errors * less verbose tests * updated ImuUThread test * fixed a log * Fixed libpointmatcher 2d normals eigen issue * Fixing libpointmatcher conversion issues * fixing libpointmatcher test on windows ci * cleanup comments, relax some test thr * disabled sequoia-intel ci build (too flaky, would need extensive testing directly on that machine)
682 lines
23 KiB
C++
682 lines
23 KiB
C++
#include "gtest/gtest.h"
|
||
#include "rtabmap/core/util3d.h"
|
||
#include "rtabmap/core/util3d_mapping.h"
|
||
#include "rtabmap/core/util3d_surface.h"
|
||
#include "rtabmap/core/CameraModel.h"
|
||
#include "rtabmap/utilite/UException.h"
|
||
#include "rtabmap/utilite/UConversion.h"
|
||
#include <pcl/io/pcd_io.h>
|
||
|
||
using namespace rtabmap;
|
||
|
||
TEST(Util3dMappingTest, RayTraceClearsFreePathWithoutObstacle) {
|
||
cv::Mat grid = cv::Mat::ones(10, 10, CV_8SC1) * 50; // Initial grid (non-zero for testing)
|
||
cv::Point2i start(2, 2);
|
||
cv::Point2i end(7, 7);
|
||
|
||
util3d::rayTrace(start, end, grid, false);
|
||
|
||
// Check that the path has been cleared (set to 0)
|
||
for (int i = 2; i < 7; ++i) {
|
||
ASSERT_EQ(grid.at<signed char>(i, i), 0);
|
||
}
|
||
}
|
||
|
||
TEST(Util3dMappingTest, RayTraceStopsOnObstacleWhenFlagTrue) {
|
||
cv::Mat grid = cv::Mat::ones(10, 10, CV_8SC1) * 50;
|
||
grid.at<signed char>(5, 5) = 100; // Add obstacle
|
||
cv::Point2i start(2, 2);
|
||
cv::Point2i end(7, 7);
|
||
|
||
util3d::rayTrace(start, end, grid, true);
|
||
|
||
// Ensure points before the obstacle are cleared, and after are not
|
||
for (int i = 2; i < 5; ++i) {
|
||
EXPECT_EQ(grid.at<signed char>(i, i), 0);
|
||
}
|
||
EXPECT_EQ(grid.at<signed char>(5, 5), 100); // Obstacle should remain
|
||
EXPECT_NE(grid.at<signed char>(6, 6), 0); // Not cleared after obstacle
|
||
}
|
||
|
||
TEST(Util3dMappingTest, RayTraceIgnoresObstacleWhenFlagFalse) {
|
||
cv::Mat grid = cv::Mat::ones(10, 10, CV_8SC1) * 50;
|
||
grid.at<signed char>(5, 5) = 100; // Add obstacle
|
||
cv::Point2i start(2, 2);
|
||
cv::Point2i end(7, 7);
|
||
|
||
util3d::rayTrace(start, end, grid, false);
|
||
|
||
// All cells in the line should be cleared regardless of obstacle
|
||
for (int i = 2; i < 7; ++i) {
|
||
EXPECT_EQ(grid.at<signed char>(i, i), 0);
|
||
}
|
||
}
|
||
|
||
TEST(Util3dMappingTest, RayTraceHandlesSteepSlopeCorrectly) {
|
||
// Create a 10x10 grid filled with 50
|
||
cv::Mat grid = cv::Mat::ones(10, 10, CV_8SC1) * 50;
|
||
|
||
// Set a steep slope: vertical-like line from bottom to top
|
||
cv::Point2i start(5, 1); // near the bottom
|
||
cv::Point2i end(6, 8); // almost vertical but not perfectly, so slope > 1
|
||
|
||
util3d::rayTrace(start, end, grid, false);
|
||
|
||
// Check that some pixels along that steep line are cleared
|
||
// We expect the values along the approximate path to be set to 0
|
||
int clearedCount = 0;
|
||
for (int y = 1; y <= 8; ++y) {
|
||
for (int x = 5; x <= 6; ++x) {
|
||
if (grid.at<signed char>(y, x) == 0) {
|
||
clearedCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// At least 5 pixels should have been cleared along the steep slope
|
||
EXPECT_GE(clearedCount, 5) << "Steep slope did not clear expected cells.";
|
||
|
||
grid = cv::Mat::ones(10, 10, CV_8SC1) * 50;
|
||
|
||
// Same thing, but inverted
|
||
util3d::rayTrace(end, start, grid, false);
|
||
|
||
// Check that some pixels along that steep line are cleared
|
||
// We expect the values along the approximate path to be set to 0
|
||
clearedCount = 0;
|
||
for (int y = 1; y <= 8; ++y) {
|
||
for (int x = 5; x <= 6; ++x) {
|
||
if (grid.at<signed char>(y, x) == 0) {
|
||
clearedCount++;
|
||
}
|
||
}
|
||
}
|
||
|
||
// At least 5 pixels should have been cleared along the steep slope
|
||
EXPECT_GE(clearedCount, 5) << "Steep slope did not clear expected cells.";
|
||
}
|
||
|
||
TEST(Util3dMappingTest, RayTraceHandlesHorizontalVerticalSlopesCorrectly) {
|
||
// Create a 10x10 grid filled with 50
|
||
cv::Mat grid = cv::Mat::ones(10, 10, CV_8SC1) * 50;
|
||
|
||
// Set horizontal slope
|
||
cv::Point2i start(1, 1);
|
||
cv::Point2i end(8, 1);
|
||
|
||
util3d::rayTrace(start, end, grid, false);
|
||
|
||
for (int x = 1; x < 8; ++x) {
|
||
EXPECT_EQ(grid.at<signed char>(1, x), 0);
|
||
}
|
||
|
||
start = cv::Point2i(4, 2);
|
||
end = cv::Point2i(4, 8);
|
||
util3d::rayTrace(start, end, grid, false);
|
||
|
||
for (int y = 2; y < 8; ++y) {
|
||
EXPECT_EQ(grid.at<signed char>(y, 4), 0);
|
||
}
|
||
}
|
||
|
||
TEST(Util3dMappingTest, RayTraceClipsEndPointToBoundary) {
|
||
cv::Mat grid = cv::Mat::ones(10, 10, CV_8SC1) * 50;
|
||
cv::Point2i start(3, 3);
|
||
cv::Point2i end(20, 20); // Way outside bounds
|
||
|
||
util3d::rayTrace(start, end, grid, false);
|
||
|
||
// All cells along the diagonal from (3,3) to (8,8) should be cleared
|
||
for (int i = 3; i < grid.rows-1 && i < grid.cols-1; ++i) {
|
||
EXPECT_EQ(grid.at<signed char>(i, i), 0);
|
||
}
|
||
}
|
||
|
||
TEST(Util3dMappingTest, Create2DMapBasicMapGeneration)
|
||
{
|
||
std::map<int, Transform> poses;
|
||
std::map<int, std::pair<cv::Mat, cv::Mat>> scans;
|
||
std::map<int, cv::Point3f> viewpoints;
|
||
|
||
int id = 1;
|
||
float cellSize = 0.05f;
|
||
float xMin, yMin;
|
||
float minMapSize = 1.0f;
|
||
float scanMaxRange = 0.5f;
|
||
bool unknownSpaceFilled = false;
|
||
|
||
// Fake pose (origin)
|
||
poses[id] = Transform::getIdentity();
|
||
|
||
// Fake viewpoint
|
||
viewpoints[id] = cv::Point3f(-0.1f, -0.0f, 0);
|
||
|
||
cv::Mat hit(1, 2, CV_32FC2);
|
||
// Create a hit point under max scan range
|
||
hit.at<cv::Vec2f>(0, 0) = cv::Vec2f(0.2f, 0.2f);
|
||
// Create a hit point over max scan range
|
||
hit.at<cv::Vec2f>(0, 1) = cv::Vec2f(3.0f, 0.0f);
|
||
|
||
// No miss points
|
||
cv::Mat noHit(1, 1, CV_32FC2);
|
||
noHit.at<cv::Vec2f>(0, 0) = cv::Vec2f(0.0f, 0.2f);
|
||
|
||
scans[id] = std::make_pair(hit, noHit);
|
||
|
||
// Call the function
|
||
cv::Mat map = util3d::create2DMap(poses, scans, viewpoints, cellSize, unknownSpaceFilled, xMin, yMin, minMapSize, scanMaxRange);
|
||
|
||
// Verify map size
|
||
ASSERT_FALSE(map.empty());
|
||
EXPECT_EQ(map.type(), CV_8S);
|
||
EXPECT_EQ(map.cols, minMapSize/cellSize+20);
|
||
EXPECT_EQ(map.rows, minMapSize/cellSize+20);
|
||
|
||
// Verify that the viewpoint is cleared
|
||
EXPECT_EQ(map.at<signed char>(
|
||
(viewpoints[id].y - yMin) / cellSize,
|
||
(viewpoints[id].x - xMin) / cellSize),
|
||
0);
|
||
|
||
// Verify that the hit cell is marked as obstacle (100)
|
||
EXPECT_EQ(map.at<signed char>(
|
||
(hit.at<cv::Vec2f>(0, 0)[1] - yMin) / cellSize,
|
||
(hit.at<cv::Vec2f>(0, 0)[0] - xMin) / cellSize),
|
||
100);
|
||
|
||
// Verify that ray tracing worked only up to max scan range (from offset viewpoint)
|
||
EXPECT_EQ(map.at<signed char>(
|
||
(hit.at<cv::Vec2f>(0, 1)[1] + viewpoints[id].y - yMin) / cellSize,
|
||
(scanMaxRange + viewpoints[id].x - xMin) / cellSize -1),
|
||
0);
|
||
|
||
EXPECT_EQ(map.at<signed char>(
|
||
(hit.at<cv::Vec2f>(0, 1)[1] + viewpoints[id].y - yMin) / cellSize,
|
||
(scanMaxRange + viewpoints[id].x - xMin) / cellSize),
|
||
-1);
|
||
|
||
// Verify that the nohit cell is marked as empty (0)
|
||
EXPECT_EQ(map.at<signed char>(
|
||
(noHit.at<cv::Vec2f>(0, 0)[1] - yMin) / cellSize -1, // ray tracing only up to the nohit cell
|
||
(noHit.at<cv::Vec2f>(0, 0)[0] - xMin) / cellSize),
|
||
0);
|
||
EXPECT_EQ(map.at<signed char>(
|
||
(noHit.at<cv::Vec2f>(0, 0)[1] - yMin) / cellSize,
|
||
(noHit.at<cv::Vec2f>(0, 0)[0] - xMin) / cellSize),
|
||
-1);
|
||
}
|
||
|
||
TEST(Util3dMappingTest, Occupancy2DFromLaserScanBasicTest)
|
||
{
|
||
// Synthetic scan with 3 hits and 2 no-hits (in 2D)
|
||
cv::Mat scanHit(1, 3, CV_32FC2);
|
||
scanHit.at<cv::Vec2f>(0, 0) = cv::Vec2f(1.0f, 0.0f);
|
||
scanHit.at<cv::Vec2f>(0, 1) = cv::Vec2f(0.0f, 1.0f);
|
||
scanHit.at<cv::Vec2f>(0, 2) = cv::Vec2f(1.0f, 1.0f);
|
||
|
||
cv::Mat scanNoHit(1, 2, CV_32FC2);
|
||
scanNoHit.at<cv::Vec2f>(0, 0) = cv::Vec2f(0.5f, 0.5f);
|
||
scanNoHit.at<cv::Vec2f>(0, 1) = cv::Vec2f(-1.0f, -1.0f);
|
||
|
||
cv::Point3f viewpoint(0.0f, 0.0f, 0.0f);
|
||
|
||
cv::Mat empty, occupied;
|
||
float cellSize = 0.1f;
|
||
float scanMaxRange = 2.0f;
|
||
|
||
util3d::occupancy2DFromLaserScan(scanHit, scanNoHit, viewpoint, empty, occupied, cellSize, true, scanMaxRange);
|
||
|
||
// Validate sizes
|
||
ASSERT_FALSE(occupied.empty()) << "Occupied matrix should not be empty.";
|
||
ASSERT_EQ(occupied.cols, 3) << "Expected 3 occupied points.";
|
||
ASSERT_EQ(occupied.type(), CV_32FC2) << "Occupied should be CV_32FC2.";
|
||
|
||
ASSERT_FALSE(empty.empty()) << "Empty matrix should not be empty.";
|
||
ASSERT_EQ(empty.type(), CV_32FC2) << "Empty should be CV_32FC2.";
|
||
ASSERT_GT(empty.cols, 20) << "There should be some empty cells.";
|
||
}
|
||
|
||
TEST(Util3dMappingTest, Create2DMapFromOccupancyLocalMapsBasic)
|
||
{
|
||
// Setup poses
|
||
std::map<int, Transform> poses;
|
||
poses[1] = Transform::getIdentity(); // Assume (0, 0)
|
||
|
||
// Setup occupancy data
|
||
std::map<int, std::pair<cv::Mat, cv::Mat>> occupancy;
|
||
|
||
// Create dummy empty cells
|
||
cv::Mat empty(1, 2, CV_32FC2);
|
||
empty.at<cv::Vec2f>(0, 0) = cv::Vec2f(1.0f, 1.0f);
|
||
empty.at<cv::Vec2f>(0, 1) = cv::Vec2f(2.0f, 1.0f);
|
||
|
||
// Create dummy occupied cells
|
||
cv::Mat occupied(1, 1, CV_32FC2);
|
||
occupied.at<cv::Vec2f>(0, 0) = cv::Vec2f(1.5f, 2.0f);
|
||
|
||
occupancy[1] = std::make_pair(empty, occupied);
|
||
|
||
// Output parameters
|
||
float xMin = 0.0f;
|
||
float yMin = 0.0f;
|
||
float cellSize = 0.1f;
|
||
|
||
// Run function
|
||
cv::Mat map = util3d::create2DMapFromOccupancyLocalMaps(
|
||
poses,
|
||
occupancy,
|
||
cellSize,
|
||
xMin,
|
||
yMin,
|
||
0.0f, // minMapSize
|
||
false, // erode
|
||
0.0f // footprintRadius
|
||
);
|
||
|
||
// Verify output
|
||
ASSERT_FALSE(map.empty());
|
||
ASSERT_EQ(map.type(), CV_8S);
|
||
|
||
// Check expected occupancy value at occupied cell
|
||
int col = static_cast<int>((occupied.at<cv::Vec2f>(0, 0)[0] - xMin) / cellSize);
|
||
int row = static_cast<int>((occupied.at<cv::Vec2f>(0, 0)[1] - yMin) / cellSize);
|
||
ASSERT_GE(row, 0);
|
||
ASSERT_GE(col, 0);
|
||
ASSERT_LT(row, map.rows);
|
||
ASSERT_LT(col, map.cols);
|
||
EXPECT_EQ(map.at<signed char>(row, col), 100); // Obstacle
|
||
|
||
// Check empty cells
|
||
int rowEmpty = static_cast<int>((empty.at<cv::Vec2f>(0, 0)[1] - yMin) / cellSize);
|
||
int colEmpty1 = static_cast<int>((empty.at<cv::Vec2f>(0, 0)[0] - xMin) / cellSize);
|
||
int colEmpty2 = static_cast<int>((empty.at<cv::Vec2f>(0, 1)[0] - xMin) / cellSize);
|
||
EXPECT_EQ(map.at<signed char>(rowEmpty, colEmpty1), 0); // Free
|
||
EXPECT_EQ(map.at<signed char>(rowEmpty, colEmpty2), 0); // Free
|
||
}
|
||
|
||
TEST(Util3dMappingTest, ConvertMap2Image8UBasicConversionNormalFormat)
|
||
{
|
||
// Create a simple 3x3 CV_8S occupancy grid
|
||
cv::Mat map8S = (cv::Mat_<signed char>(3, 3) <<
|
||
-1, 0, 100,
|
||
-2, 50, 75,
|
||
25, -1, 0);
|
||
|
||
// Call the function in normal format
|
||
cv::Mat result = util3d::convertMap2Image8U(map8S, false);
|
||
|
||
ASSERT_EQ(result.type(), CV_8U);
|
||
ASSERT_EQ(result.rows, 3);
|
||
ASSERT_EQ(result.cols, 3);
|
||
|
||
// Expected grayscale values for normal format
|
||
EXPECT_EQ(result.at<uchar>(0, 0), 89); // -1 (unknown)
|
||
EXPECT_EQ(result.at<uchar>(0, 1), 178); // 0 (free)
|
||
EXPECT_EQ(result.at<uchar>(0, 2), 0); // 100 (obstacle)
|
||
EXPECT_EQ(result.at<uchar>(1, 0), 200); // -2 (footprint)
|
||
EXPECT_EQ(result.at<uchar>(1, 1), 89); // 50
|
||
EXPECT_LT(result.at<uchar>(1, 2), 89); // 75 → scaled toward obstacle (0–89)
|
||
EXPECT_GT(result.at<uchar>(2, 0), 89); // 25 → scaled toward free (89–178)
|
||
EXPECT_EQ(result.at<uchar>(2, 1), 89); // -1
|
||
EXPECT_EQ(result.at<uchar>(2, 2), 178); // 0
|
||
}
|
||
|
||
TEST(Util3dMappingTest, ConvertMap2Image8UBasicConversionPGMFormat)
|
||
{
|
||
cv::Mat map8S = (cv::Mat_<signed char>(2, 2) <<
|
||
-1, 0,
|
||
-2, 100);
|
||
|
||
// Call with pgmFormat = true
|
||
cv::Mat result = util3d::convertMap2Image8U(map8S, true);
|
||
|
||
ASSERT_EQ(result.type(), CV_8U);
|
||
ASSERT_EQ(result.rows, 2);
|
||
ASSERT_EQ(result.cols, 2);
|
||
|
||
// Because pgmFormat flips vertically, test accordingly
|
||
EXPECT_EQ(result.at<uchar>(0, 0), 254); // 0 (was at [1,0])
|
||
EXPECT_EQ(result.at<uchar>(0, 1), 0); // 100
|
||
EXPECT_EQ(result.at<uchar>(1, 0), 205); // -1
|
||
EXPECT_EQ(result.at<uchar>(1, 1), 254); // -2
|
||
}
|
||
|
||
TEST(Util3dMappingTest, ConvertImage8U2MapNonPGMFormat)
|
||
{
|
||
// Create a 2x2 grayscale image using non-PGM expected values
|
||
cv::Mat input = (cv::Mat_<uchar>(2, 2) << 178, 0, 200, 89);
|
||
|
||
// Call conversion
|
||
cv::Mat map = util3d::convertImage8U2Map(input, false);
|
||
|
||
ASSERT_EQ(map.type(), CV_8S);
|
||
ASSERT_EQ(map.rows, 2);
|
||
ASSERT_EQ(map.cols, 2);
|
||
|
||
// Expected values: 0 (free), 100 (occupied), -2 (footprint), -1 (unknown)
|
||
EXPECT_EQ(map.at<signed char>(0, 0), 0);
|
||
EXPECT_EQ(map.at<signed char>(0, 1), 100);
|
||
EXPECT_EQ(map.at<signed char>(1, 0), -2);
|
||
EXPECT_EQ(map.at<signed char>(1, 1), -1);
|
||
}
|
||
|
||
TEST(Util3dMappingTest, ConvertImage8U2MapPGMFormat)
|
||
{
|
||
// Create a 2x2 grayscale image using PGM expected values
|
||
cv::Mat input = (cv::Mat_<uchar>(2, 2) << 254, 0, 205, 205);
|
||
|
||
// Call conversion
|
||
cv::Mat map = util3d::convertImage8U2Map(input, true);
|
||
|
||
ASSERT_EQ(map.type(), CV_8S);
|
||
ASSERT_EQ(map.rows, 2);
|
||
ASSERT_EQ(map.cols, 2);
|
||
|
||
// Because PGM inverts rows vertically, we need to check accordingly
|
||
// map.at<signed char>(i, j) corresponds to input.at<uchar>((rows-1)-i, j)
|
||
EXPECT_EQ(map.at<signed char>(0, 0), -1); // input(1, 0) = 205
|
||
EXPECT_EQ(map.at<signed char>(0, 1), -1); // input(1, 1) = 205
|
||
EXPECT_EQ(map.at<signed char>(1, 0), 0); // input(0, 0) = 254
|
||
EXPECT_EQ(map.at<signed char>(1, 1), 100); // input(0, 1) = 0
|
||
}
|
||
|
||
TEST(Util3dMappingTest, ErodeMapBasicErosion)
|
||
{
|
||
// Create a 5x5 map with CV_8SC1 type
|
||
// -1 = unknown, 0 = free, 100 = obstacle
|
||
cv::Mat map = (cv::Mat_<signed char>(5,5) <<
|
||
0, 0, 0, 0, 0,
|
||
0, 100, 100, 100, 0,
|
||
0, 0, 100, 100, 100,
|
||
0, 100, 100, 100, 100,
|
||
0, 0, 100, 100, 100);
|
||
|
||
cv::Mat erodedMap = util3d::erodeMap(map);
|
||
|
||
cv::Mat expected = (cv::Mat_<signed char>(5,5) <<
|
||
0, 0, 0, 0, 0,
|
||
0, 0, 100, 100, 0,
|
||
0, 0, 100, 100, 100,
|
||
0, 0, 100, 100, 100,
|
||
0, 0, 100, 100, 100);
|
||
|
||
cv::Mat diff;
|
||
cv::compare(erodedMap, expected, diff, cv::CMP_NE);
|
||
// Count non-zero elements in diff (means different)
|
||
EXPECT_EQ(cv::countNonZero(diff), 0);
|
||
|
||
}
|
||
|
||
TEST(Util3dMappingTest, ErodeMapNoErosionWithUnknown)
|
||
{
|
||
// Create a 3x3 map with obstacle touching unknown cell
|
||
cv::Mat map = (cv::Mat_<signed char>(3,3) <<
|
||
0, 0, 0,
|
||
0, 100, -1,
|
||
0, 0, 0);
|
||
|
||
cv::Mat erodedMap = util3d::erodeMap(map);
|
||
|
||
// Obstacle should NOT be eroded because adjacent unknown cell (-1)
|
||
EXPECT_EQ(erodedMap.at<signed char>(1,1), 100);
|
||
}
|
||
|
||
TEST(Util3dMappingTest, ProjectCloudOnXYPlaneZCoordinatesAreZero)
|
||
{
|
||
// Create a test point cloud
|
||
pcl::PointCloud<pcl::PointXYZ>::Ptr input_cloud(new pcl::PointCloud<pcl::PointXYZ>);
|
||
input_cloud->push_back(pcl::PointXYZ(1.0, 2.0, 3.0));
|
||
input_cloud->push_back(pcl::PointXYZ(4.0, 5.0, -1.0));
|
||
input_cloud->push_back(pcl::PointXYZ(0.0, 0.0, 10.0));
|
||
|
||
// Project the cloud
|
||
auto projected_cloud = util3d::projectCloudOnXYPlane<pcl::PointXYZ>(*input_cloud);
|
||
|
||
// Check that the size remains the same
|
||
ASSERT_EQ(projected_cloud->size(), input_cloud->size());
|
||
|
||
// Check that x and y remain the same, and z is set to zero
|
||
for (size_t i = 0; i < projected_cloud->size(); ++i)
|
||
{
|
||
EXPECT_FLOAT_EQ(projected_cloud->at(i).x, input_cloud->at(i).x);
|
||
EXPECT_FLOAT_EQ(projected_cloud->at(i).y, input_cloud->at(i).y);
|
||
EXPECT_FLOAT_EQ(projected_cloud->at(i).z, 0.0f);
|
||
}
|
||
}
|
||
|
||
TEST(Util3dMappingTest, SegmentObstaclesFromGround)
|
||
{
|
||
// Create a cloud of a floor, then elevate some part of it to make a flat obstacle
|
||
pcl::IndicesPtr expected_ground(new std::vector<int>);
|
||
pcl::IndicesPtr expected_big_obstacles(new std::vector<int>);
|
||
pcl::IndicesPtr expected_small_obstacles(new std::vector<int>);
|
||
pcl::IndicesPtr expected_flat_obstacles(new std::vector<int>);
|
||
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
|
||
for(int i=0; i<10; ++i) {
|
||
for(int j=0; j<10; ++j) {
|
||
if(i>5 && j>5) {
|
||
expected_flat_obstacles->push_back(cloud->size());
|
||
}
|
||
else {
|
||
expected_ground->push_back(cloud->size());
|
||
}
|
||
cloud->push_back(pcl::PointXYZ(0.05f*i, 0.05f*j, 0.01f*i+(i>5 && j>5 ? 0.25f:0.0f)));
|
||
}
|
||
}
|
||
// Add a wall and a small obstacle
|
||
for(int i=0; i<10; ++i) {
|
||
for(int k=0; k<10; ++k) {
|
||
if(i>5 && k>5) {
|
||
expected_small_obstacles->push_back(cloud->size());
|
||
cloud->push_back(pcl::PointXYZ(0.05f*i, -0.35f, 0.05f*k));
|
||
}
|
||
expected_big_obstacles->push_back(cloud->size());
|
||
cloud->push_back(pcl::PointXYZ(0.05f*i, -0.15f, 0.05f*k));
|
||
}
|
||
}
|
||
|
||
pcl::IndicesPtr ground, obstacles, flatObs;
|
||
|
||
// test basic
|
||
float normalKSearch = 5;
|
||
float angleMax = 20.0f*M_PI/180.0f; // Allow 20 degrees of deviation
|
||
float clusterRadius = 0.1f;
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
1, // minClusterSize
|
||
false, // segmentFlatObstacles
|
||
0.0f, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,2,0), // viewpoint
|
||
0.8f // groundNormalsUp
|
||
);
|
||
|
||
ASSERT_EQ(ground->size(), expected_ground->size() + expected_flat_obstacles->size());
|
||
ASSERT_EQ(obstacles->size(), expected_big_obstacles->size() + expected_small_obstacles->size());
|
||
|
||
// test indices
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
expected_ground,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
1, // minClusterSize
|
||
false, // segmentFlatObstacles
|
||
0.0f, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,2,0), // viewpoint
|
||
0.8f // groundNormalsUp
|
||
);
|
||
|
||
ASSERT_EQ(ground->size(), expected_ground->size());
|
||
ASSERT_EQ(obstacles->size(), 0);
|
||
|
||
// test flat obstacles
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
1, // minClusterSize
|
||
true, // segmentFlatObstacles
|
||
0.0f, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,2,0), // viewpoint
|
||
0.8f // groundNormalsUp
|
||
);
|
||
|
||
ASSERT_EQ(flatObs->size(), expected_flat_obstacles->size());
|
||
ASSERT_EQ(ground->size(), expected_ground->size());
|
||
ASSERT_EQ(obstacles->size(), expected_big_obstacles->size() + expected_small_obstacles->size() + expected_flat_obstacles->size());
|
||
|
||
// test flat obstacles with maxGroundHeight
|
||
for(int i=0; i<2; ++i) {
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
1, // minClusterSize
|
||
i==0, // segmentFlatObstacles
|
||
0.1f, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,2,0), // viewpoint
|
||
0.8f // groundNormalsUp
|
||
);
|
||
|
||
ASSERT_EQ(flatObs->size(), expected_flat_obstacles->size());
|
||
ASSERT_EQ(ground->size(), expected_ground->size());
|
||
// all obstacles under maxGroundHeight are ignored
|
||
ASSERT_EQ(obstacles->size(), expected_big_obstacles->size() + expected_small_obstacles->size() + expected_flat_obstacles->size() - 20);
|
||
}
|
||
|
||
// test viewpoint (ceiling segmentation)
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
1, // minClusterSize
|
||
false, // segmentFlatObstacles
|
||
0.0f, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,0.15f,0), // viewpoint under the top flat obstacle
|
||
0.8f // groundNormalsUp
|
||
);
|
||
|
||
ASSERT_EQ(ground->size(), expected_ground->size());
|
||
ASSERT_EQ(obstacles->size(), expected_big_obstacles->size() + expected_small_obstacles->size() + expected_flat_obstacles->size());
|
||
|
||
// test min cluster radius
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
17, // minClusterSize
|
||
false, // segmentFlatObstacles
|
||
0.0f, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,2,0),
|
||
0.8f // groundNormalsUp
|
||
);
|
||
|
||
ASSERT_EQ(ground->size(), expected_ground->size());
|
||
ASSERT_EQ(obstacles->size(), expected_big_obstacles->size());
|
||
|
||
// Everything obstacles
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
1, // minClusterSize
|
||
false, // segmentFlatObstacles
|
||
-0.1f, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,2,0),
|
||
0.8f // groundNormalsUp
|
||
);
|
||
|
||
ASSERT_EQ(ground->size(), 0);
|
||
ASSERT_EQ(obstacles->size(), expected_ground->size() + expected_big_obstacles->size() + expected_small_obstacles->size() + expected_flat_obstacles->size());
|
||
|
||
// Everything ground or ignored
|
||
for(int i=0; i<2; ++i) {
|
||
util3d::segmentObstaclesFromGround<pcl::PointXYZ>(
|
||
cloud,
|
||
ground,
|
||
obstacles,
|
||
normalKSearch,
|
||
angleMax,
|
||
clusterRadius,
|
||
1, // minClusterSize
|
||
i==0, // segmentFlatObstacles
|
||
10, // maxGroundHeight
|
||
&flatObs, // flatObstacles
|
||
Eigen::Vector4f(0,0,2,0),
|
||
0.8f // groundNormalsUp
|
||
);
|
||
// wether we segment or not, all flat surfaces are under 10 meters
|
||
ASSERT_EQ(ground->size(), expected_ground->size() + expected_flat_obstacles->size());
|
||
ASSERT_EQ(obstacles->size(), 0);
|
||
}
|
||
}
|
||
|
||
TEST(Util3dMappingTest, Occupancy2DFromGroundObstaclesBasic)
|
||
{
|
||
pcl::PointCloud<pcl::PointXYZ>::Ptr groundCloud(new pcl::PointCloud<pcl::PointXYZ>);
|
||
pcl::PointCloud<pcl::PointXYZ>::Ptr obstaclesCloud(new pcl::PointCloud<pcl::PointXYZ>);
|
||
|
||
// Ground points (clustered near 0,0)
|
||
groundCloud->push_back(pcl::PointXYZ(0.05f, 0.05f, -0.2f));
|
||
groundCloud->push_back(pcl::PointXYZ(0.06f, 0.04f, -0.1f));
|
||
groundCloud->push_back(pcl::PointXYZ(0.5f, 0.5f, -0.2f)); // Separate voxel
|
||
|
||
// Obstacle points
|
||
obstaclesCloud->push_back(pcl::PointXYZ(1.0f, 1.0f, 1.0f));
|
||
obstaclesCloud->push_back(pcl::PointXYZ(1.02f, 1.01f, 1.2f)); // Same voxel
|
||
obstaclesCloud->push_back(pcl::PointXYZ(2.0f, 2.0f, 1.5f));
|
||
|
||
cv::Mat groundMat, obstaclesMat;
|
||
float cellSize = 0.1f;
|
||
|
||
util3d::occupancy2DFromGroundObstacles<pcl::PointXYZ>(groundCloud, obstaclesCloud, groundMat, obstaclesMat, cellSize);
|
||
|
||
// Check that points were voxelized and projected
|
||
EXPECT_EQ(groundMat.rows, 1);
|
||
EXPECT_EQ(groundMat.cols, 2); // Expect 2 distinct voxels
|
||
EXPECT_EQ(groundMat.type(), CV_32FC2);
|
||
|
||
EXPECT_NEAR(groundMat.at<cv::Vec2f>(0)[0], 0.055, 0.001);
|
||
EXPECT_NEAR(groundMat.at<cv::Vec2f>(0)[1], 0.045, 0.001);
|
||
EXPECT_NEAR(groundMat.at<cv::Vec2f>(1)[0], 0.5, 0.001);
|
||
EXPECT_NEAR(groundMat.at<cv::Vec2f>(1)[1], 0.5, 0.001);
|
||
|
||
EXPECT_EQ(obstaclesMat.rows, 1);
|
||
EXPECT_EQ(obstaclesMat.cols, 2); // Expect 2 voxels (1.0,1.0) and (2.0,2.0)
|
||
EXPECT_EQ(obstaclesMat.type(), CV_32FC2);
|
||
|
||
EXPECT_NEAR(obstaclesMat.at<cv::Vec2f>(0)[0], 1.01, 0.001);
|
||
EXPECT_NEAR(obstaclesMat.at<cv::Vec2f>(0)[1], 1.005, 0.001);
|
||
EXPECT_NEAR(obstaclesMat.at<cv::Vec2f>(1)[0], 2, 0.001);
|
||
EXPECT_NEAR(obstaclesMat.at<cv::Vec2f>(1)[1], 2, 0.001);
|
||
} |