From e9291bd926d02d957d4449bc5e56c7c991aee982 Mon Sep 17 00:00:00 2001 From: matlabbe Date: Tue, 23 Dec 2025 11:50:16 -0800 Subject: [PATCH] Added Stereo tests --- corelib/include/rtabmap/core/Stereo.h | 245 +++++++++++++- corelib/src/Stereo.cpp | 14 + corelib/test/CMakeLists.txt | 9 +- corelib/test/test_stereo.cpp | 315 ++++++++++++++++++ .../test/{stereo => }/test_stereo_dense.cpp | 0 5 files changed, 572 insertions(+), 11 deletions(-) create mode 100644 corelib/test/test_stereo.cpp rename corelib/test/{stereo => }/test_stereo_dense.cpp (100%) diff --git a/corelib/include/rtabmap/core/Stereo.h b/corelib/include/rtabmap/core/Stereo.h index 497f8067..3bfffed7 100644 --- a/corelib/include/rtabmap/core/Stereo.h +++ b/corelib/include/rtabmap/core/Stereo.h @@ -35,21 +35,111 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 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 { 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()); 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()); + + /** + * @brief Virtual destructor + */ 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); + + /** + * @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 computeCorrespondences( const cv::Mat & leftImage, const cv::Mat & rightImage, const std::vector & leftCorners, std::vector & status) const; #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 computeCorrespondences( const cv::cuda::GpuMat & leftImage, const cv::cuda::GpuMat & rightImage, @@ -57,30 +147,125 @@ public: std::vector & status) const; #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_);} + + /** + * @brief Get the number of iterations + * @return Number of iterations for the matching algorithm + */ 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_;} + + /** + * @brief Get the minimum disparity value + * @return Minimum disparity value to search (in pixels) + */ float minDisparity() const {return minDisparity_;} + + /** + * @brief Get the maximum disparity value + * @return Maximum disparity value to search (in pixels) + */ 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_;} + + /** + * @brief Check if GPU acceleration is enabled + * @return Always returns false for the base Stereo class + */ virtual bool isGpuEnabled() const {return false;} private: - int winWidth_; - int winHeight_; - int iterations_; - int maxLevel_; - float minDisparity_; - float maxDisparity_; - bool winSSD_; + int winWidth_; ///< Search window width (default: from Parameters::defaultStereoWinWidth()) + int winHeight_; ///< Search window height (default: from Parameters::defaultStereoWinHeight()) + int iterations_; ///< Number of iterations for matching (default: from Parameters::defaultStereoIterations()) + int maxLevel_; ///< Maximum pyramid level (default: from Parameters::defaultStereoMaxLevel()) + float minDisparity_; ///< Minimum disparity value to search (default: from Parameters::defaultStereoMinDisparity()) + float maxDisparity_; ///< Maximum disparity value to search (default: from Parameters::defaultStereoMaxDisparity()) + 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 { 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()); + + /** + * @brief Virtual destructor + */ 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); + + /** + * @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 computeCorrespondences( const cv::Mat & leftImage, const cv::Mat & rightImage, @@ -88,6 +273,21 @@ public: std::vector & status) const; #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 computeCorrespondences( const cv::cuda::GpuMat & leftImage, const cv::cuda::GpuMat & rightImage, @@ -95,18 +295,45 @@ public: std::vector & status) const; #endif + /** + * @brief Get the convergence threshold (epsilon) + * @return Convergence threshold for optical flow iteration termination + */ 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; 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( const std::vector & leftCorners, const std::vector & rightCorners, std::vector & status) const; private: - float epsilon_; - bool gpu_; + float epsilon_; ///< Convergence threshold for optical flow (default: from Parameters::defaultStereoEps()) + bool gpu_; ///< Enable GPU acceleration (default: from Parameters::defaultStereoGpu(), requires OpenCV CUDA) }; } /* namespace rtabmap */ diff --git a/corelib/src/Stereo.cpp b/corelib/src/Stereo.cpp index 0269f2b5..1a1db1c1 100644 --- a/corelib/src/Stereo.cpp +++ b/corelib/src/Stereo.cpp @@ -79,8 +79,15 @@ std::vector Stereo::computeCorrespondences( const std::vector & leftCorners, std::vector & status) const { + if(leftCorners.empty()) + { + status.clear(); + return std::vector(); + } + UASSERT(!leftImage.empty() && !rightImage.empty()); UASSERT(leftImage.type() == CV_8UC1); UASSERT(rightImage.type() == CV_8UC1); + UASSERT(leftImage.size() == rightImage.size()); std::vector rightCorners; UDEBUG("util2d::calcStereoCorrespondences() begin"); rightCorners = util2d::calcStereoCorrespondences( @@ -147,8 +154,15 @@ std::vector StereoOpticalFlow::computeCorrespondences( const std::vector & leftCorners, std::vector & status) const { + if(leftCorners.empty()) + { + status.clear(); + return std::vector(); + } + UASSERT(!leftImage.empty() && !rightImage.empty()); UASSERT(leftImage.type() == CV_8UC1); UASSERT(rightImage.type() == CV_8UC1); + UASSERT(leftImage.size() == rightImage.size()); std::vector rightCorners; std::vector err; #ifdef HAVE_OPENCV_CUDAOPTFLOW diff --git a/corelib/test/CMakeLists.txt b/corelib/test/CMakeLists.txt index 101b55a8..8488281b 100644 --- a/corelib/test/CMakeLists.txt +++ b/corelib/test/CMakeLists.txt @@ -67,6 +67,11 @@ target_link_libraries(test_transform gtest_main rtabmap_core) gtest_discover_tests(test_transform) #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) -gtest_discover_tests(test_stereo_dense) \ No newline at end of file +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) \ No newline at end of file diff --git a/corelib/test/test_stereo.cpp b/corelib/test/test_stereo.cpp new file mode 100644 index 00000000..87b4044d --- /dev/null +++ b/corelib/test/test_stereo.cpp @@ -0,0 +1,315 @@ +#include +#include +#include +#include +#include "rtabmap/core/Stereo.h" +#include "rtabmap/core/Parameters.h" +#include "rtabmap/utilite/ULogger.h" +#include "rtabmap/utilite/UException.h" +#include +#include + +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(i, 0), corners.at(i, 1))); + } + + // Filter corners to be in valid region (not too close to edges and within valid disparity range) + std::vector 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 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::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::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::create(params)); + EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow); + + std::vector status; + std::vector 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(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::create(params)); + EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow); + + std::vector emptyCorners; + std::vector status; + + std::vector 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::create(params)); + EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow); + + cv::Mat smallRight = cv::Mat::zeros(50, 50, CV_8UC1); + std::vector 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::create(params)); + EXPECT_NE(stereo.get(), nullptr) << "Strategy: " << getStrategyName(opticalFlow); + + cv::Mat emptyLeft, emptyRight; + std::vector 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::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::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 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::create(params)); + EXPECT_NE(stereo.get(), nullptr); + + // Cast to StereoOpticalFlow to access epsilon() + StereoOpticalFlow* opticalFlow = dynamic_cast(stereo.get()); + EXPECT_NE(opticalFlow, nullptr); + EXPECT_FLOAT_EQ(opticalFlow->epsilon(), 0.005f); +} + diff --git a/corelib/test/stereo/test_stereo_dense.cpp b/corelib/test/test_stereo_dense.cpp similarity index 100% rename from corelib/test/stereo/test_stereo_dense.cpp rename to corelib/test/test_stereo_dense.cpp