mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-03 16:47:47 +08:00
added doc/gtest for util3d_mapping.h (missing hpp functions)
This commit is contained in:
@@ -62,6 +62,33 @@ RTABMAP_DEPRECATED void RTABMAP_CORE_EXPORT occupancy2DFromLaserScan(
|
||||
bool unknownSpaceFilled = false,
|
||||
float scanMaxRange = 0.0f);
|
||||
|
||||
/**
|
||||
* @brief Generates 2D occupancy grid maps (free and occupied cells) from laser scan data.
|
||||
*
|
||||
* This function takes in a laser scan composed of hit and no-hit points and computes two
|
||||
* 2D maps:
|
||||
* - `empty`: 2D coordinates of free space (where the laser passed without hitting obstacles).
|
||||
* - `occupied`: precise 2D coordinates of obstacle hits (where the laser reflected).
|
||||
*
|
||||
* The internal representation uses a temporary occupancy map generated from `create2DMap()`,
|
||||
* from which free cells are extracted. Obstacle points are directly passed through, potentially
|
||||
* clipped by a maximum range filter.
|
||||
*
|
||||
* @param[in] scanHitIn CV_32FC2 or CV_32FC(n>=2) matrix representing obstacle hits in 2D or 3D space (relative to base frame, not laser frame).
|
||||
* @param[in] scanNoHitIn CV_32FC2 or CV_32FC(n>=2) matrix representing laser rays that did not hit an obstacle (relative to base frame, not laser frame).
|
||||
* @param[in] viewpoint The viewpoint (sensor origin) from which the scan was taken, in 3D space, relative to base frame.
|
||||
* This used to determinate the origin of ray tracing.
|
||||
* @param[out] empty Output matrix (CV_32FC2) of free space points derived from ray tracing.
|
||||
* @param[out] occupied Output matrix (CV_32FC2) of occupied (hit) points, filtered by max range if required.
|
||||
* @param[in] cellSize The resolution of the occupancy map grid (in meters per cell).
|
||||
* @param[in] unknownSpaceFilled If true, unknown space between hits is also filled via ray tracing.
|
||||
* @param[in] scanMaxRange Maximum range of the scan (in meters). Values beyond this are clipped.
|
||||
*
|
||||
* @note The function assumes a single scan already converted in base frame for internal processing.
|
||||
* @note If `scanMaxRange <= cellSize`, no range filtering is applied to the occupied points.
|
||||
*
|
||||
* @see create2DMap(), util3d::rangeFiltering()
|
||||
*/
|
||||
void RTABMAP_CORE_EXPORT occupancy2DFromLaserScan(
|
||||
const cv::Mat & scanHit, // in /base_link frame
|
||||
const cv::Mat & scanNoHit, // in /base_link frame
|
||||
@@ -72,6 +99,42 @@ void RTABMAP_CORE_EXPORT occupancy2DFromLaserScan(
|
||||
bool unknownSpaceFilled = false,
|
||||
float scanMaxRange = 0.0f); // would be set if unknownSpaceFilled=true
|
||||
|
||||
/**
|
||||
* @brief Creates a 2D occupancy grid map from local occupancy data.
|
||||
*
|
||||
* Generates a 2D occupancy grid (`CV_8S`) where:
|
||||
* - -1 indicates unknown space,
|
||||
* - 0 indicates free space,
|
||||
* - 100 indicates an occupied (obstacle) cell.
|
||||
*
|
||||
* This function transforms and merges local empty/occupied occupancy maps
|
||||
* from multiple robot poses into a single global 2D grid map.
|
||||
*
|
||||
* @param posesIn Map of robot poses, indexed by node ID.
|
||||
* @param occupancy Map of local occupancy data, indexed by node ID.
|
||||
* Each pair contains two cv::Mat elements:
|
||||
* - First: empty cells (CV_32FC2) relative to base frame,
|
||||
* - Second: occupied cells (CV_32FC2) relative to base frame.
|
||||
* @param cellSize The resolution of the map in meters per cell.
|
||||
* @param[out] xMin Minimum x-coordinate (origin offset) of the resulting map (in meters).
|
||||
* @param[out] yMin Minimum y-coordinate (origin offset) of the resulting map (in meters).
|
||||
* @param minMapSize Minimum width/height of the output map in meters.
|
||||
* If 0, size is computed from poses and occupancy data.
|
||||
* @param erode Whether to post-process (erode) noisy obstacles.
|
||||
* This helps remove isolated or thin obstacle artifacts.
|
||||
* @param footprintRadius Radius of the robot footprint (in meters).
|
||||
* Free space will be cleared under the robot.
|
||||
*
|
||||
* @return cv::Mat The resulting occupancy grid map (CV_8S):
|
||||
* -1 = unknown,
|
||||
* 0 = free space,
|
||||
* 100 = obstacle.
|
||||
*
|
||||
* @warning The output map can be very large if poses are far apart or cellSize is small.
|
||||
* The function will not create a map if the estimated size exceeds reasonable limits (e.g. > 1.5 km).
|
||||
*
|
||||
* @note The function will fill small holes surrounded by known cells and optionally erode noisy borders.
|
||||
*/
|
||||
cv::Mat RTABMAP_CORE_EXPORT create2DMapFromOccupancyLocalMaps(
|
||||
const std::map<int, Transform> & poses,
|
||||
const std::map<int, std::pair<cv::Mat, cv::Mat> > & occupancy,
|
||||
@@ -104,19 +167,39 @@ RTABMAP_DEPRECATED cv::Mat RTABMAP_CORE_EXPORT create2DMap(const std::map<int, T
|
||||
float scanMaxRange = 0.0f);
|
||||
|
||||
/**
|
||||
* Create 2d Occupancy grid (CV_8S)
|
||||
* -1 = unknown
|
||||
* 0 = empty space
|
||||
* 100 = obstacle
|
||||
* @param poses
|
||||
* @param scans, should be CV_32FC2 type!
|
||||
* @param viewpoints
|
||||
* @param cellSize m
|
||||
* @param unknownSpaceFilled if false no fill, otherwise a virtual laser sweeps the unknown space from each pose (stopping on detected obstacle)
|
||||
* @param xMin
|
||||
* @param yMin
|
||||
* @param minMapSize minimum map size in meters
|
||||
* @param scanMaxRange laser scan maximum range, would be set if unknownSpaceFilled=true
|
||||
* @brief Generates a 2D occupancy grid map from a set of poses, laser scans, and viewpoints.
|
||||
*
|
||||
* This function aggregates laser scan data from multiple poses and creates a 2D occupancy grid
|
||||
* (CV_8S: signed 8-bit) where:
|
||||
* - `-1` represents unknown space,
|
||||
* - `0` represents free space,
|
||||
* - `100` represents occupied space (obstacles).
|
||||
*
|
||||
* The scans are first transformed into the global map frame using the corresponding pose.
|
||||
* Obstacles and free space are inserted using ray tracing. Optionally, unknown areas between
|
||||
* known rays can also be filled using radial sweeping.
|
||||
*
|
||||
* @param poses A map of node IDs to 3D poses (used to transform local scans to the global frame).
|
||||
* @param scans A map of node IDs to pairs of laser scans (`<hit, no-hit>`), each as a `cv::Mat` of type `CV_32FC2`.
|
||||
* - `first`: endpoints of beams hitting obstacles (relative to base frame, not laser frame).
|
||||
* - `second`: endpoints of beams not hitting any obstacle (relative to base frame, not laser frame).
|
||||
* @param viewpoints A map of node IDs to local sensor origin offsets relative to each pose (e.g., lidar offset /base_link -> /base_scan).
|
||||
* This is used to determinate the starting point for each ray trace.
|
||||
* @param cellSize The size of each grid cell in meters.
|
||||
* @param unknownSpaceFilled If true, fills areas between known rays (fan sweeping) up to `scanMaxRange`.
|
||||
* @param[out] xMin The minimum x value (in meters) of the grid origin relative to map coordinates.
|
||||
* @param[out] yMin The minimum y value (in meters) of the grid origin relative to map coordinates.
|
||||
* @param minMapSize The minimum width and height (in meters) of the map. Ensures the output map has a minimum footprint.
|
||||
* @param scanMaxRange The maximum range (in meters) of the sensor. Used to limit ray tracing and padding.
|
||||
*
|
||||
* @return A 2D occupancy grid map (`cv::Mat` of type `CV_8S`) where:
|
||||
* - `-1` = unknown
|
||||
* - `0` = free space
|
||||
* - `100` = obstacle
|
||||
*
|
||||
* @note If `scanMaxRange <= 0`, map size is determined based on scan data bounds.
|
||||
* @note Grid coordinates are calculated with padding to ensure all points fall within the map.
|
||||
* @note This function uses ray tracing internally via the `rayTrace()` function.
|
||||
*/
|
||||
cv::Mat RTABMAP_CORE_EXPORT create2DMap(const std::map<int, Transform> & poses,
|
||||
const std::map<int, std::pair<cv::Mat, cv::Mat> > & scans, // <id, <hit, no hit> >, in /base_link frame
|
||||
@@ -126,16 +209,106 @@ cv::Mat RTABMAP_CORE_EXPORT create2DMap(const std::map<int, Transform> & poses,
|
||||
float & xMin,
|
||||
float & yMin,
|
||||
float minMapSize = 0.0f,
|
||||
float scanMaxRange = 0.0f); // would be set if unknownSpaceFilled=true
|
||||
float scanMaxRange = 0.0f);
|
||||
|
||||
/**
|
||||
* @brief Performs a 2D ray tracing operation between two points on a grid map.
|
||||
*
|
||||
* This function draws a line from the `start` point to the `end` point on a grid (e.g., occupancy grid),
|
||||
* marking all traversed cells as free (value = 0) unless an obstacle (value = 100) is encountered.
|
||||
* The line follows an integer rasterization algorithm (like Bresenham’s line), accounting for steep slopes
|
||||
* by transposing axes when needed.
|
||||
*
|
||||
* @param start The starting point of the ray (2D grid coordinates).
|
||||
* @param end The ending point of the ray (2D grid coordinates). This point is clipped to the grid bounds.
|
||||
* @param grid A mutable 2D grid represented as a `cv::Mat` of signed char values.
|
||||
* Assumes 100 denotes obstacles; 0 denotes free space.
|
||||
* @param stopOnObstacle If true, the ray trace stops upon hitting a cell marked with 100 (an obstacle).
|
||||
*
|
||||
* @note
|
||||
* - If the slope of the line is steep (outside the range [-1, 1]), the algorithm swaps x and y axes for correctness.
|
||||
* - The function ensures both the start and end points are within the bounds of the grid.
|
||||
* - All visited cells along the path (except obstacles when `stopOnObstacle` is true) will be updated to 0 (free).
|
||||
* - The grid must have type `CV_8SC1` (signed 8-bit single-channel matrix).
|
||||
*
|
||||
*/
|
||||
void RTABMAP_CORE_EXPORT rayTrace(const cv::Point2i & start,
|
||||
const cv::Point2i & end,
|
||||
cv::Mat & grid,
|
||||
bool stopOnObstacle);
|
||||
|
||||
/**
|
||||
* @brief Converts an occupancy grid map (CV_8S) to a grayscale image (CV_8U).
|
||||
*
|
||||
* This function takes a signed 8-bit occupancy grid map and produces a corresponding
|
||||
* 8-bit unsigned grayscale image. The pixel values are mapped based on the occupancy values:
|
||||
*
|
||||
* - `0` (free space) → 178 (normal) or 254 (PGM format)
|
||||
* - `100` (obstacle) → 0 (black)
|
||||
* - `-2` (robot footprint) → 200 (normal) or 254 (PGM format)
|
||||
* - `-1` (unknown) → 89 (normal) or 205 (PGM format)
|
||||
* - `v > 50` (partial obstacle): scaled to range [0, 89]
|
||||
* - `v < 50` (partial free): scaled to range [89, 178]
|
||||
*
|
||||
* If `pgmFormat` is true, the vertical axis is flipped (for PGM format compatibility).
|
||||
*
|
||||
* @param map8S The input occupancy grid map as a CV_8S single-channel matrix.
|
||||
* Must contain values such as -1 (unknown), 0 (free), 100 (occupied).
|
||||
* @param pgmFormat If true, output will be formatted for PGM file format (inverted Y-axis and different gray scale mapping).
|
||||
*
|
||||
* @return A CV_8U grayscale image with pixel values representing occupancy status.
|
||||
*
|
||||
* @throws UASSERT if the input map is not a single-channel CV_8S matrix.
|
||||
*/
|
||||
cv::Mat RTABMAP_CORE_EXPORT convertMap2Image8U(const cv::Mat & map8S, bool pgmFormat = false);
|
||||
|
||||
/**
|
||||
* @brief Converts a grayscale occupancy image (CV_8U) to an occupancy grid map (CV_8S).
|
||||
*
|
||||
* This function interprets grayscale pixel values from an input image and converts
|
||||
* them into occupancy values used in a typical occupancy grid map:
|
||||
* - 100: Occupied
|
||||
* - 0: Free
|
||||
* - -1: Unknown
|
||||
* - -2: Free space under robot footprint (non-PGM only)
|
||||
*
|
||||
* The interpretation differs slightly depending on whether the input image is in
|
||||
* PGM format (common in ROS map_server) or in standard grayscale.
|
||||
*
|
||||
* @param map8U Input grayscale image (type CV_8U, single-channel).
|
||||
* @param pgmFormat If true, assumes PGM format:
|
||||
* - 0 = occupied
|
||||
* - 254 = free
|
||||
* - 205 = unknown
|
||||
* If false (normal format):
|
||||
* - 0 = occupied
|
||||
* - 178 = free
|
||||
* - 200 = footprint (free space under robot)
|
||||
* - 89 = unknown
|
||||
*
|
||||
* @return A CV_8S occupancy grid map with encoded occupancy values.
|
||||
*
|
||||
* @throws Assertion failure if input image is not of type CV_8U or not single-channel.
|
||||
*/
|
||||
cv::Mat RTABMAP_CORE_EXPORT convertImage8U2Map(const cv::Mat & map8U, bool pgmFormat = false);
|
||||
|
||||
/**
|
||||
* @brief Performs erosion on an occupancy grid map to reduce small noisy obstacles.
|
||||
*
|
||||
* This function scans a given occupancy grid (`CV_8SC1` format) and removes
|
||||
* obstacle cells (value `100`) that are surrounded by at least 3 empty cells (value `0`)
|
||||
* and no adjacent unknown cells (value `-1`). These obstacles are likely noise
|
||||
* and are converted into empty space (value `0`) in the resulting map.
|
||||
*
|
||||
* @param map Input occupancy grid map of type `CV_8SC1` where:
|
||||
* - `100` represents obstacles,
|
||||
* - `0` represents free space,
|
||||
* - `-1` represents unknown space.
|
||||
*
|
||||
* @return A new `cv::Mat` of the same size and type as the input map,
|
||||
* with eroded obstacles.
|
||||
*
|
||||
*/
|
||||
cv::Mat RTABMAP_CORE_EXPORT erodeMap(const cv::Mat & map);
|
||||
|
||||
template<typename PointT>
|
||||
|
||||
@@ -818,18 +818,29 @@ void rayTrace(const cv::Point2i & start, const cv::Point2i & end, cv::Mat & grid
|
||||
ptA = start;
|
||||
ptB = end;
|
||||
|
||||
float slope = float(ptB.y - ptA.y)/float(ptB.x - ptA.x);
|
||||
|
||||
// clip end point
|
||||
ptB.x = std::min(std::max(ptB.x, 0), grid.cols-1);
|
||||
ptB.y = std::min(std::max(ptB.y, 0), grid.rows-1);
|
||||
|
||||
if(ptA == ptB) {
|
||||
signed char * v = &grid.at<signed char>(ptA.y, ptA.x);
|
||||
if(*v == 100 && stopOnObstacle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
*v = 0; // free space
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float slope = ptB.x - ptA.x != 0 ? float(ptB.y - ptA.y)/float(ptB.x - ptA.x) : std::numeric_limits<float>::quiet_NaN();
|
||||
|
||||
bool swapped = false;
|
||||
if(slope<-1.0f || slope>1.0f)
|
||||
if(!uIsFinite(slope) || slope<-1.0f || slope>1.0f)
|
||||
{
|
||||
// swap x and y
|
||||
slope = 1.0f/slope;
|
||||
|
||||
int tmp = ptA.x;
|
||||
ptA.x = ptA.y;
|
||||
ptA.y = tmp;
|
||||
@@ -838,6 +849,13 @@ void rayTrace(const cv::Point2i & start, const cv::Point2i & end, cv::Mat & grid
|
||||
ptB.x = ptB.y;
|
||||
ptB.y = tmp;
|
||||
|
||||
if(!uIsFinite(slope)) {
|
||||
slope = 0.0f;
|
||||
}
|
||||
else {
|
||||
slope = 1.0f/slope;
|
||||
}
|
||||
|
||||
swapped = true;
|
||||
}
|
||||
|
||||
@@ -860,13 +878,13 @@ void rayTrace(const cv::Point2i & start, const cv::Point2i & end, cv::Mat & grid
|
||||
|
||||
if(!swapped)
|
||||
{
|
||||
UASSERT_MSG(lowerbound >= 0 && lowerbound < grid.rows, uFormat("lowerbound=%f grid.rows=%d x=%d slope=%f b=%f x=%f", lowerbound, grid.rows, x, slope, b, x).c_str());
|
||||
UASSERT_MSG(upperbound >= 0 && upperbound < grid.rows, uFormat("upperbound=%f grid.rows=%d x+1=%d slope=%f b=%f x=%f", upperbound, grid.rows, x+1, slope, b, x).c_str());
|
||||
UASSERT_MSG(lowerbound >= 0 && lowerbound < grid.rows, uFormat("lowerbound=%d upperbound=%d grid.rows=%d x=%d slope=%f b=%f", lowerbound, upperbound, grid.rows, x, slope, b).c_str());
|
||||
UASSERT_MSG(upperbound >= 0 && upperbound < grid.rows, uFormat("lowerbound=%d upperbound=%d grid.rows=%d x+1=%d slope=%f b=%f", lowerbound, upperbound, grid.rows, x+1, slope, b).c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
UASSERT_MSG(lowerbound >= 0 && lowerbound < grid.cols, uFormat("lowerbound=%f grid.cols=%d x=%d slope=%f b=%f x=%f", lowerbound, grid.cols, x, slope, b, x).c_str());
|
||||
UASSERT_MSG(upperbound >= 0 && upperbound < grid.cols, uFormat("upperbound=%f grid.cols=%d x+1=%d slope=%f b=%f x=%f", upperbound, grid.cols, x+1, slope, b, x).c_str());
|
||||
UASSERT_MSG(lowerbound >= 0 && lowerbound < grid.cols, uFormat("lowerbound=%d upperbound=%d grid.cols=%d x=%d slope=%f b=%f", lowerbound, upperbound, grid.cols, x, slope, b).c_str());
|
||||
UASSERT_MSG(upperbound >= 0 && upperbound < grid.cols, uFormat("lowerbound=%d upperbound=%d grid.cols=%d x+1=%d slope=%f b=%f", lowerbound, upperbound, grid.cols, x+1, slope, b).c_str());
|
||||
}
|
||||
|
||||
for(int y = lowerbound; y<=(int)upperbound; ++y)
|
||||
@@ -989,9 +1007,9 @@ cv::Mat erodeMap(const cv::Mat & map)
|
||||
{
|
||||
UASSERT(map.type() == CV_8SC1);
|
||||
cv::Mat erodedMap = map.clone();
|
||||
for(int i=0; i<map.rows; ++i)
|
||||
for(int i=1; i<map.rows-1; ++i)
|
||||
{
|
||||
for(int j=0; j<map.cols; ++j)
|
||||
for(int j=1; j<map.cols-1; ++j)
|
||||
{
|
||||
if(map.at<signed char>(i, j) == 100)
|
||||
{
|
||||
|
||||
@@ -34,4 +34,9 @@ gtest_discover_tests(test_util3d_features)
|
||||
#util3d_correspondences.h
|
||||
add_executable(test_util3d_correspondences test_util3d_correspondences.cpp)
|
||||
target_link_libraries(test_util3d_correspondences gtest_main rtabmap_core)
|
||||
gtest_discover_tests(test_util3d_correspondences)
|
||||
gtest_discover_tests(test_util3d_correspondences)
|
||||
|
||||
#util3d_mapping.h
|
||||
add_executable(test_util3d_mapping test_util3d_mapping.cpp)
|
||||
target_link_libraries(test_util3d_mapping gtest_main rtabmap_core)
|
||||
gtest_discover_tests(test_util3d_mapping)
|
||||
@@ -0,0 +1,421 @@
|
||||
#include "gtest/gtest.h"
|
||||
#include "rtabmap/core/util3d.h"
|
||||
#include "rtabmap/core/util3d_mapping.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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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(Util3dMapping, 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);
|
||||
}
|
||||
Reference in New Issue
Block a user