#include "gtest/gtest.h" #include "rtabmap/core/util2d.h" #include "rtabmap/utilite/UException.h" #include "rtabmap/core/Features2d.h" #include "rtabmap/core/util3d.h" #include "rtabmap/core/util3d_filtering.h" #include "pcl/io/pcd_io.h" using namespace rtabmap; TEST(Util2dTest, SsdIdenticalImages) { cv::Mat img1 = (cv::Mat_(3,3) << 10, 20, 30, 40, 50, 60, 70, 80, 90); cv::Mat img2 = img1.clone(); float score = util2d::ssd(img1, img2); EXPECT_FLOAT_EQ(score, 0.0f); } TEST(Util2dTest, SsdDifferentImages) { cv::Mat img1 = (cv::Mat_(2,2) << 10, 20, 30, 40); cv::Mat img2 = (cv::Mat_(2,2) << 11, 19, 31, 39); float score = util2d::ssd(img1, img2); float expected = pow(10 - 11, 2) + pow(20 - 19, 2) + pow(30 - 31, 2) + pow(40 - 39, 2); EXPECT_FLOAT_EQ(score, expected); } TEST(Util2dTest, SsdStereoLikeInput) { cv::Mat left = (cv::Mat_(1,1) << cv::Vec2s(10, 20)); cv::Mat right = (cv::Mat_(1,1) << cv::Vec2s(11, 19)); float avgL = (10 + 20) / 2.0f; float avgR = (11 + 19) / 2.0f; float expected = (avgL - avgR) * (avgL - avgR); EXPECT_FLOAT_EQ(util2d::ssd(left, right), expected); } TEST(Util2dTest, SadIdenticalImages) { cv::Mat img1 = (cv::Mat_(2,2) << 1.0, 2.0, 3.0, 4.0); cv::Mat img2 = img1.clone(); float score = util2d::sad(img1, img2); EXPECT_FLOAT_EQ(score, 0.0f); } TEST(Util2dTest, SadDifferentImages) { cv::Mat img1 = (cv::Mat_(2,2) << 1.0, 2.0, 3.0, 4.0); cv::Mat img2 = (cv::Mat_(2,2) << 0.5, 2.5, 2.5, 5.0); float score = fabs(1.0 - 0.5) + fabs(2.0 - 2.5) + fabs(3.0 - 2.5) + fabs(4.0 - 5.0); EXPECT_FLOAT_EQ(util2d::sad(img1, img2), score); } TEST(Util2dTest, SadStereoLikeInput) { cv::Mat left = (cv::Mat_(1,1) << cv::Vec2s(5, 15)); cv::Mat right = (cv::Mat_(1,1) << cv::Vec2s(10, 10)); float avgL = (5 + 15) / 2.0f; float avgR = (10 + 10) / 2.0f; float expected = fabs(avgL - avgR); EXPECT_FLOAT_EQ(util2d::sad(left, right), expected); } TEST(Util2dTest, CalcStereoCorrespondencesCheckInputs) { // Create synthetic images cv::Mat left = cv::Mat::zeros(100, 100, CV_8UC1); cv::Mat right = cv::Mat::zeros(100, 100, CV_8UC1); std::vector emptyCorners; std::vector status; // Check inputs ASSERT_THROW(util2d::calcStereoCorrespondences(left, cv::Mat(), emptyCorners, status), UException); ASSERT_THROW(util2d::calcStereoCorrespondences(cv::Mat(), right, emptyCorners, status), UException); ASSERT_THROW(util2d::calcStereoCorrespondences(left, cv::Mat(2,2,CV_8UC1), emptyCorners, status), UException); ASSERT_THROW(util2d::calcStereoCorrespondences(left, cv::Mat(left.size(),CV_8UC3), emptyCorners, status), UException); ASSERT_THROW(util2d::calcStereoCorrespondences(left, right, emptyCorners, status, cv::Size(1,1)), cv::Exception); ASSERT_NO_THROW(util2d::calcStereoCorrespondences(left, right, emptyCorners, status, cv::Size(3,3))); ASSERT_THROW(util2d::calcStereoCorrespondences(left, cv::Mat(2,2,CV_8UC1), emptyCorners, status, cv::Size(6,3), 3, 5, -1.0f), UException); ASSERT_THROW(util2d::calcStereoCorrespondences(left, cv::Mat(2,2,CV_8UC1), emptyCorners, status, cv::Size(6,3), 3, 5, 10, 1), UException); // Check results ASSERT_TRUE(util2d::calcStereoCorrespondences(left, right, emptyCorners, status).empty()); } TEST(Util2dTest, CalcStereoCorrespondencesSSD) { // Create synthetic images cv::Mat left = cv::Mat::zeros(100, 100, CV_8UC1); cv::Mat right = cv::Mat::zeros(100, 100, CV_8UC1); std::vector status; // Draw white dots in both images cv::Point2f leftPoint(50, 50); cv::Point2f subPixelLeftPoint(25, 25); cv::Point2f wrongPoint(75, 75); left.at((int)leftPoint.y, (int)leftPoint.x) = 255; right.at((int)leftPoint.y, (int)leftPoint.x-5) = 255; left.at((int)subPixelLeftPoint.y, (int)subPixelLeftPoint.x) = 64; left.at((int)subPixelLeftPoint.y, (int)subPixelLeftPoint.x+1) = 191; right.at((int)subPixelLeftPoint.y, (int)subPixelLeftPoint.x-6) = 255; left.at((int)wrongPoint.y, (int)wrongPoint.x) = 75; std::vector leftCorners = { leftPoint, subPixelLeftPoint, wrongPoint }; // SSD std::vector rightCorners = util2d::calcStereoCorrespondences(left, right, leftCorners, status, cv::Size(6,3), 3, 5, 0.0f, 64.0f, true); ASSERT_EQ(rightCorners.size(), 3); ASSERT_EQ(status.size(), 3); ASSERT_TRUE(status[0]); ASSERT_TRUE(status[1]); ASSERT_FALSE(status[2]); ASSERT_NEAR(leftCorners[0].x - rightCorners[0].x, 5.0f, 0.1f); ASSERT_NEAR(leftCorners[1].x - rightCorners[1].x, 6.75f, 0.1f); } TEST(Util2dTest, CalcStereoCorrespondencesSAD) { // Create synthetic images cv::Mat left = cv::Mat::zeros(100, 100, CV_8UC1); cv::Mat right = cv::Mat::zeros(100, 100, CV_8UC1); std::vector status; // Draw white dots in both images cv::Point2f leftPoint(50, 50); cv::Point2f subPixelLeftPoint(25, 25); cv::Point2f wrongPoint(75, 75); left.at((int)leftPoint.y, (int)leftPoint.x) = 255; right.at((int)leftPoint.y, (int)leftPoint.x-5) = 255; left.at((int)subPixelLeftPoint.y, (int)subPixelLeftPoint.x) = 64; left.at((int)subPixelLeftPoint.y, (int)subPixelLeftPoint.x+1) = 191; right.at((int)subPixelLeftPoint.y, (int)subPixelLeftPoint.x-6) = 255; left.at((int)wrongPoint.y, (int)wrongPoint.x) = 75; std::vector leftCorners = { leftPoint, subPixelLeftPoint, wrongPoint }; // SAD std::vector rightCorners = util2d::calcStereoCorrespondences(left, right, leftCorners, status, cv::Size(6,3), 3, 5, 0.0f, 64.0f, false); ASSERT_EQ(rightCorners.size(), 3); ASSERT_EQ(status.size(), 3); ASSERT_TRUE(status[0]); ASSERT_TRUE(status[1]); ASSERT_FALSE(status[2]); ASSERT_NEAR(leftCorners[0].x - rightCorners[0].x, 5.0f, 0.1f); ASSERT_NEAR(leftCorners[1].x - rightCorners[1].x, 6.75f, 0.1f); } TEST(Util2dTest, CalcOpticalFlowPyrLKStereo) { // Create synthetic images cv::Mat left = cv::Mat::zeros(100, 100, CV_8UC1); cv::Mat right = cv::Mat::zeros(100, 100, CV_8UC1); // Draw a white dot in both images, shifted by 5 pixels in x-direction and 0.5 pixel in y-direction cv::Point2f leftPoint(50, 50); cv::Point2f rightPoint = leftPoint - cv::Point2f(5.0f, 0.5f); left.at((int)leftPoint.y, (int)leftPoint.x) = 255; right.at((int)rightPoint.y, (int)rightPoint.x) = 255; std::vector prevPts = { leftPoint }; std::vector nextPts; cv::Mat status, err; // Run the stereo optical flow function util2d::calcOpticalFlowPyrLKStereo( left, right, prevPts, nextPts, status, err, cv::Size(21, 21), // window size 3, // max level cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 30, 0.01), 0, // no flags 1e-4 // minEigThreshold ); ASSERT_EQ(status.at(0), 1) << "Optical flow failed."; ASSERT_NEAR(nextPts[0].y, leftPoint.y, 0.1f) << "Y position should remain constant."; ASSERT_NEAR(leftPoint.x - nextPts[0].x, 5.0f, 0.1f) << "X displacement should be close to 5."; } TEST(Util2dTest, DisparityFromStereoImages) { // Check inputs ASSERT_THROW(util2d::disparityFromStereoImages(cv::Mat(), cv::Mat(2,2,CV_8UC1)), UException); ASSERT_THROW(util2d::disparityFromStereoImages(cv::Mat(2,2,CV_8UC1), cv::Mat()), UException); ASSERT_THROW(util2d::disparityFromStereoImages(cv::Mat(2,2,CV_8UC1), cv::Mat(3,3,CV_8UC1)), UException); ASSERT_THROW(util2d::disparityFromStereoImages(cv::Mat(2,2,CV_8UC3), cv::Mat(2,2,CV_8UC3)), UException); cv::Mat left = cv::imread(std::string(RTABMAP_TEST_DATA_ROOT)+"/stereo_rect/left/50.jpg"); cv::Mat right = cv::imread(std::string(RTABMAP_TEST_DATA_ROOT)+"/stereo_rect/right/50.jpg", cv::IMREAD_GRAYSCALE); cv::Mat disparity = util2d::disparityFromStereoImages(left, right); EXPECT_FALSE(disparity.empty()); EXPECT_EQ(disparity.size(), left.size()); EXPECT_EQ(disparity.type(), CV_16SC1); cv::Rect rect(300, 400, 40, 40); cv::Mat roi = disparity(rect); // Check if all values in ROI are close to 10 (float comparison) double minVal, maxVal; cv::minMaxLoc(roi, &minVal, &maxVal); EXPECT_NEAR(minVal, 313, 0.5); EXPECT_NEAR(maxVal, 388, 0.5); } TEST(Util2dTest, DepthFromDisparityFloat32) { const int rows = 2; const int cols = 3; const float fx = 500.0f; // focal length in pixels const float baseline = 0.1f; // 10 cm baseline const float disparityValue = 25.0f; cv::Mat disparity(rows, cols, CV_32FC1, cv::Scalar(disparityValue)); cv::Mat depth = util2d::depthFromDisparity(disparity, fx, baseline, CV_32FC1); ASSERT_EQ(depth.rows, rows); ASSERT_EQ(depth.cols, cols); ASSERT_EQ(depth.type(), CV_32FC1); float expectedDepth = baseline * fx / disparityValue; // = 0.1 * 500 / 25 = 2.0 for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) { EXPECT_NEAR(depth.at(i, j), expectedDepth, 1e-5); } } } TEST(Util2dTest, DepthFromDisparityUInt16) { const int rows = 2; const int cols = 3; const float fx = 500.0f; const float baseline = 0.1f; const float disparityValue = 25.0f; cv::Mat disparity(rows, cols, CV_32FC1, cv::Scalar(disparityValue)); cv::Mat depth = util2d::depthFromDisparity(disparity, fx, baseline, CV_16UC1); ASSERT_EQ(depth.rows, rows); ASSERT_EQ(depth.cols, cols); ASSERT_EQ(depth.type(), CV_16UC1); unsigned short expectedDepth = static_cast((baseline * fx / disparityValue) * 1000); // in mm for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) { EXPECT_EQ(depth.at(i, j), expectedDepth); } } } TEST(Util2dTest, DepthFromStereoImages) { // Parameters const int width = 20; const int height = 20; float fx = 100.0f; // focal length in pixels float baseline = 0.1f; // 10 cm int flowWinSize = 5; int flowMaxLevel = 3; int flowIterations = 10; double flowEps = 0.01; // Create synthetic stereo images cv::Mat leftImage = cv::Mat::zeros(height, width, CV_8UC1); cv::Mat rightImage = cv::Mat::zeros(height, width, CV_8UC1); // Place a dot at (10,10) in the left image and simulate a disparity of 5 pixels leftImage.at(10, 10) = 255; rightImage.at(10, 5) = 255; // Known point to track std::vector leftCorners = {cv::Point2f(10.0f, 10.0f)}; // Call the function under test cv::Mat depth = util2d::depthFromStereoImages( leftImage, rightImage, leftCorners, fx, baseline, flowWinSize, flowMaxLevel, flowIterations, flowEps ); // Check result float expectedDisparity = 5.0f; float expectedDepth = (fx * baseline) / expectedDisparity; float actualDepth = depth.at(10, 10); ASSERT_NEAR(actualDepth, expectedDepth, 1e-3); } TEST(Util2dTest, DisparityFromStereoCorrespondencesDisparityComputation) { // Test setup cv::Size disparitySize(5, 5); std::vector leftCorners = { cv::Point2f(1.0f, 1.0f), cv::Point2f(2.0f, 1.0f), cv::Point2f(3.0f, 1.0f), cv::Point2f(4.0f, 1.0f) }; std::vector rightCorners = { cv::Point2f(0.5f, 1.0f), cv::Point2f(1.5f, 1.0f), cv::Point2f(2.5f, 1.0f), cv::Point2f(3.5f, 1.0f) }; std::vector mask = {1, 1, 1, 1}; // Call the function to test cv::Mat disparity = util2d::disparityFromStereoCorrespondences(disparitySize, leftCorners, rightCorners, mask); // Verify the disparity values EXPECT_EQ(disparity.at(1, 1), 0.5f); // 1.0f - 0.5f = 0.5f EXPECT_EQ(disparity.at(2, 1), 0.5f); // 2.0f - 1.5f = 0.5f EXPECT_EQ(disparity.at(3, 1), 0.5f); // 3.0f - 2.5f = 0.5f EXPECT_EQ(disparity.at(4, 1), 0.5f); // 4.0f - 3.5f = 0.5f // Check that locations with no disparity (e.g., the first col) remain 0 EXPECT_EQ(disparity.at(0, 0), 0.0f); EXPECT_EQ(disparity.at(1, 0), 0.0f); EXPECT_EQ(disparity.at(2, 0), 0.0f); EXPECT_EQ(disparity.at(3, 0), 0.0f); EXPECT_EQ(disparity.at(4, 0), 0.0f); } TEST(Util2dTest, DisparityFromStereoCorrespondencesEmptyMask) { // Test with empty mask (all points should be included) cv::Size disparitySize(4, 4); std::vector leftCorners = { cv::Point2f(1.0f, 1.0f), cv::Point2f(2.0f, 1.0f), cv::Point2f(3.0f, 1.0f) }; std::vector rightCorners = { cv::Point2f(0.5f, 1.0f), cv::Point2f(1.5f, 1.0f), cv::Point2f(2.5f, 1.0f) }; std::vector mask; // Call the function cv::Mat disparity = util2d::disparityFromStereoCorrespondences(disparitySize, leftCorners, rightCorners, mask); // Verify the disparity values for all points EXPECT_EQ(disparity.at(1, 1), 0.5f); // 1.0f - 0.5f = 0.5f EXPECT_EQ(disparity.at(2, 1), 0.5f); // 2.0f - 1.5f = 0.5f EXPECT_EQ(disparity.at(3, 1), 0.5f); // 3.0f - 2.5f = 0.5f } TEST(Util2dTest, DisparityFromStereoCorrespondencesInconsistentCornersSize) { // Test with inconsistent corners size (should fail) cv::Size disparitySize(5, 5); std::vector leftCorners = { cv::Point2f(1.0f, 1.0f), cv::Point2f(2.0f, 1.0f) }; std::vector rightCorners = { cv::Point2f(0.5f, 1.0f) }; std::vector mask = {1}; ASSERT_THROW(util2d::disparityFromStereoCorrespondences(disparitySize, leftCorners, rightCorners, mask), UException); rightCorners.push_back(cv::Point2f(1.5f,1.0f)); ASSERT_THROW(util2d::disparityFromStereoCorrespondences(disparitySize, leftCorners, rightCorners, mask), UException); mask.push_back(1); leftCorners.back().x = -2; ASSERT_THROW(util2d::disparityFromStereoCorrespondences(disparitySize, leftCorners, rightCorners, mask), UException); leftCorners.back().x = 6; ASSERT_THROW(util2d::disparityFromStereoCorrespondences(disparitySize, leftCorners, rightCorners, mask), UException); } TEST(Util2dTest, DepthFromStereoCorrespondences) { // Create a dummy left image (only used for size) cv::Mat leftImage = cv::Mat::zeros(10, 10, CV_8UC1); // Define synthetic stereo correspondences std::vector leftCorners = { cv::Point2f(5.0f, 5.0f) }; std::vector rightCorners = { cv::Point2f(2.0f, 5.0f) // disparity = 3.0 }; std::vector mask = {1}; // Intrinsics float fx = 100.0f; // focal length in pixels float baseline = 0.1f; // 10 cm // Expected depth = fx * baseline / disparity = 100 * 0.1 / 3 = 3.333... float expectedDepth = (fx * baseline) / (leftCorners[0].x - rightCorners[0].x); // Call function cv::Mat depth = util2d::depthFromStereoCorrespondences(leftImage, leftCorners, rightCorners, mask, fx, baseline); // Check result ASSERT_EQ(depth.type(), CV_32FC1); float actualDepth = depth.at(5, 5); // rounded position from input ASSERT_NEAR(actualDepth, expectedDepth, 1e-4); } TEST(Util2dTest, CvtDepthFromFloat) { // Create a simple 3x3 depth image in meters cv::Mat depth32F = (cv::Mat_(3, 3) << 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f); // in meters // Convert to 16-bit unsigned short (mm) cv::Mat depth16U = util2d::cvtDepthFromFloat(depth32F); ASSERT_EQ(depth16U.type(), CV_16UC1); ASSERT_EQ(depth16U.rows, 3); ASSERT_EQ(depth16U.cols, 3); EXPECT_EQ(depth16U.at(0, 0), 500); // 0.5m -> 500mm EXPECT_EQ(depth16U.at(0, 1), 1000); // 1.0m -> 1000mm EXPECT_EQ(depth16U.at(2, 2), 7000); // 7.0m -> 7000mm } TEST(Util2dTest, CvtDepthToFloat) { // Create a simple 3x3 depth image in millimeters cv::Mat depth16U = (cv::Mat_(3, 3) << 500, 1000, 1500, 2000, 3000, 4000, 5000, 6000, 7000); // in mm // Convert to 32-bit float (meters) cv::Mat depth32F = util2d::cvtDepthToFloat(depth16U); ASSERT_EQ(depth32F.type(), CV_32FC1); ASSERT_EQ(depth32F.rows, 3); ASSERT_EQ(depth32F.cols, 3); EXPECT_FLOAT_EQ(depth32F.at(0, 0), 0.5f); // 500mm -> 0.5m EXPECT_FLOAT_EQ(depth32F.at(0, 1), 1.0f); // 1000mm -> 1.0m EXPECT_FLOAT_EQ(depth32F.at(2, 2), 7.0f); // 7000mm -> 7.0m } TEST(Util2dTest, CvtDepthRoundTripConversion) { // Test round-trip conversion cv::Mat original = (cv::Mat_(2, 2) << 0.25f, 1.0f, 2.5f, 3.3f); cv::Mat depth16U = util2d::cvtDepthFromFloat(original); cv::Mat depth32F = util2d::cvtDepthToFloat(depth16U); for (int i = 0; i < original.rows; ++i) { for (int j = 0; j < original.cols; ++j) { float expected = std::floor(original.at(i, j) * 1000.0f + 0.5f) / 1000.0f; EXPECT_NEAR(depth32F.at(i, j), expected, 0.001); } } } TEST(Util2dTest, GetDepthCenterDepthValue32FNoSmoothingNoEstimation) { cv::Mat depth = cv::Mat::zeros(5, 5, CV_32FC1); depth.at(2, 2) = 1.5f; float result = util2d::getDepth(depth, 2.0f, 2.0f, false, 0.1f, false); EXPECT_FLOAT_EQ(result, 1.5f); } TEST(Util2dTest, GetDepthCenterDepthValue16UNoSmoothingNoEstimation) { cv::Mat depth = cv::Mat::zeros(5, 5, CV_16UC1); depth.at(2, 2) = 1500; // 1.5 meters float result = util2d::getDepth(depth, 2.0f, 2.0f, false, 0.1f, false); EXPECT_FLOAT_EQ(result, 1.5f); } TEST(Util2dTest, GetDepthSmoothing32F) { cv::Mat depth = cv::Mat::zeros(5, 5, CV_32FC1); depth.at(2, 2) = 1.0f; depth.at(2, 1) = 1.0f; depth.at(1, 2) = 1.0f; depth.at(2, 3) = 1.0f; depth.at(3, 2) = 1.0f; float result = util2d::getDepth(depth, 2.0f, 2.0f, true, 0.1f, false); EXPECT_NEAR(result, 1.0f, 1e-5f); } TEST(Util2dTest, GetDepthEstimationFromNeighbors16U) { cv::Mat depth = cv::Mat::zeros(5, 5, CV_16UC1); depth.at(2, 1) = 1500; // 1.5m depth.at(1, 2) = 1500; float result = util2d::getDepth(depth, 2.0f, 2.0f, false, 0.1f, true); EXPECT_NEAR(result, 1.5f, 1e-3f); } TEST(Util2dTest, GetDepthOutOfBounds) { cv::Mat depth = cv::Mat::ones(5, 5, CV_32FC1); float result = util2d::getDepth(depth, 5.5f, 5.5f, false, 0.1f, false); EXPECT_FLOAT_EQ(result, 0.0f); } TEST(Util2dTest, ComputeRoiValidStringInput) { cv::Size imageSize(200, 100); cv::Mat image = cv::Mat::zeros(imageSize, CV_8UC1); cv::Rect roi = util2d::computeRoi(image, "0.1 0.1 0.2 0.2"); EXPECT_EQ(roi.x, 20); EXPECT_EQ(roi.width, 160); EXPECT_EQ(roi.y, 20); EXPECT_EQ(roi.height, 60); roi = util2d::computeRoi(imageSize, "0.1 0.1 0.2 0.2"); EXPECT_EQ(roi.x, 20); EXPECT_EQ(roi.width, 160); EXPECT_EQ(roi.y, 20); EXPECT_EQ(roi.height, 60); } TEST(Util2dTest, ComputeRoiInvalidStringInput) { cv::Mat image = cv::Mat::zeros(100, 200, CV_8UC1); cv::Rect roi = util2d::computeRoi(image, "0.5 0.6"); // Invalid format EXPECT_EQ(roi, cv::Rect()); // Expect empty ROI } TEST(Util2dTest, ComputeRoiValidVectorInput) { cv::Size imageSize(300, 150); cv::Mat image = cv::Mat::zeros(imageSize, CV_8UC1); std::vector ratios = {0.1f, 0.2f, 0.1f, 0.1f}; cv::Rect roi = util2d::computeRoi(image, ratios); EXPECT_EQ(roi.x, 30); EXPECT_EQ(roi.width, 210); EXPECT_EQ(roi.y, 15); EXPECT_EQ(roi.height, 120); roi = util2d::computeRoi(imageSize, ratios); EXPECT_EQ(roi.x, 30); EXPECT_EQ(roi.width, 210); EXPECT_EQ(roi.y, 15); EXPECT_EQ(roi.height, 120); } TEST(Util2dTest, ComputeRoiInvalidVectorSize) { cv::Size imageSize(300, 150); std::vector invalidRatios = {0.1f, 0.2f}; // Invalid size cv::Rect roi = util2d::computeRoi(imageSize, invalidRatios); EXPECT_EQ(roi, cv::Rect()); } TEST(Util2dTest, ComputeRoiZeroImageSize) { cv::Size imageSize(0, 0); cv::Mat image = cv::Mat::zeros(imageSize, CV_8UC1); std::vector ratios = {0.1f, 0.1f, 0.1f, 0.1f}; cv::Rect roi = util2d::computeRoi(image, ratios); EXPECT_EQ(roi, cv::Rect()); roi = util2d::computeRoi(imageSize, ratios); EXPECT_EQ(roi, cv::Rect()); } TEST(Util2dTest, DecimateFloatDepthImage) { // Create a 4x4 depth image with increasing values cv::Mat depth = (cv::Mat_(4, 4) << 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12, 13,14,15,16); // Decimate by 2 cv::Mat decimated = util2d::decimate(depth, 2); ASSERT_EQ(decimated.rows, 2); ASSERT_EQ(decimated.cols, 2); EXPECT_FLOAT_EQ(decimated.at(0,0), 1); EXPECT_FLOAT_EQ(decimated.at(0,1), 3); EXPECT_FLOAT_EQ(decimated.at(1,0), 9); EXPECT_FLOAT_EQ(decimated.at(1,1), 11); } TEST(Util2dTest, Decimate16UDepthImage) { cv::Mat depth = (cv::Mat_(4, 4) << 100, 200, 300, 400, 500, 600, 700, 800, 900,1000,1100,1200, 1300,1400,1500,1600); cv::Mat decimated = util2d::decimate(depth, 2); ASSERT_EQ(decimated.rows, 2); ASSERT_EQ(decimated.cols, 2); EXPECT_EQ(decimated.at(0,0), 100); EXPECT_EQ(decimated.at(0,1), 300); EXPECT_EQ(decimated.at(1,0), 900); EXPECT_EQ(decimated.at(1,1), 1100); } TEST(Util2dTest, InterpolateFloatDepthImage) { // Create a simple 2x2 image to interpolate into 4x4 cv::Mat input = (cv::Mat_(2,2) << 1.0f, 1.0f, 1.0f, 1.0f); int factor = 2; float depthErrorRatio = 0.1f; cv::Mat interpolated = util2d::interpolate(input, factor, depthErrorRatio); ASSERT_EQ(interpolated.rows, 4); ASSERT_EQ(interpolated.cols, 4); // All interpolated values should still be 1.0f for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { EXPECT_FLOAT_EQ(interpolated.at(r, c), 1.0f); } } } TEST(Util2dTest, Interpolate16UDepthImage) { cv::Mat input = (cv::Mat_(2,2) << 1000, 1000, 1000, 1000); int factor = 2; float depthErrorRatio = 0.1f; cv::Mat interpolated = util2d::interpolate(input, factor, depthErrorRatio); ASSERT_EQ(interpolated.rows, 4); ASSERT_EQ(interpolated.cols, 4); for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { EXPECT_EQ(interpolated.at(r, c), 1000); } } } TEST(Util2dTest, RegisterDepth) { // Create a 2x2 synthetic depth image in meters cv::Mat depth = (cv::Mat_(2,2) << 1.0f, 1.0f, 1.0f, 1.0f); cv::Mat depthK = (cv::Mat_(3,3) << 1.0, 0, 0.5, 0, 1.0, 0.5, 0, 0, 1.0); // RGB image size (same size for simplicity) cv::Size colorSize = depth.size(); cv::Mat colorK = depthK.clone(); // Identity transform (depth and color cameras are perfectly aligned) rtabmap::Transform transform = rtabmap::Transform::getIdentity(); // default is identity // Run registration cv::Mat registered = util2d::registerDepth(depth, depthK, colorSize, colorK, transform); // Check that the output is the same as input since transform is identity and intrinsics match ASSERT_EQ(registered.type(), depth.type()); ASSERT_EQ(registered.size(), colorSize); for (int y = 0; y < registered.rows; ++y) { for (int x = 0; x < registered.cols; ++x) { EXPECT_FLOAT_EQ(registered.at(y, x), 1.0f); } } } TEST(Util2dTest, RegisterDepthWithOverlap) { cv::Mat depth = cv::Mat::ones(11,11,CV_32FC1); cv::Mat depthK = (cv::Mat_(3,3) << 10.0, 0, 5, 0, 10.0, 5, 0, 0, 1.0); cv::Size colorSize = depth.size(); cv::Mat colorK = depthK; // move the color camera right and rotate to left (optical frame) rtabmap::Transform transform = rtabmap::Transform(1.5,0.0,1.0,0,-M_PI/2,0); // Run registration cv::Mat registered = util2d::registerDepth(depth, depthK, colorSize, colorK, transform.inverse()); ASSERT_EQ(registered.type(), depth.type()); ASSERT_EQ(registered.size(), colorSize); // last column from original should match the middle column of registered // all other columns should be 0 for (int y = 0; y < registered.rows; ++y) { for (int x = 0; x < registered.cols; ++x) { EXPECT_FLOAT_EQ(registered.at(y, x), x != registered.cols/2 ? 0.0f : 1.0f); } } } TEST(Util2dTest, FillDepthHoles) { cv::Mat depth = (cv::Mat_(3,3) << 1, 0, 2, 0, 3, 0, 2, 0, 4); int maximumHoleSize = 2; float errorRatio = 1.0f; cv::Mat filledDepth = util2d::fillDepthHoles(depth, maximumHoleSize, errorRatio); EXPECT_EQ(filledDepth.size(), depth.size()); EXPECT_EQ(filledDepth.type(), depth.type()); cv::Mat expected = (cv::Mat_(3,3) << 1, 1.5, 2, 1.5, 3, 0, 2, 0, 4); for (int i = 0; i < filledDepth.rows; i++) { for (int j = 0; j < filledDepth.cols; j++) { EXPECT_EQ(filledDepth.at(i, j), expected.at(i,j)); } } } TEST(Util2dTest, FillDepthHolesLargeHoleTest) { cv::Mat depth = (cv::Mat_(5,5) << 1, 0, 2, 3, 5, 0, 3, 0, 0, 4, 2, 0, 4, 2, 3, 1, 0, 0, 0, 2, 0, 3, 0, 9, 7); cv::Mat expected = (cv::Mat_(5,5) << 1, 1.5, 2, 3, 5, 1.5, 3, 3.16, 3.08, 4, 2, 3, 4, 2, 3, 1, 3, 0, 0, 2, 0, 3, 0, 9, 7); int maximumHoleSize = 2; float errorRatio = 1.0f; cv::Mat filledDepth = util2d::fillDepthHoles(depth, maximumHoleSize, errorRatio); EXPECT_EQ(filledDepth.size(), depth.size()); EXPECT_EQ(filledDepth.type(), depth.type()); for (int i = 0; i < filledDepth.rows; i++) { for (int j = 0; j < filledDepth.cols; j++) { EXPECT_NEAR(filledDepth.at(i, j), expected.at(i,j), 0.1f); } } } TEST(Util2dTest, FillDepthHolesInvalidInputTest) { cv::Mat depth(5, 5, CV_32FC1); // Test with invalid maximumHoleSize (<= 0) int maximumHoleSize = 0; float errorRatio = 0.1f; EXPECT_THROW(util2d::fillDepthHoles(depth, maximumHoleSize, errorRatio), UException); } cv::Mat createTestDepthImage() { cv::Mat img = (cv::Mat_(5, 5) << 1000, 0, 1010, 0, 1020, 0, 0, 0, 0, 0, 1005, 0, 1015, 0, 1025, 0, 0, 0, 0, 0, 1010, 0, 1020, 0, 1030); return img; } TEST(Util2dTest, FillRegisteredDepthHolesVerticalFilling) { cv::Mat input = createTestDepthImage(); cv::Mat original = input.clone(); util2d::fillRegisteredDepthHoles(input, true, false, false); // Vertical only for (int i = 0; i < input.rows; i++) { for (int j = 0; j < input.cols; j++) { if((i==1 && j==2) || (i==3 && j==2)) { // only middle column should have now all valid values EXPECT_GT(input.at(i, j), 0); } else { EXPECT_EQ(input.at(i, j), original.at(i,j)); } } } } TEST(Util2dTest, FillRegisteredDepthHolesHorizontalFilling) { cv::Mat input = createTestDepthImage(); cv::Mat original = input.clone(); util2d::fillRegisteredDepthHoles(input, false, true, false); // Horizontal only for (int i = 0; i < input.rows; i++) { for (int j = 0; j < input.cols; j++) { if((i==2 && j==1) || (i==2 && j==3)) { // only middle row should have now all valid values EXPECT_GT(input.at(i, j), 0); } else { EXPECT_EQ(input.at(i, j), original.at(i,j)); } } } } TEST(Util2dTest, FillRegisteredDepthHolesDoubleHoleFilling) { cv::Mat input = (cv::Mat_(5, 5) << 1000, 1005, 0, 1020, 1030, 0, 1010, 0, 0, 1015, 1010, 0, 0, 0, 1020, 1015, 0, 0, 1015, 1025, 1010, 1015, 1020, 1025, 1030); cv::Mat expected = (cv::Mat_(5, 5) << 1000, 1005, 0, 1020, 1030, 0, 1010, 1011, 1013, 1015, 1010, 1011, 1013, 0, 1020, // <- Note that when double filling is on, there is a double contour 1015, 1013, 1017, 1015, 1025, 1010, 1015, 1020, 1025, 1030); util2d::fillRegisteredDepthHoles(input, true, true, true); // Fill double holes too for (int i = 0; i < input.rows; i++) { for (int j = 0; j < input.cols; j++) { EXPECT_EQ(input.at(i, j), expected.at(i,j)); } } } TEST(Util2dTest, FastBilateralFilteringEmptyInput) { cv::Mat empty; EXPECT_THROW(util2d::fastBilateralFiltering(empty, 2.0f, 0.1f, false), UException); } TEST(Util2dTest, FastBilateralFilteringAllZeroInput) { cv::Mat depth = cv::Mat::zeros(10, 10, CV_32FC1); cv::Mat result = util2d::fastBilateralFiltering(depth, 2.0f, 0.1f, false); // All values should still be 0 for (int i = 0; i < result.rows; ++i) { for (int j = 0; j < result.cols; ++j) { EXPECT_EQ(result.at(i, j), 0.0f); } } } TEST(Util2dTest, FastBilateralFilteringSimpleFloatInput) { cv::Mat depth = cv::Mat::ones(5, 5, CV_32FC1) * 1.0f; depth.at(2,2) = 1.05f; cv::Mat result = util2d::fastBilateralFiltering(depth, 3.0f, 0.05f, false); ASSERT_FALSE(result.empty()); EXPECT_EQ(result.type(), CV_32FC1); EXPECT_EQ(result.rows, depth.rows); EXPECT_EQ(result.cols, depth.cols); // All values should still be close to 1.0 (since no variation) for (int i = 0; i < result.rows; ++i) { for (int j = 0; j < result.cols; ++j) { EXPECT_NEAR(result.at(i, j), 1.0f, 1e-2); } } } TEST(Util2dTest, FastBilateralFilteringSimpleUShortInput) { cv::Mat depth = cv::Mat::ones(5, 5, CV_16UC1) * 1000; // 1 meter depth.at(2,2) = 1050; cv::Mat result = util2d::fastBilateralFiltering(depth, 2.0f, 0.05f, true); ASSERT_FALSE(result.empty()); EXPECT_EQ(result.type(), CV_32FC1); EXPECT_EQ(result.rows, depth.rows); EXPECT_EQ(result.cols, depth.cols); for (int i = 0; i < result.rows; ++i) { for (int j = 0; j < result.cols; ++j) { EXPECT_NEAR(result.at(i, j), 1.0f, 1e-2); } } } // High Depth Variation (should preserve edge due to sigmaR) TEST(Util2dTest, FastBilateralFilteringPreservesEdgesWithLowSigmaR) { // Create a step edge: left side = 1.0f, right side = 3.0f cv::Mat depth = cv::Mat::ones(5, 10, CV_32FC1); depth.colRange(5, 10).setTo(3.0f); depth.at(2,7) = 3.1f; depth.at(2,2) = 1.1f; float sigmaS = 2.0f; float sigmaR = 0.1f; // Low sigmaR to preserve edge cv::Mat result = util2d::fastBilateralFiltering(depth, sigmaS, sigmaR, false); ASSERT_FALSE(result.empty()); for (int y = 0; y < depth.rows; ++y) { EXPECT_LT(result.at(y, 4), 2.0f); // Should stay close to 1.0 EXPECT_GT(result.at(y, 5), 2.0f); // Should stay close to 3.0 } } // High sigmaR (should blur across the edge) TEST(Util2dTest, FastBilateralFilteringBlursEdgesWithHighSigmaR) { cv::Mat depth = cv::Mat::ones(5, 10, CV_32FC1); depth.colRange(5, 10).setTo(3.0f); float sigmaS = 2.0f; float sigmaR = 5.0f; // High sigmaR allows more blurring cv::Mat result = util2d::fastBilateralFiltering(depth, sigmaS, sigmaR, false); ASSERT_FALSE(result.empty()); // Middle column should be somewhere between 1.0 and 3.0 for (int y = 0; y < depth.rows; ++y) { float val = result.at(y, 5); EXPECT_GT(val, 1.1f); EXPECT_LT(val, 2.9f); } } // Random Depth Values with NaNs or Invalids TEST(Util2dTest, FastBilateralFilteringHandlesInvalidDepthValues) { cv::Mat depth = cv::Mat::ones(5, 5, CV_32FC1); depth.at(2, 2) = std::numeric_limits::quiet_NaN(); depth.at(1, 3) = -1.0f; depth.at(1, 2) = 1.1f; cv::Mat result = util2d::fastBilateralFiltering(depth, 2.0f, 0.1f, false); ASSERT_FALSE(result.empty()); // Make sure the invalid pixels are skipped, and output is still valid for (int y = 0; y < result.rows; ++y) { for (int x = 0; x < result.cols; ++x) { float val = result.at(y, x); EXPECT_TRUE((val!=0.0f && val-1.0f<0.02f) || val==0.0f); } } } // Early Division Toggle TEST(Util2dTest, FastBilateralFilteringEarlyDivisionOptionConsistency) { cv::Mat depth = cv::Mat::ones(5, 5, CV_32FC1) * 2.0f; depth.at(2,2) = 2.1f; cv::Mat result1 = util2d::fastBilateralFiltering(depth, 2.0f, 0.1f, true); cv::Mat result2 = util2d::fastBilateralFiltering(depth, 2.0f, 0.1f, false); ASSERT_FALSE(result1.empty()); ASSERT_FALSE(result2.empty()); for (int y = 0; y < depth.rows; ++y) { for (int x = 0; x < depth.cols; ++x) { EXPECT_NEAR(result1.at(y, x), result2.at(y, x), 1e-3); } } } // Test for empty input TEST(Util2dTest, DepthBleedingFilteringHandlesEmptyInput) { cv::Mat empty; EXPECT_NO_THROW(util2d::depthBleedingFiltering(empty, 0.1f)); } // Test that borders are zeroed out TEST(Util2dTest, DepthBleedingFilteringBordersAreZeroed) { cv::Mat depth = cv::Mat::ones(5, 5, CV_32FC1); util2d::depthBleedingFiltering(depth, 0.1f); for(int i = 0; i < 5; ++i) { EXPECT_EQ(depth.at(0, i), 0.0f); EXPECT_EQ(depth.at(4, i), 0.0f); EXPECT_EQ(depth.at(i, 0), 0.0f); EXPECT_EQ(depth.at(i, 4), 0.0f); } } // Test that valid depths are not removed TEST(Util2dTest, DepthBleedingFilteringKeepsValidDepths) { cv::Mat depth = cv::Mat::ones(5, 5, CV_32FC1); depth.at(2,2) = 1.01f; // Within threshold of 0.1 util2d::depthBleedingFiltering(depth, 0.1f); EXPECT_GT(depth.at(2,2), 0.0f); } // Test that invalid depth is removed TEST(Util2dTest, DepthBleedingFilteringFiltersInvalidDepths) { cv::Mat depth = cv::Mat::ones(5, 5, CV_32FC1); depth.at(2,2) = 5.0f; // Large depth jump util2d::depthBleedingFiltering(depth, 0.1f); EXPECT_EQ(depth.at(2,2), 0.0f); } // Repeat the above for CV_16UC1 TEST(Util2dTest, DepthBleedingFilteringFiltersInvalidDepths16U) { cv::Mat depth = cv::Mat::ones(5, 5, CV_16UC1) * 1000; // 1.0m in mm depth.at(2,2) = 5000; // 5.0m util2d::depthBleedingFiltering(depth, 0.1f); EXPECT_EQ(depth.at(2,2), 0); } TEST(Util2dTest, DepthBleedingFilteringKeepsValidDepths16U) { cv::Mat depth = cv::Mat::ones(5, 5, CV_16UC1) * 1000; depth.at(2,2) = 1090; // 0.09m difference, within 0.1m util2d::depthBleedingFiltering(depth, 0.1f); EXPECT_GT(depth.at(2,2), 0); } TEST(Util2dTest, HSVtoRGBPureRed) { float r, g, b; util2d::HSVtoRGB(&r, &g, &b, 0.0f, 1.0f, 1.0f); EXPECT_NEAR(r, 1.0f, 1e-4f); EXPECT_NEAR(g, 0.0f, 1e-4f); EXPECT_NEAR(b, 0.0f, 1e-4f); } TEST(Util2dTest, HSVtoRGBPureGreen) { float r, g, b; util2d::HSVtoRGB(&r, &g, &b, 120.0f, 1.0f, 1.0f); EXPECT_NEAR(r, 0.0f, 1e-4f); EXPECT_NEAR(g, 1.0f, 1e-4f); EXPECT_NEAR(b, 0.0f, 1e-4f); } TEST(Util2dTest, HSVtoRGBPureBlue) { float r, g, b; util2d::HSVtoRGB(&r, &g, &b, 240.0f, 1.0f, 1.0f); EXPECT_NEAR(r, 0.0f, 1e-4f); EXPECT_NEAR(g, 0.0f, 1e-4f); EXPECT_NEAR(b, 1.0f, 1e-4f); } TEST(Util2dTest, HSVtoRGBGrayscaleFromZeroSaturation) { float r, g, b; util2d::HSVtoRGB(&r, &g, &b, 0.0f, 0.0f, 0.5f); EXPECT_NEAR(r, 0.5f, 1e-4f); EXPECT_NEAR(g, 0.5f, 1e-4f); EXPECT_NEAR(b, 0.5f, 1e-4f); } TEST(Util2dTest, HSVtoRGBHueWrapAround360) { float r, g, b; util2d::HSVtoRGB(&r, &g, &b, 360.0f, 1.0f, 1.0f); // Hue 360 = Hue 0 EXPECT_NEAR(r, 1.0f, 1e-4f); EXPECT_NEAR(g, 0.0f, 1e-4f); EXPECT_NEAR(b, 0.0f, 1e-4f); } TEST(Util2dTest, HSVtoRGBHalfSaturationHalfBrightness) { float r, g, b; util2d::HSVtoRGB(&r, &g, &b, 60.0f, 0.5f, 0.5f); // Yellowish EXPECT_NEAR(r, 0.5f, 1e-4f); EXPECT_NEAR(g, 0.5f, 1e-4f); EXPECT_NEAR(b, 0.25f, 1e-4f); } TEST(Util2dTest, NMSKeepsStrongestKeypointOnly) { std::vector inputKeypoints = { cv::KeyPoint(cv::Point2f(50, 50), 1.f, -1, 0.8f), // weaker cv::KeyPoint(cv::Point2f(52, 52), 1.f, -1, 0.9f), // stronger, within dist_thresh cv::KeyPoint(cv::Point2f(100, 100), 1.f, -1, 0.7f) // far enough }; cv::Mat descriptorsIn = cv::Mat::eye(3, 256, CV_32F); // mock descriptors std::vector outputKeypoints; cv::Mat descriptorsOut; int dist_thresh = 5; int width = 200; int height = 200; util2d::NMS(inputKeypoints, descriptorsIn, outputKeypoints, descriptorsOut, dist_thresh, width, height); ASSERT_EQ(outputKeypoints.size(), 2); EXPECT_EQ(outputKeypoints[0].pt, cv::Point2f(52, 52)); // stronger one in the cluster EXPECT_EQ(outputKeypoints[1].pt, cv::Point2f(100, 100)); ASSERT_EQ(descriptorsOut.rows, 2); EXPECT_TRUE(cv::countNonZero(descriptorsOut.row(0) != descriptorsIn.row(1)) == 0); } TEST(Util2dTest, NMSNoDescriptorsHandledGracefully) { std::vector inputKeypoints = { cv::KeyPoint(cv::Point2f(10, 10), 1.f, -1, 1.0f) }; std::vector outputKeypoints; cv::Mat descriptorsOut; util2d::NMS(inputKeypoints, cv::Mat(), outputKeypoints, descriptorsOut, 3, 50, 50); ASSERT_EQ(outputKeypoints.size(), 1); EXPECT_TRUE(descriptorsOut.empty()); } TEST(Util2dTest, NMSAllSuppressedDueToProximity) { std::vector inputKeypoints = { cv::KeyPoint(cv::Point2f(30, 30), 1.f, -1, 0.5f), cv::KeyPoint(cv::Point2f(31, 31), 1.f, -1, 0.4f), cv::KeyPoint(cv::Point2f(32, 32), 1.f, -1, 0.3f) }; cv::Mat descriptorsIn = cv::Mat::ones(3, 256, CV_32F); std::vector outputKeypoints; cv::Mat descriptorsOut; util2d::NMS(inputKeypoints, descriptorsIn, outputKeypoints, descriptorsOut, 3, 100, 100); ASSERT_EQ(outputKeypoints.size(), 1); // only strongest survives EXPECT_NEAR(outputKeypoints[0].response, 0.5f, 1e-6); } TEST(Util2dTest, NMSImageBoundsRespected) { std::vector inputKeypoints = { cv::KeyPoint(cv::Point2f(0, 0), 1.f, -1, 1.0f), cv::KeyPoint(cv::Point2f(199, 199), 1.f, -1, 1.0f), cv::KeyPoint(cv::Point2f(300, 300), 1.f, -1, 1.0f) // out of bounds }; cv::Mat descriptorsIn = cv::Mat::eye(3, 256, CV_32F); std::vector outputKeypoints; cv::Mat descriptorsOut; util2d::NMS(inputKeypoints, descriptorsIn, outputKeypoints, descriptorsOut, 2, 200, 200); ASSERT_EQ(outputKeypoints.size(), 2); for (const auto& kp : outputKeypoints) { EXPECT_LT(kp.pt.x, 200); EXPECT_LT(kp.pt.y, 200); } } std::vector generateGridKeypoints(int cols, int rows, int spacing) { std::vector keypoints; for (int y = 0; y < rows; y += spacing) { for (int x = 0; x < cols; x += spacing) { keypoints.emplace_back(cv::Point2f(x + 0.5f, y + 0.5f), 1.0f, -1, float(rand() % 100) / 100.0f); } } return keypoints; } TEST(Util2dTest, SSCSelectsRoughlyCorrectNumber) { int imgWidth = 300; int imgHeight = 300; auto keypoints = generateGridKeypoints(imgWidth, imgHeight, 5); // creates many keypoints int maxKeypoints = 100; float tolerance = 0.1f; std::vector selected = util2d::SSC(keypoints, maxKeypoints, tolerance, imgWidth, imgHeight, {}); // util2d::SSC() first reduces maxKeypoints by tolerance, then searches within ±tolerance of that. const int effectiveMax = maxKeypoints - int(std::round(maxKeypoints * tolerance)); const int kMin = int(std::round(effectiveMax * (1.f - tolerance))); const int kMax = int(std::round(effectiveMax * (1.f + tolerance))); EXPECT_GE(selected.size(), kMin); EXPECT_LE(selected.size(), kMax); EXPECT_LE(selected.size(), maxKeypoints); } TEST(Util2dTest, SSCReturnsEmptyOnEmptyInput) { std::vector keypoints; std::vector selected = util2d::SSC(keypoints, 100, 0.1f, 100, 100, {}); EXPECT_TRUE(selected.empty()); } TEST(Util2dTest, SSCRespectsProvidedIndices) { int imgWidth = 200; int imgHeight = 200; auto keypoints = generateGridKeypoints(imgWidth, imgHeight, 10); // Sort keypoints manually (e.g., by response) and use the first N indices std::vector topIndices(keypoints.size()); for(size_t i=0; i selected = util2d::SSC(keypoints, maxKeypoints, 0.1f, imgWidth, imgHeight, topIndices); for (int idx : selected) { EXPECT_TRUE(std::find(topIndices.begin(), topIndices.end(), idx) != topIndices.end()); } EXPECT_GE(selected.size(), 4); EXPECT_LE(selected.size(), 6); } TEST(Util2dTest, SSCSpatialDistributionCheck) { int imgWidth = 100; int imgHeight = 100; auto keypoints = generateGridKeypoints(imgWidth, imgHeight, 1); // dense points int maxKeypoints = 10; float tolerance = 0.2f; std::vector selected = util2d::SSC(keypoints, maxKeypoints, tolerance, imgWidth, imgHeight, {}); // Check that no two selected keypoints are too close float minDistance = float(imgWidth + imgHeight) / float(maxKeypoints); // loose estimate for (size_t i = 0; i < selected.size(); ++i) { for (size_t j = i + 1; j < selected.size(); ++j) { auto& pt1 = keypoints[selected[i]].pt; auto& pt2 = keypoints[selected[j]].pt; float dist = cv::norm(pt1 - pt2); EXPECT_GT(dist, minDistance * 0.2f); // 20% of estimated spacing } } } cv::Mat createTestImage(int width, int height, uchar value = 100) { return cv::Mat(height, width, CV_8UC3, cv::Scalar(value, value, value)); } namespace { // A 3-channel "marker" pixel that's distinguishable from the uniform base color. const cv::Vec3b kRgbMarker(255, 0, 0); const cv::Vec3b kDepthMarker(123, 45, 67); // Marker pixel position in the input 640x480 (cols x rows) image (top-left quadrant). const int kMarkerRow = 100; const int kMarkerCol = 200; void stampMarker(cv::Mat & rgb, cv::Mat & depth) { rgb.at(kMarkerRow, kMarkerCol) = kRgbMarker; depth.at(kMarkerRow, kMarkerCol) = kDepthMarker; } // Verifies the marker landed at (expectedRow, expectedCol) and that no other pixel // in the rotated image carries the marker color (i.e. the rotation is direction- // correct, not just dimension-correct). void expectMarkerAt(const cv::Mat & img, const cv::Vec3b & marker, int expectedRow, int expectedCol) { EXPECT_EQ(img.at(expectedRow, expectedCol), marker); int strays = 0; for(int r = 0; r < img.rows; ++r) { for(int c = 0; c < img.cols; ++c) { if(r == expectedRow && c == expectedCol) continue; if(img.at(r, c) == marker) ++strays; } } EXPECT_EQ(strays, 0); } } // namespace TEST(Util2dTest, RotateImagesUpsideUpIfNecessaryNoRotation) { CameraModel model(500, 500, 320, 240, CameraModel::opticalRotation(), 0, cv::Size(640, 480)); cv::Mat rgb = createTestImage(640, 480); cv::Mat depth = createTestImage(640, 480); stampMarker(rgb, depth); bool rotated = util2d::rotateImagesUpsideUpIfNecessary(model, rgb, depth); EXPECT_FALSE(rotated); EXPECT_EQ(rgb.cols, 640); EXPECT_EQ(rgb.rows, 480); EXPECT_EQ(depth.cols, 640); EXPECT_EQ(depth.rows, 480); // Marker stays in place when no rotation is applied. expectMarkerAt(rgb, kRgbMarker, kMarkerRow, kMarkerCol); expectMarkerAt(depth, kDepthMarker, kMarkerRow, kMarkerCol); float roll,pitch,yaw; (model.localTransform() * CameraModel::opticalRotation().inverse()).getEulerAngles(roll, pitch, yaw); EXPECT_EQ(roll, 0.0f); EXPECT_EQ(pitch, 0.0f); EXPECT_EQ(yaw, 0.0f); } TEST(Util2dTest, RotateImagesUpsideUpIfNecessaryRotation90Degrees) { // Simulate +pi/2 roll (camera tilted right) -> upright correction is a 90 CW // rotation of the image (transpose then flip(axis=1)). For an input marker at // (row=100, col=200) in a 480x640 image: // transpose: (100, 200) -> (200, 100) in 640x480 // flip(1): (200, 100) -> (200, 480-1-100) = (200, 379) Transform rot = Transform(0,0,0, M_PI / 2, 0, 0); CameraModel model(500, 500, 320, 240, rot*CameraModel::opticalRotation(), 0, cv::Size(640, 480)); cv::Mat rgb = createTestImage(640, 480, 150); cv::Mat depth = createTestImage(640, 480, 200); stampMarker(rgb, depth); bool rotated = util2d::rotateImagesUpsideUpIfNecessary(model, rgb, depth); EXPECT_TRUE(rotated); EXPECT_EQ(rgb.cols, 480); // Transposed EXPECT_EQ(rgb.rows, 640); EXPECT_EQ(depth.cols, 480); EXPECT_EQ(depth.rows, 640); expectMarkerAt(rgb, kRgbMarker, /*row=*/200, /*col=*/379); expectMarkerAt(depth, kDepthMarker, /*row=*/200, /*col=*/379); // After correction the camera is upright (roll=0). float roll,pitch,yaw; (model.localTransform() * CameraModel::opticalRotation().inverse()).getEulerAngles(roll, pitch, yaw); EXPECT_NEAR(roll, 0.0f, 1e-5); EXPECT_NEAR(pitch, 0.0f, 1e-5); EXPECT_NEAR(yaw, 0.0f, 1e-5); } TEST(Util2dTest, RotateImagesUpsideUpIfNecessaryRotation180Degrees) { // Simulate pi roll (camera upside down) -> 180 rotation: // flip(1) + flip(0). For (100, 200) in 480x640: // flip(1): (100, 200) -> (100, 640-1-200) = (100, 439) // flip(0): (100, 439) -> (480-1-100, 439) = (379, 439) Transform rot = Transform(0,0,0, M_PI, 0, 0); CameraModel model(500, 500, 320, 240, rot*CameraModel::opticalRotation(), 0, cv::Size(640, 480)); cv::Mat rgb = createTestImage(640, 480, 123); cv::Mat depth = createTestImage(640, 480, 77); stampMarker(rgb, depth); bool rotated = util2d::rotateImagesUpsideUpIfNecessary(model, rgb, depth); EXPECT_TRUE(rotated); EXPECT_EQ(rgb.cols, 640); // Same size EXPECT_EQ(rgb.rows, 480); EXPECT_EQ(depth.cols, 640); // Same size EXPECT_EQ(depth.rows, 480); expectMarkerAt(rgb, kRgbMarker, /*row=*/379, /*col=*/439); expectMarkerAt(depth, kDepthMarker, /*row=*/379, /*col=*/439); float roll,pitch,yaw; (model.localTransform() * CameraModel::opticalRotation().inverse()).getEulerAngles(roll, pitch, yaw); EXPECT_NEAR(roll, 0.0f, 1e-5); EXPECT_NEAR(pitch, 0.0f, 1e-5); EXPECT_NEAR(yaw, 0.0f, 1e-5); } TEST(Util2dTest, RotateImagesUpsideUpIfNecessaryRotation270Degrees) { // Simulate 3*pi/2 roll (camera tilted left) -> upright correction is a 90 CCW // rotation of the image (flip(axis=1) then transpose). For (100, 200) in 480x640: // flip(1): (100, 200) -> (100, 640-1-200) = (100, 439) // transpose: (100, 439) -> (439, 100) in 640x480 Transform rot = Transform(0,0,0, 3*M_PI/2, 0, 0); CameraModel model(500, 500, 320, 240, rot*CameraModel::opticalRotation(), 0, cv::Size(640, 480)); cv::Mat rgb = createTestImage(640, 480, 90); cv::Mat depth = createTestImage(640, 480, 60); stampMarker(rgb, depth); bool rotated = util2d::rotateImagesUpsideUpIfNecessary(model, rgb, depth); EXPECT_TRUE(rotated); EXPECT_EQ(rgb.cols, 480); EXPECT_EQ(rgb.rows, 640); EXPECT_EQ(depth.cols, 480); EXPECT_EQ(depth.rows, 640); expectMarkerAt(rgb, kRgbMarker, /*row=*/439, /*col=*/100); expectMarkerAt(depth, kDepthMarker, /*row=*/439, /*col=*/100); // After correction the camera is upright (roll=0). float roll,pitch,yaw; (model.localTransform() * CameraModel::opticalRotation().inverse()).getEulerAngles(roll, pitch, yaw); EXPECT_NEAR(roll, 0.0f, 1e-5); EXPECT_NEAR(pitch, 0.0f, 1e-5); EXPECT_NEAR(yaw, 0.0f, 1e-5); } TEST(Util2dTest, RotateImagesUpsideUpIfNecessaryPitchTooHighShouldSkip) { // Simulate roll = 90°, but pitch = 90° too (invalid) Transform rot = Transform(0,0,0, M_PI/2, M_PI/2, 0); CameraModel model(500, 500, 320, 240, rot*CameraModel::opticalRotation(), 0, cv::Size(640, 480)); Transform orgTransform = model.localTransform(); cv::Mat rgb = createTestImage(640, 480); cv::Mat depth = createTestImage(640, 480); bool rotated = util2d::rotateImagesUpsideUpIfNecessary(model, rgb, depth); EXPECT_FALSE(rotated); EXPECT_EQ(rgb.cols, 640); EXPECT_EQ(rgb.rows, 480); float roll,pitch,yaw; (model.localTransform().inverse() * orgTransform).getEulerAngles(roll, pitch, yaw); EXPECT_NEAR(roll, 0.0f, 1e-5); EXPECT_NEAR(pitch, 0.0f, 1e-5); EXPECT_NEAR(yaw, 0.0f, 1e-5); }