feat(stereo): add optional disparity post-filters
- add configurable median and edge-aware WLS filtering to the CPU SGBM matcher - preserve SGBM rejection holes when WLS smooths valid disparities - detect and link OpenCV ximgproc when available, with a graceful fallback otherwise - forward CPU tuning through the matcher factory and cover validation and hole preservation
This commit is contained in:
parent
4f21e2ea38
commit
96bc82f0fb
@ -15,6 +15,9 @@ namespace score {
|
||||
/// away from the true surface.
|
||||
class CpuStereoMatcher : public IStereoMatcher {
|
||||
public:
|
||||
/// @brief True when this build can apply the WLS disparity filter.
|
||||
[[nodiscard]] static bool wls_available() noexcept;
|
||||
|
||||
/// @brief SGBM tuning parameters.
|
||||
struct Params {
|
||||
int block_size; ///< Odd matching block size (SADWindowSize).
|
||||
@ -23,11 +26,27 @@ class CpuStereoMatcher : public IStereoMatcher {
|
||||
int speckle_range; ///< Max disparity variation inside a blob.
|
||||
int disp12_max_diff; ///< Max left-right disparity mismatch (px).
|
||||
int pre_filter_cap; ///< x-derivative clipping value.
|
||||
/// Median filter applied to the disparity map (0 disables, else odd
|
||||
/// 3 or 5). Only trims isolated spikes; SGBM's sub-pixel noise is
|
||||
/// correlated over several pixels, so prefer the WLS filter.
|
||||
int median_kernel;
|
||||
/// Edge-aware weighted-least-squares smoothing of the disparity
|
||||
/// (cv::ximgproc::DisparityWLSFilter). Off by default: on SCARED it
|
||||
/// halves the fine-scale surface roughness (mesh normals 45 -> 23
|
||||
/// deg off-axis) but costs accuracy (MAE 0.84 -> 0.92 mm, within
|
||||
/// 2 mm 80 -> 78 %) and a second SGBM pass (~2x matching time).
|
||||
/// Enable for visualisation, keep off for measurement. Ignored,
|
||||
/// with a warning, when OpenCV was built without ximgproc.
|
||||
bool wls_filter;
|
||||
double wls_lambda; ///< Smoothness weight (2000; 8000 over-smooths).
|
||||
double wls_sigma; ///< Edge sensitivity (typical 0.8-2.0).
|
||||
// Explicit constructor avoids a GCC limitation with nested-struct
|
||||
// default-member-initialisers used as default function arguments.
|
||||
Params() noexcept
|
||||
: block_size(5), uniqueness_ratio(10), speckle_window_size(100),
|
||||
speckle_range(2), disp12_max_diff(1), pre_filter_cap(31) {}
|
||||
speckle_range(2), disp12_max_diff(1), pre_filter_cap(31),
|
||||
median_kernel(0), wls_filter(false), wls_lambda(2000.0),
|
||||
wls_sigma(1.5) {}
|
||||
};
|
||||
|
||||
CpuStereoMatcher(int min_disparity = 0, int num_disparities = 128,
|
||||
@ -39,6 +58,11 @@ class CpuStereoMatcher : public IStereoMatcher {
|
||||
|
||||
private:
|
||||
cv::Ptr<cv::StereoSGBM> sgbm_;
|
||||
cv::Ptr<cv::StereoMatcher> right_matcher_; ///< Only with WLS.
|
||||
cv::Ptr<cv::Algorithm> wls_; ///< DisparityWLSFilter.
|
||||
int median_kernel_;
|
||||
double wls_lambda_;
|
||||
double wls_sigma_;
|
||||
};
|
||||
|
||||
} // namespace score
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "cloud_point/cpu_stereo_matcher.hpp"
|
||||
#include "cloud_point/stereo_matcher.hpp"
|
||||
#include <memory>
|
||||
|
||||
@ -12,12 +13,16 @@ class StereoMatcherFactory {
|
||||
public:
|
||||
/// @brief Create a stereo matcher of the requested type.
|
||||
/// If GPU is requested but unavailable, falls back to CPU.
|
||||
/// @param num_disparities Number of disparity levels for SGBM (default 128).
|
||||
/// @param num_disparities Number of disparity levels for SGBM (default
|
||||
/// 128).
|
||||
/// Must be a positive multiple of 16.
|
||||
/// @param cpu_params SGBM tuning used for the CPU matcher (and the
|
||||
/// CPU fallback of the GPU matcher).
|
||||
/// @throws std::invalid_argument if @p num_disparities is not a positive
|
||||
/// multiple of 16.
|
||||
[[nodiscard]] static std::unique_ptr<IStereoMatcher>
|
||||
create(StereoAlgorithmType type, int num_disparities = 128);
|
||||
create(StereoAlgorithmType type, int num_disparities = 128,
|
||||
CpuStereoMatcher::Params cpu_params = CpuStereoMatcher::Params{});
|
||||
};
|
||||
|
||||
} // namespace score
|
||||
|
||||
@ -1,12 +1,35 @@
|
||||
#include "cloud_point/cpu_stereo_matcher.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <glog/logging.h>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#ifdef CLOUD_POINT_HAVE_OPENCV_XIMGPROC
|
||||
#include <opencv2/ximgproc/disparity_filter.hpp>
|
||||
#endif
|
||||
|
||||
namespace score {
|
||||
|
||||
bool CpuStereoMatcher::wls_available() noexcept {
|
||||
#ifdef CLOUD_POINT_HAVE_OPENCV_XIMGPROC
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
CpuStereoMatcher::CpuStereoMatcher(int min_disparity, int num_disparities,
|
||||
Params params) {
|
||||
Params params)
|
||||
: median_kernel_(params.median_kernel), wls_lambda_(params.wls_lambda),
|
||||
wls_sigma_(params.wls_sigma) {
|
||||
if (params.median_kernel != 0 && params.median_kernel != 3 &&
|
||||
params.median_kernel != 5) {
|
||||
throw std::invalid_argument(
|
||||
"CpuStereoMatcher: median_kernel must be 0, 3 or 5, got " +
|
||||
std::to_string(params.median_kernel));
|
||||
}
|
||||
if (params.block_size < 1 || params.block_size % 2 == 0) {
|
||||
throw std::invalid_argument(
|
||||
"CpuStereoMatcher: block_size must be a positive odd number, got " +
|
||||
@ -23,11 +46,57 @@ CpuStereoMatcher::CpuStereoMatcher(int min_disparity, int num_disparities,
|
||||
params.disp12_max_diff, params.pre_filter_cap, params.uniqueness_ratio,
|
||||
params.speckle_window_size, params.speckle_range,
|
||||
cv::StereoSGBM::MODE_SGBM);
|
||||
|
||||
if (params.wls_filter) {
|
||||
#ifdef CLOUD_POINT_HAVE_OPENCV_XIMGPROC
|
||||
// NB: createDisparityWLSFilter(sgbm_) would silently disable the
|
||||
// left matcher's uniqueness/speckle/LRC post-filters to obtain a
|
||||
// dense map for its own confidence estimate. The generic factory
|
||||
// leaves our tuned matcher alone; we then keep SGBM's holes invalid.
|
||||
right_matcher_ = cv::ximgproc::createRightMatcher(sgbm_);
|
||||
auto wls = cv::ximgproc::createDisparityWLSFilterGeneric(
|
||||
/*use_confidence=*/true);
|
||||
wls->setLambda(params.wls_lambda);
|
||||
wls->setSigmaColor(params.wls_sigma);
|
||||
wls->setLRCthresh(24); // 1.5 px in SGBM's 16x fixed point
|
||||
wls->setDepthDiscontinuityRadius(
|
||||
std::max(1, params.block_size / 2 + 1));
|
||||
wls_ = wls;
|
||||
#else
|
||||
LOG(WARNING) << "CpuStereoMatcher: WLS filter requested but OpenCV "
|
||||
"was built without ximgproc; disparity is unfiltered";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
cv::Mat CpuStereoMatcher::compute(const cv::Mat &left, const cv::Mat &right) {
|
||||
cv::Mat disparity;
|
||||
sgbm_->compute(left, right, disparity);
|
||||
|
||||
#ifdef CLOUD_POINT_HAVE_OPENCV_XIMGPROC
|
||||
// The WLS filter needs a valid ROI to the right of the disparity search
|
||||
// range; skip it (raw SGBM output) for images narrower than that.
|
||||
const int roi_width = sgbm_->getMinDisparity() +
|
||||
sgbm_->getNumDisparities() + sgbm_->getBlockSize();
|
||||
if (wls_ && left.cols > roi_width) {
|
||||
// Snapshot SGBM's holes first: the filter may modify its inputs.
|
||||
const cv::Mat holes = disparity <= 0;
|
||||
cv::Mat right_disparity, filtered;
|
||||
right_matcher_->compute(right, left, right_disparity);
|
||||
auto wls = wls_.dynamicCast<cv::ximgproc::DisparityWLSFilter>();
|
||||
wls->filter(disparity, left, filtered, right_disparity);
|
||||
// WLS extrapolates into pixels SGBM rejected; keep those invalid so
|
||||
// holes stay holes and coverage is not inflated with guesses.
|
||||
filtered.setTo(cv::Scalar(sgbm_->getMinDisparity() * 16 - 16), holes);
|
||||
disparity = filtered;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (median_kernel_ > 0) {
|
||||
cv::Mat filtered;
|
||||
cv::medianBlur(disparity, filtered, median_kernel_);
|
||||
disparity = filtered;
|
||||
}
|
||||
return disparity;
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,19 @@ if opencv_cuda_available
|
||||
cpp_args += '-DHAVE_OPENCV_CUDA'
|
||||
endif
|
||||
|
||||
# Optional ximgproc (opencv_contrib) for the WLS disparity post-filter.
|
||||
opencv_ximgproc_lib = disabler()
|
||||
if cxx.has_header('opencv2/ximgproc/disparity_filter.hpp', dependencies: opencv_dep)
|
||||
opencv_ximgproc_lib = cxx.find_library('opencv_ximgproc', required: false)
|
||||
endif
|
||||
if opencv_ximgproc_lib.found()
|
||||
cpc_deps += opencv_ximgproc_lib
|
||||
cpp_args += '-DCLOUD_POINT_HAVE_OPENCV_XIMGPROC'
|
||||
message('opencv_ximgproc found: WLS disparity filter enabled')
|
||||
else
|
||||
message('opencv_ximgproc not found: WLS disparity filter disabled')
|
||||
endif
|
||||
|
||||
cloud_point_compute_lib = shared_library('cloud_point_compute',
|
||||
sources: cloud_point_sources,
|
||||
include_directories: inc,
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
namespace score {
|
||||
|
||||
std::unique_ptr<IStereoMatcher>
|
||||
StereoMatcherFactory::create(StereoAlgorithmType type, int num_disparities) {
|
||||
StereoMatcherFactory::create(StereoAlgorithmType type, int num_disparities,
|
||||
CpuStereoMatcher::Params cpu_params) {
|
||||
if (num_disparities <= 0 || num_disparities % 16 != 0) {
|
||||
throw std::invalid_argument(
|
||||
"StereoMatcherFactory: num_disparities must be a positive "
|
||||
@ -17,14 +18,16 @@ StereoMatcherFactory::create(StereoAlgorithmType type, int num_disparities) {
|
||||
}
|
||||
switch (type) {
|
||||
case StereoAlgorithmType::CPU:
|
||||
return std::make_unique<CpuStereoMatcher>(0, num_disparities);
|
||||
return std::make_unique<CpuStereoMatcher>(0, num_disparities,
|
||||
cpu_params);
|
||||
case StereoAlgorithmType::GPU:
|
||||
try {
|
||||
return std::make_unique<GpuStereoMatcher>(0, num_disparities);
|
||||
} catch (const std::exception &e) {
|
||||
LOG(WARNING) << "GPU stereo matcher unavailable: " << e.what()
|
||||
<< ". Falling back to CPU.";
|
||||
return std::make_unique<CpuStereoMatcher>(0, num_disparities);
|
||||
return std::make_unique<CpuStereoMatcher>(0, num_disparities,
|
||||
cpu_params);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
@ -55,6 +55,41 @@ TEST(StereoMatcherTest, CpuMatcherRejectsEvenBlockSize) {
|
||||
EXPECT_THROW(CpuStereoMatcher(0, 64, params), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(StereoMatcherTest, CpuMatcherRejectsBadMedianKernel) {
|
||||
CpuStereoMatcher::Params params;
|
||||
params.median_kernel = 4;
|
||||
EXPECT_THROW(CpuStereoMatcher(0, 64, params), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(StereoMatcherTest, WlsKeepsSgbmHolesInvalid) {
|
||||
if (!CpuStereoMatcher::wls_available())
|
||||
GTEST_SKIP() << "OpenCV built without ximgproc";
|
||||
// Left 64 px of the right image have no counterpart -> SGBM leaves the
|
||||
// left border invalid; WLS must not fill it with extrapolated values.
|
||||
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(8, 0, left.cols - 8, left.rows))
|
||||
.copyTo(right(cv::Rect(0, 0, left.cols - 8, left.rows)));
|
||||
|
||||
CpuStereoMatcher::Params raw;
|
||||
raw.wls_filter = false;
|
||||
CpuStereoMatcher::Params wls;
|
||||
wls.wls_filter = true;
|
||||
cv::Mat d_raw = CpuStereoMatcher(0, 64, raw).compute(left, right);
|
||||
cv::Mat d_wls = CpuStereoMatcher(0, 64, wls).compute(left, right);
|
||||
ASSERT_EQ(d_wls.type(), CV_16S);
|
||||
ASSERT_EQ(d_wls.size(), d_raw.size());
|
||||
const int raw_invalid = cv::countNonZero(d_raw <= 0);
|
||||
EXPECT_GT(raw_invalid, 0);
|
||||
// No SGBM hole may be filled with an extrapolated disparity.
|
||||
const int filled = cv::countNonZero((d_raw <= 0) & (d_wls > 0));
|
||||
EXPECT_EQ(filled, 0);
|
||||
// The filter must still produce a valid map elsewhere.
|
||||
EXPECT_GT(cv::countNonZero(d_wls > 0), 0);
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user