Added Stereo tests

This commit is contained in:
matlabbe
2025-12-23 11:50:16 -08:00
parent 2f6fab02e5
commit e9291bd926
5 changed files with 572 additions and 11 deletions
+236 -9
View File
@@ -35,21 +35,111 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
namespace rtabmap { namespace rtabmap {
/**
* @class Stereo
* @brief Sparse stereo matching using block matching
*
* This class implements sparse stereo matching to find corresponding feature points
* between stereo image pairs using block matching with a search window. Unlike dense
* stereo matching, this class works with sparse feature points rather than computing
* disparity for every pixel.
*
* The algorithm uses a pyramidal approach for efficiency, searching for correspondences
* within a specified disparity range using either SAD (Sum of Absolute Differences) or
* SSD (Sum of Squared Differences) as the matching cost.
*
* @note Input images must be grayscale (CV_8UC1).
* @see StereoOpticalFlow for an alternative implementation using optical flow
* @see StereoDense for dense stereo matching
*/
class RTABMAP_CORE_EXPORT Stereo { class RTABMAP_CORE_EXPORT Stereo {
public: public:
/**
* @brief Factory method to create a Stereo instance
*
* Creates a Stereo instance based on the stereo optical flow parameter
* in the provided parameters map. If optical flow is enabled, creates a
* StereoOpticalFlow instance; otherwise, creates a standard Stereo instance.
*
* @param parameters Parameters map containing configuration values.
* The Parameters::kStereoOpticalFlow() parameter determines
* which implementation to create.
* @return Pointer to the created Stereo instance (caller owns the memory).
* Returns StereoOpticalFlow if optical flow is enabled, otherwise Stereo.
*/
static Stereo * create(const ParametersMap & parameters = ParametersMap()); static Stereo * create(const ParametersMap & parameters = ParametersMap());
public: public:
/**
* @brief Constructor
*
* Initializes a Stereo instance with default parameter values and then
* parses the provided parameters map to override defaults.
*
* @param parameters Optional parameters map containing configuration values.
* If empty, default values are used.
*/
Stereo(const ParametersMap & parameters = ParametersMap()); Stereo(const ParametersMap & parameters = ParametersMap());
/**
* @brief Virtual destructor
*/
virtual ~Stereo() {} virtual ~Stereo() {}
/**
* @brief Parse parameters from a parameters map
*
* Updates the algorithm's configuration based on the provided parameters map.
* Supported parameters:
* - Parameters::kStereoWinWidth() - Search window width
* - Parameters::kStereoWinHeight() - Search window height
* - Parameters::kStereoIterations() - Number of iterations
* - Parameters::kStereoMaxLevel() - Maximum pyramid level
* - Parameters::kStereoMinDisparity() - Minimum disparity value
* - Parameters::kStereoMaxDisparity() - Maximum disparity value
* - Parameters::kStereoSSD() - Use SSD instead of SAD
*
* @param parameters Parameters map containing configuration values
*/
virtual void parseParameters(const ParametersMap & parameters); virtual void parseParameters(const ParametersMap & parameters);
/**
* @brief Compute stereo correspondences using block matching
*
* Finds corresponding points in the right stereo image for the given
* points in the left stereo image using block matching with a search window.
* The algorithm uses a pyramidal approach for efficiency.
*
* @param leftImage Left stereo image (must be CV_8UC1 grayscale)
* @param rightImage Right stereo image (must be CV_8UC1 grayscale)
* @param leftCorners Input vector of feature points in the left image
* @param status Output vector indicating which correspondences are valid (1) or invalid (0).
* The size matches leftCorners.size().
* @return Vector of corresponding points in the right image. The size matches leftCorners.size().
* Invalid correspondences may have coordinates outside the image bounds.
* @note Both input images must be grayscale (CV_8UC1).
* @note The algorithm searches for correspondences within the disparity range
* [minDisparity(), maxDisparity()] and uses a search window of size winSize().
*/
virtual std::vector<cv::Point2f> computeCorrespondences( virtual std::vector<cv::Point2f> computeCorrespondences(
const cv::Mat & leftImage, const cv::Mat & leftImage,
const cv::Mat & rightImage, const cv::Mat & rightImage,
const std::vector<cv::Point2f> & leftCorners, const std::vector<cv::Point2f> & leftCorners,
std::vector<unsigned char> & status) const; std::vector<unsigned char> & status) const;
#ifdef HAVE_OPENCV_CUDEV #ifdef HAVE_OPENCV_CUDEV
/**
* @brief Compute stereo correspondences using GPU (not implemented)
*
* GPU version of computeCorrespondences. Currently not implemented for the
* standard Stereo class. Use StereoOpticalFlow with GPU enabled for GPU acceleration.
*
* @param leftImage Left stereo image on GPU (must be CV_8UC1 grayscale)
* @param rightImage Right stereo image on GPU (must be CV_8UC1 grayscale)
* @param leftCorners Input vector of feature points in the left image
* @param status Output vector indicating which correspondences are valid
* @return Empty vector (GPU support not implemented for this class)
* @note This method always returns an empty vector and logs an error.
*/
virtual std::vector<cv::Point2f> computeCorrespondences( virtual std::vector<cv::Point2f> computeCorrespondences(
const cv::cuda::GpuMat & leftImage, const cv::cuda::GpuMat & leftImage,
const cv::cuda::GpuMat & rightImage, const cv::cuda::GpuMat & rightImage,
@@ -57,30 +147,125 @@ public:
std::vector<unsigned char> & status) const; std::vector<unsigned char> & status) const;
#endif #endif
/**
* @brief Get the search window size
* @return Search window size as cv::Size(winWidth_, winHeight_)
*/
cv::Size winSize() const {return cv::Size(winWidth_, winHeight_);} cv::Size winSize() const {return cv::Size(winWidth_, winHeight_);}
/**
* @brief Get the number of iterations
* @return Number of iterations for the matching algorithm
*/
int iterations() const {return iterations_;} int iterations() const {return iterations_;}
/**
* @brief Get the maximum pyramid level
* @return Maximum pyramid level used in the pyramidal approach
*/
int maxLevel() const {return maxLevel_;} int maxLevel() const {return maxLevel_;}
/**
* @brief Get the minimum disparity value
* @return Minimum disparity value to search (in pixels)
*/
float minDisparity() const {return minDisparity_;} float minDisparity() const {return minDisparity_;}
/**
* @brief Get the maximum disparity value
* @return Maximum disparity value to search (in pixels)
*/
float maxDisparity() const {return maxDisparity_;} float maxDisparity() const {return maxDisparity_;}
/**
* @brief Check if SSD (Sum of Squared Differences) is used
* @return true if SSD is used, false if SAD (Sum of Absolute Differences) is used
*/
bool winSSD() const {return winSSD_;} bool winSSD() const {return winSSD_;}
/**
* @brief Check if GPU acceleration is enabled
* @return Always returns false for the base Stereo class
*/
virtual bool isGpuEnabled() const {return false;} virtual bool isGpuEnabled() const {return false;}
private: private:
int winWidth_; int winWidth_; ///< Search window width (default: from Parameters::defaultStereoWinWidth())
int winHeight_; int winHeight_; ///< Search window height (default: from Parameters::defaultStereoWinHeight())
int iterations_; int iterations_; ///< Number of iterations for matching (default: from Parameters::defaultStereoIterations())
int maxLevel_; int maxLevel_; ///< Maximum pyramid level (default: from Parameters::defaultStereoMaxLevel())
float minDisparity_; float minDisparity_; ///< Minimum disparity value to search (default: from Parameters::defaultStereoMinDisparity())
float maxDisparity_; float maxDisparity_; ///< Maximum disparity value to search (default: from Parameters::defaultStereoMaxDisparity())
bool winSSD_; bool winSSD_; ///< Use SSD instead of SAD for matching cost (default: from Parameters::defaultStereoSSD())
}; };
/**
* @class StereoOpticalFlow
* @brief Sparse stereo matching using optical flow
*
* This class implements sparse stereo matching using optical flow (Lucas-Kanade)
* to find corresponding feature points between stereo image pairs. It extends the
* base Stereo class with optical flow-based matching, which can be more robust
* than simple block matching, especially for textured regions.
*
* The algorithm uses pyramidal Lucas-Kanade optical flow to track feature points
* from the left image to the right image, then filters the results based on
* disparity constraints.
*
* @note Input images must be grayscale (CV_8UC1).
* @note GPU acceleration is available if RTAB-Map is built with OpenCV CUDA support.
* @see Stereo for block matching-based implementation
*/
class RTABMAP_CORE_EXPORT StereoOpticalFlow : public Stereo { class RTABMAP_CORE_EXPORT StereoOpticalFlow : public Stereo {
public: public:
/**
* @brief Constructor
*
* Initializes a StereoOpticalFlow instance with default parameter values
* and then parses the provided parameters map to override defaults.
*
* @param parameters Optional parameters map containing configuration values.
* If empty, default values are used.
*/
StereoOpticalFlow(const ParametersMap & parameters = ParametersMap()); StereoOpticalFlow(const ParametersMap & parameters = ParametersMap());
/**
* @brief Virtual destructor
*/
virtual ~StereoOpticalFlow() {} virtual ~StereoOpticalFlow() {}
/**
* @brief Parse parameters from a parameters map
*
* Updates the algorithm's configuration based on the provided parameters map.
* First calls the base class parseParameters(), then parses optical flow-specific
* parameters:
* - Parameters::kStereoEps() - Convergence threshold for optical flow
* - Parameters::kStereoGpu() - Enable GPU acceleration (requires OpenCV CUDA)
*
* @param parameters Parameters map containing configuration values
* @note If GPU is enabled but RTAB-Map is not built with OpenCV CUDA support,
* GPU will be automatically disabled and an error message will be logged.
*/
virtual void parseParameters(const ParametersMap & parameters); virtual void parseParameters(const ParametersMap & parameters);
/**
* @brief Compute stereo correspondences using optical flow
*
* Finds corresponding points in the right stereo image for the given
* points in the left stereo image using pyramidal Lucas-Kanade optical flow.
* The results are filtered based on disparity constraints (minDisparity to maxDisparity).
*
* @param leftImage Left stereo image (must be CV_8UC1 grayscale)
* @param rightImage Right stereo image (must be CV_8UC1 grayscale)
* @param leftCorners Input vector of feature points in the left image
* @param status Output vector indicating which correspondences are valid (1) or invalid (0).
* The size matches leftCorners.size().
* @return Vector of corresponding points in the right image. The size matches leftCorners.size().
* Invalid correspondences may have coordinates outside the image bounds.
* @note Both input images must be grayscale (CV_8UC1).
* @note If GPU is enabled, the GPU version of this method is called automatically.
*/
virtual std::vector<cv::Point2f> computeCorrespondences( virtual std::vector<cv::Point2f> computeCorrespondences(
const cv::Mat & leftImage, const cv::Mat & leftImage,
const cv::Mat & rightImage, const cv::Mat & rightImage,
@@ -88,6 +273,21 @@ public:
std::vector<unsigned char> & status) const; std::vector<unsigned char> & status) const;
#ifdef HAVE_OPENCV_CUDEV #ifdef HAVE_OPENCV_CUDEV
/**
* @brief Compute stereo correspondences using GPU-accelerated optical flow
*
* GPU-accelerated version of computeCorrespondences using CUDA-optimized
* sparse pyramidal Lucas-Kanade optical flow. This method is automatically
* called when GPU is enabled.
*
* @param leftImage Left stereo image on GPU (must be CV_8UC1 grayscale)
* @param rightImage Right stereo image on GPU (must be CV_8UC1 grayscale)
* @param leftCorners Input vector of feature points in the left image
* @param status Output vector indicating which correspondences are valid
* @return Vector of corresponding points in the right image
* @note Requires RTAB-Map to be built with OpenCV CUDA support (HAVE_OPENCV_CUDAOPTFLOW).
* @note The results are filtered based on disparity constraints after GPU computation.
*/
virtual std::vector<cv::Point2f> computeCorrespondences( virtual std::vector<cv::Point2f> computeCorrespondences(
const cv::cuda::GpuMat & leftImage, const cv::cuda::GpuMat & leftImage,
const cv::cuda::GpuMat & rightImage, const cv::cuda::GpuMat & rightImage,
@@ -95,18 +295,45 @@ public:
std::vector<unsigned char> & status) const; std::vector<unsigned char> & status) const;
#endif #endif
/**
* @brief Get the convergence threshold (epsilon)
* @return Convergence threshold for optical flow iteration termination
*/
float epsilon() const {return epsilon_;} float epsilon() const {return epsilon_;}
/**
* @brief Check if GPU acceleration is enabled
*
* Returns whether GPU acceleration is currently enabled for optical flow computation.
* GPU acceleration requires OpenCV CUDA support to be compiled in.
*
* @return true if GPU is enabled and available, false otherwise
*/
virtual bool isGpuEnabled() const; virtual bool isGpuEnabled() const;
private: private:
/**
* @brief Update status vector based on disparity constraints
*
* Filters the correspondence results by checking if the computed disparity
* (leftCorners[i].x - rightCorners[i].x) falls within the valid range
* [minDisparity(), maxDisparity()]. Points outside this range are marked
* as invalid in the status vector.
*
* @param leftCorners Input feature points in the left image
* @param rightCorners Corresponding points in the right image
* @param status Status vector to update (1 = valid, 0 = invalid)
* @note This method is called automatically after optical flow computation
* to filter results based on disparity constraints.
*/
void updateStatus( void updateStatus(
const std::vector<cv::Point2f> & leftCorners, const std::vector<cv::Point2f> & leftCorners,
const std::vector<cv::Point2f> & rightCorners, const std::vector<cv::Point2f> & rightCorners,
std::vector<unsigned char> & status) const; std::vector<unsigned char> & status) const;
private: private:
float epsilon_; float epsilon_; ///< Convergence threshold for optical flow (default: from Parameters::defaultStereoEps())
bool gpu_; bool gpu_; ///< Enable GPU acceleration (default: from Parameters::defaultStereoGpu(), requires OpenCV CUDA)
}; };
} /* namespace rtabmap */ } /* namespace rtabmap */
+14
View File
@@ -79,8 +79,15 @@ std::vector<cv::Point2f> Stereo::computeCorrespondences(
const std::vector<cv::Point2f> & leftCorners, const std::vector<cv::Point2f> & leftCorners,
std::vector<unsigned char> & status) const std::vector<unsigned char> & status) const
{ {
if(leftCorners.empty())
{
status.clear();
return std::vector<cv::Point2f>();
}
UASSERT(!leftImage.empty() && !rightImage.empty());
UASSERT(leftImage.type() == CV_8UC1); UASSERT(leftImage.type() == CV_8UC1);
UASSERT(rightImage.type() == CV_8UC1); UASSERT(rightImage.type() == CV_8UC1);
UASSERT(leftImage.size() == rightImage.size());
std::vector<cv::Point2f> rightCorners; std::vector<cv::Point2f> rightCorners;
UDEBUG("util2d::calcStereoCorrespondences() begin"); UDEBUG("util2d::calcStereoCorrespondences() begin");
rightCorners = util2d::calcStereoCorrespondences( rightCorners = util2d::calcStereoCorrespondences(
@@ -147,8 +154,15 @@ std::vector<cv::Point2f> StereoOpticalFlow::computeCorrespondences(
const std::vector<cv::Point2f> & leftCorners, const std::vector<cv::Point2f> & leftCorners,
std::vector<unsigned char> & status) const std::vector<unsigned char> & status) const
{ {
if(leftCorners.empty())
{
status.clear();
return std::vector<cv::Point2f>();
}
UASSERT(!leftImage.empty() && !rightImage.empty());
UASSERT(leftImage.type() == CV_8UC1); UASSERT(leftImage.type() == CV_8UC1);
UASSERT(rightImage.type() == CV_8UC1); UASSERT(rightImage.type() == CV_8UC1);
UASSERT(leftImage.size() == rightImage.size());
std::vector<cv::Point2f> rightCorners; std::vector<cv::Point2f> rightCorners;
std::vector<float> err; std::vector<float> err;
#ifdef HAVE_OPENCV_CUDAOPTFLOW #ifdef HAVE_OPENCV_CUDAOPTFLOW
+7 -2
View File
@@ -67,6 +67,11 @@ target_link_libraries(test_transform gtest_main rtabmap_core)
gtest_discover_tests(test_transform) gtest_discover_tests(test_transform)
#StereoDense.h (tests both BM and SGBM strategies) #StereoDense.h (tests both BM and SGBM strategies)
add_executable(test_stereo_dense stereo/test_stereo_dense.cpp) add_executable(test_stereo_dense test_stereo_dense.cpp)
target_link_libraries(test_stereo_dense gtest_main rtabmap_core) target_link_libraries(test_stereo_dense gtest_main rtabmap_core)
gtest_discover_tests(test_stereo_dense) gtest_discover_tests(test_stereo_dense)
#Stereo.h (tests both BlockMatching and OpticalFlow strategies)
add_executable(test_stereo test_stereo.cpp)
target_link_libraries(test_stereo gtest_main rtabmap_core)
gtest_discover_tests(test_stereo)
+315
View File
@@ -0,0 +1,315 @@
#include <gtest/gtest.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/features2d.hpp>
#include "rtabmap/core/Stereo.h"
#include "rtabmap/core/Parameters.h"
#include "rtabmap/utilite/ULogger.h"
#include "rtabmap/utilite/UException.h"
#include <memory>
#include <vector>
using namespace rtabmap;
class StereoTest : public ::testing::Test {
protected:
void SetUp() override {
// Create synthetic stereo images for testing
// Generate random pattern for left image
int imageWidth = 160;
int imageHeight = 120;
leftImage_ = cv::Mat(imageHeight, imageWidth, CV_8UC1);
cv::randu(leftImage_, cv::Scalar(0), cv::Scalar(256));
// Shift the entire left image left by shiftPixels_ to create the right image
// Right image will have zeros on the right edge where there's no correspondence
shiftPixels_ = 5;
rightImage_ = cv::Mat::zeros(leftImage_.size(), CV_8UC1);
cv::Rect leftROI(shiftPixels_, 0, leftImage_.cols - shiftPixels_, leftImage_.rows);
cv::Rect rightROI(0, 0, leftImage_.cols - shiftPixels_, leftImage_.rows);
leftImage_(leftROI).copyTo(rightImage_(rightROI));
// Detect feature points in the left image
// Use a simple corner detector to get test points
cv::Mat corners;
cv::goodFeaturesToTrack(leftImage_, corners, 50, 0.01, 10);
leftCorners_.clear();
for(int i = 0; i < corners.rows; ++i)
{
leftCorners_.push_back(cv::Point2f(corners.at<float>(i, 0), corners.at<float>(i, 1)));
}
// Filter corners to be in valid region (not too close to edges and within valid disparity range)
std::vector<cv::Point2f> filteredCorners;
for(const cv::Point2f& pt : leftCorners_)
{
// Ensure corner is not too close to edges and will have valid correspondence
if(pt.x >= shiftPixels_ + 10 && pt.x < leftImage_.cols - 10 &&
pt.y >= 10 && pt.y < leftImage_.rows - 10)
{
filteredCorners.push_back(pt);
}
}
leftCorners_ = filteredCorners;
// Ensure we have some corners to test with
if(leftCorners_.empty())
{
// Add some manual test points if no corners detected
leftCorners_.push_back(cv::Point2f(50, 50));
leftCorners_.push_back(cv::Point2f(80, 60));
leftCorners_.push_back(cv::Point2f(100, 40));
}
}
void TearDown() override {
}
cv::Mat leftImage_;
cv::Mat rightImage_;
std::vector<cv::Point2f> leftCorners_;
int shiftPixels_; ///< Number of pixels the right image is shifted left (expected disparity)
};
// Helper function to get strategy name for error messages
const char* getStrategyName(bool opticalFlow) {
return opticalFlow ? "OpticalFlow" : "BlockMatching";
}
// Stereo Tests
TEST_F(StereoTest, Constructor)
{
// Test with both strategies
bool strategies[] = {false, true}; // false = BlockMatching, true = OpticalFlow
for(bool opticalFlow : strategies)
{
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), opticalFlow ? "true" : "false"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow);
}
}
TEST_F(StereoTest, ParseParameters)
{
// Test with both strategies
bool strategies[] = {false, true};
for(bool opticalFlow : strategies)
{
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), opticalFlow ? "true" : "false"));
params.insert(ParametersPair(Parameters::kStereoWinWidth(), "10"));
params.insert(ParametersPair(Parameters::kStereoWinHeight(), "10"));
params.insert(ParametersPair(Parameters::kStereoIterations(), "10"));
params.insert(ParametersPair(Parameters::kStereoMaxLevel(), "2"));
params.insert(ParametersPair(Parameters::kStereoMinDisparity(), "0.0"));
params.insert(ParametersPair(Parameters::kStereoMaxDisparity(), "64.0"));
params.insert(ParametersPair(Parameters::kStereoSSD(), "true"));
params.insert(ParametersPair(Parameters::kStereoEps(), "0.01"));
params.insert(ParametersPair(Parameters::kStereoGpu(), "false"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow);
stereo->parseParameters(params);
// Should not throw
EXPECT_TRUE(true) << "Strategy: " << getStrategyName(opticalFlow);
}
}
TEST_F(StereoTest, ComputeCorrespondences)
{
// Test with both strategies
bool strategies[] = {false, true};
for(bool opticalFlow : strategies)
{
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), opticalFlow ? "true" : "false"));
params.insert(ParametersPair(Parameters::kStereoMinDisparity(), "0.0"));
params.insert(ParametersPair(Parameters::kStereoMaxDisparity(), "20.0")); // Should cover shiftPixels_ (5)
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow);
std::vector<unsigned char> status;
std::vector<cv::Point2f> rightCorners = stereo->computeCorrespondences(
leftImage_, rightImage_, leftCorners_, status);
EXPECT_EQ(rightCorners.size(), leftCorners_.size()) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_EQ(status.size(), leftCorners_.size()) << "Strategy: " << getStrategyName(opticalFlow);
// Count valid correspondences
int validCount = 0;
int correctDisparityCount = 0;
for(size_t i = 0; i < status.size(); ++i)
{
if(status[i] != 0)
{
validCount++;
// Check that disparity is approximately shiftPixels_
float disparity = leftCorners_[i].x - rightCorners[i].x;
if(std::abs(disparity - shiftPixels_) < 2.0f) // Allow 2 pixels tolerance
{
correctDisparityCount++;
}
}
}
// Should have some valid correspondences
EXPECT_GT(validCount, 0) << "Strategy: " << getStrategyName(opticalFlow);
// Most valid correspondences should have correct disparity
if(validCount > 0)
{
float correctRatio = static_cast<float>(correctDisparityCount) / validCount;
EXPECT_GT(correctRatio, 0.5f) << "Strategy: " << getStrategyName(opticalFlow); // At least 50% should be correct
}
}
}
TEST_F(StereoTest, ComputeCorrespondencesEmptyCorners)
{
// Test with both strategies
bool strategies[] = {false, true};
for(bool opticalFlow : strategies)
{
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), opticalFlow ? "true" : "false"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow);
std::vector<cv::Point2f> emptyCorners;
std::vector<unsigned char> status;
std::vector<cv::Point2f> rightCorners;
rightCorners = stereo->computeCorrespondences(
leftImage_, rightImage_, emptyCorners, status);
EXPECT_EQ(rightCorners.size(), 0u) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_EQ(status.size(), 0u) << "Strategy: " << getStrategyName(opticalFlow);
}
}
TEST_F(StereoTest, ComputeCorrespondencesDifferentSizes)
{
// Test with both strategies
bool strategies[] = {false, true};
for(bool opticalFlow : strategies)
{
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), opticalFlow ? "true" : "false"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow);
cv::Mat smallRight = cv::Mat::zeros(50, 50, CV_8UC1);
std::vector<unsigned char> status;
// Should throw
EXPECT_THROW(stereo->computeCorrespondences(leftImage_, smallRight, leftCorners_, status), UException)
<< "Strategy: " << getStrategyName(opticalFlow);
}
}
TEST_F(StereoTest, ComputeCorrespondencesEmptyImages)
{
// Test with both strategies
bool strategies[] = {false, true};
for(bool opticalFlow : strategies)
{
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), opticalFlow ? "true" : "false"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow);
cv::Mat emptyLeft, emptyRight;
std::vector<unsigned char> status;
// Should throw
EXPECT_THROW(stereo->computeCorrespondences(emptyLeft, emptyRight, leftCorners_, status), UException)
<< "Strategy: " << getStrategyName(opticalFlow);
}
}
TEST_F(StereoTest, GetterMethods)
{
// Test with both strategies
bool strategies[] = {false, true};
for(bool opticalFlow : strategies)
{
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), opticalFlow ? "true" : "false"));
params.insert(ParametersPair(Parameters::kStereoWinWidth(), "12"));
params.insert(ParametersPair(Parameters::kStereoWinHeight(), "8"));
params.insert(ParametersPair(Parameters::kStereoIterations(), "15"));
params.insert(ParametersPair(Parameters::kStereoMaxLevel(), "3"));
params.insert(ParametersPair(Parameters::kStereoMinDisparity(), "1.0"));
params.insert(ParametersPair(Parameters::kStereoMaxDisparity(), "50.0"));
params.insert(ParametersPair(Parameters::kStereoSSD(), "false"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow);
cv::Size winSize = stereo->winSize();
EXPECT_EQ(winSize.width, 12) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_EQ(winSize.height, 8) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_EQ(stereo->iterations(), 15) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_EQ(stereo->maxLevel(), 3) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_FLOAT_EQ(stereo->minDisparity(), 1.0f) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_FLOAT_EQ(stereo->maxDisparity(), 50.0f) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_FALSE(stereo->winSSD()) << "Strategy: " << getStrategyName(opticalFlow);
EXPECT_FALSE(stereo->isGpuEnabled()) << "Strategy: " << getStrategyName(opticalFlow);
}
}
TEST_F(StereoTest, StereoOpticalFlow_IsGpuEnabled)
{
// Test GPU enable/disable for optical flow
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), "true"));
params.insert(ParametersPair(Parameters::kStereoGpu(), "false"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr);
EXPECT_FALSE(stereo->isGpuEnabled());
// Test with GPU enabled (may not be available)
ParametersMap params2;
params2.insert(ParametersPair(Parameters::kStereoOpticalFlow(), "true"));
params2.insert(ParametersPair(Parameters::kStereoGpu(), "true"));
std::unique_ptr<Stereo> stereo2(Stereo::create(params2));
EXPECT_NE(stereo2.get(), nullptr);
// GPU may or may not be enabled depending on build configuration
// Just verify the method doesn't crash
bool gpuEnabled = stereo2->isGpuEnabled();
(void)gpuEnabled; // Suppress unused variable warning
}
TEST_F(StereoTest, StereoOpticalFlow_Epsilon)
{
// Test epsilon parameter for optical flow
ParametersMap params;
params.insert(ParametersPair(Parameters::kStereoOpticalFlow(), "true"));
params.insert(ParametersPair(Parameters::kStereoEps(), "0.005"));
std::unique_ptr<Stereo> stereo(Stereo::create(params));
EXPECT_NE(stereo.get(), nullptr);
// Cast to StereoOpticalFlow to access epsilon()
StereoOpticalFlow* opticalFlow = dynamic_cast<StereoOpticalFlow*>(stereo.get());
EXPECT_NE(opticalFlow, nullptr);
EXPECT_FLOAT_EQ(opticalFlow->epsilon(), 0.005f);
}