score-back/tests/test_stereo_matcher.cpp
Artur Mukhamadiev 4f21e2ea38 fix(cloud_point): regularise SGBM and export viewable PLY meshes
- Configure StereoSGBM with P1/P2 penalties, 5x5 block, uniqueness ratio,
  speckle filter and left-right check; previously it ran unregularised and
  produced a heavy tail of low-disparity outliers reprojecting metres away
  (SCARED benchmark MAE3D 44 mm -> 0.8 mm, RMSE 289 mm -> 1.4 mm)
- Pass the requested disparity count to the CUDA SGM matcher (rounded to
  64/128/256) instead of a hard-coded 16
- Add optional `cloud_point` config section (algorithm, num_disparities,
  depth range) consumed by CLI options 4/5; ship config.scared.yml
- write_ply now emits a binary PLY with a grid-triangulated mesh so web
  viewers stop fabricating slivers from consecutive vertices
- Add config, matcher and PLY export tests; document in README
2026-09-14 10:43:24 +03:00

128 lines
4.4 KiB
C++

#include <cmath>
#include <gtest/gtest.h>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <tuple>
#include "cloud_point/cpu_stereo_matcher.hpp"
#include "cloud_point/gpu_stereo_matcher.hpp"
#include "cloud_point/stereo_matcher_factory.hpp"
using namespace score;
namespace {
/// @brief Create a simple synthetic stereo pair with a horizontal shift.
std::pair<cv::Mat, cv::Mat> make_synthetic_stereo(int shift = 2) {
cv::Mat left = cv::Mat::zeros(100, 100, CV_8UC1);
cv::Mat right = cv::Mat::zeros(100, 100, CV_8UC1);
for (int y = 0; y < 100; ++y) {
for (int x = 0; x < 100; ++x) {
left.at<uchar>(y, x) = static_cast<uchar>(x % 256);
right.at<uchar>(y, x) = static_cast<uchar>((x + shift) % 256);
}
}
return {left, right};
}
} // namespace
TEST(StereoMatcherTest, CpuMatcherComputesDisparity) {
auto [left, right] = make_synthetic_stereo();
CpuStereoMatcher matcher;
cv::Mat disparity = matcher.compute(left, right);
EXPECT_FALSE(disparity.empty());
EXPECT_EQ(disparity.rows, left.rows);
EXPECT_EQ(disparity.cols, left.cols);
}
TEST(StereoMatcherTest, FactoryCpuCreatesNonNull) {
auto matcher = StereoMatcherFactory::create(StereoAlgorithmType::CPU);
ASSERT_NE(matcher, nullptr);
auto [left, right] = make_synthetic_stereo();
cv::Mat disparity = matcher->compute(left, right);
EXPECT_FALSE(disparity.empty());
}
TEST(StereoMatcherTest, CpuMatcherRejectsEvenBlockSize) {
CpuStereoMatcher::Params params;
params.block_size = 4;
EXPECT_THROW(CpuStereoMatcher(0, 64, params), std::invalid_argument);
}
TEST(StereoMatcherTest, CpuMatcherRecoversKnownShift) {
// Textured synthetic pair: right image is the left shifted by 8 px, so
// the (16x fixed-point) disparity in the interior should be ~8 px.
constexpr int kShift = 8;
cv::Mat left(120, 200, CV_8UC1);
cv::randu(left, 0, 255);
cv::blur(left, left, cv::Size(3, 3));
cv::Mat right = cv::Mat::zeros(left.size(), CV_8UC1);
left(cv::Rect(kShift, 0, left.cols - kShift, left.rows))
.copyTo(right(cv::Rect(0, 0, left.cols - kShift, left.rows)));
CpuStereoMatcher matcher(0, 64);
cv::Mat disparity = matcher.compute(left, right);
ASSERT_EQ(disparity.type(), CV_16S);
int good = 0, total = 0;
for (int y = 10; y < left.rows - 10; ++y) {
for (int x = 70; x < left.cols - 20; ++x) {
const float d = disparity.at<short>(y, x) / 16.0f;
if (d <= 0)
continue;
++total;
if (std::abs(d - kShift) <= 1.0f)
++good;
}
}
ASSERT_GT(total, 0);
EXPECT_GT(static_cast<double>(good) / total, 0.9);
}
TEST(StereoMatcherTest, GpuSupportedNumDisparitiesRoundsUp) {
EXPECT_EQ(GpuStereoMatcher::supported_num_disparities(16), 64);
EXPECT_EQ(GpuStereoMatcher::supported_num_disparities(64), 64);
EXPECT_EQ(GpuStereoMatcher::supported_num_disparities(96), 128);
EXPECT_EQ(GpuStereoMatcher::supported_num_disparities(160), 256);
EXPECT_THROW(std::ignore = GpuStereoMatcher::supported_num_disparities(272),
std::invalid_argument);
}
TEST(StereoMatcherTest, FactoryRejectsInvalidNumDisparities) {
EXPECT_THROW(std::ignore =
StereoMatcherFactory::create(StereoAlgorithmType::CPU, 0),
std::invalid_argument);
EXPECT_THROW(std::ignore = StereoMatcherFactory::create(
StereoAlgorithmType::CPU, -16),
std::invalid_argument);
EXPECT_THROW(std::ignore = StereoMatcherFactory::create(
StereoAlgorithmType::CPU, 150),
std::invalid_argument);
}
TEST(StereoMatcherTest, FactoryAcceptsValidNumDisparities) {
auto matcher = StereoMatcherFactory::create(StereoAlgorithmType::CPU, 160);
ASSERT_NE(matcher, nullptr);
}
TEST(StereoMatcherTest, FactoryGpuFallsBackToCpuWhenUnavailable) {
// On this machine CUDA is absent; factory should fall back to CPU.
auto matcher = StereoMatcherFactory::create(StereoAlgorithmType::GPU);
ASSERT_NE(matcher, nullptr);
auto [left, right] = make_synthetic_stereo();
EXPECT_NO_THROW(std::ignore = matcher->compute(left, right));
}
TEST(StereoMatcherTest, GpuMatcherThrowsOnThisMachine) {
// Direct construction of GpuStereoMatcher should throw because
// HAVE_OPENCV_CUDA is undefined here.
EXPECT_THROW(GpuStereoMatcher(), std::runtime_error);
}