You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2365 lines
83 KiB
2365 lines
83 KiB
/* |
|
* SemiGlobalMatcher.cpp |
|
* |
|
* Copyright (c) 2014-2015 SEACAVE |
|
* |
|
* Author(s): |
|
* |
|
* cDc <cdc.seacave@gmail.com> |
|
* |
|
* |
|
* This program is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU Affero General Public License as published by |
|
* the Free Software Foundation, either version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* This program is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU Affero General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Affero General Public License |
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
* |
|
* |
|
* Additional Terms: |
|
* |
|
* You are required to preserve legal notices and author attributions in |
|
* that material or in the Appropriate Legal Notices displayed by works |
|
* containing it. |
|
*/ |
|
|
|
#include "Common.h" |
|
#include "SemiGlobalMatcher.h" |
|
#include "Scene.h" |
|
|
|
using namespace MVS; |
|
|
|
using namespace STEREO; |
|
|
|
|
|
// D E F I N E S /////////////////////////////////////////////////// |
|
|
|
// uncomment to enable OpenCV filter demo |
|
//#define _USE_FILTER_DEMO |
|
|
|
|
|
// S T R U C T S /////////////////////////////////////////////////// |
|
|
|
#ifdef _USE_FILTER_DEMO |
|
#include "opencv2/ximgproc/disparity_filter.hpp" |
|
|
|
const String keys = |
|
"{help h usage ? | | print this message }" |
|
"{GT |None | optional ground-truth disparity (MPI-Sintel or Middlebury format) }" |
|
"{dst_path |None | optional path to save the resulting filtered disparity map }" |
|
"{dst_raw_path |None | optional path to save raw disparity map before filtering }" |
|
"{algorithm |bm | stereo matching method (bm or sgbm) }" |
|
"{filter |wls_conf| used post-filtering (wls_conf or wls_no_conf) }" |
|
"{no-display | | don't display results }" |
|
"{no-downscale | | force stereo matching on full-sized views to improve quality }" |
|
"{dst_conf_path |None | optional path to save the confidence map used in filtering }" |
|
"{vis_mult |1.0 | coefficient used to scale disparity map visualizations }" |
|
"{max_disparity |160 | parameter of stereo matching }" |
|
"{window_size |-1 | parameter of stereo matching }" |
|
"{wls_lambda |8000.0 | parameter of post-filtering }" |
|
"{wls_sigma |1.5 | parameter of post-filtering }" |
|
; |
|
|
|
cv::Rect computeROI(cv::Size2i src_sz, cv::Ptr<cv::StereoMatcher> matcher_instance) |
|
{ |
|
int min_disparity = matcher_instance->getMinDisparity(); |
|
int num_disparities = matcher_instance->getNumDisparities(); |
|
int block_size = matcher_instance->getBlockSize(); |
|
|
|
int bs2 = block_size/2; |
|
int minD = min_disparity, maxD = min_disparity + num_disparities - 1; |
|
|
|
int xmin = maxD + bs2; |
|
int xmax = src_sz.width + minD - bs2; |
|
int ymin = bs2; |
|
int ymax = src_sz.height - bs2; |
|
|
|
return cv::Rect(xmin, ymin, xmax - xmin, ymax - ymin); |
|
} |
|
|
|
int disparityFiltering(cv::Mat left, cv::Mat right, int argc, const LPCSTR* argv) |
|
{ |
|
cv::CommandLineParser parser(argc,argv,keys); |
|
parser.about("Disparity Filtering Demo"); |
|
if(parser.has("help")) |
|
{ |
|
parser.printMessage(); |
|
return 0; |
|
} |
|
|
|
String GT_path = parser.get<String>("GT"); |
|
String dst_path = parser.get<String>("dst_path"); |
|
String dst_raw_path = parser.get<String>("dst_raw_path"); |
|
String dst_conf_path = parser.get<String>("dst_conf_path"); |
|
String algo = parser.get<String>("algorithm"); |
|
String filter = parser.get<String>("filter"); |
|
bool no_display = parser.has("no-display"); |
|
bool no_downscale = parser.has("no-downscale"); |
|
int max_disp = parser.get<int>("max_disparity"); |
|
double lambda = parser.get<double>("wls_lambda"); |
|
double sigma = parser.get<double>("wls_sigma"); |
|
double vis_mult = parser.get<double>("vis_mult"); |
|
|
|
int wsize; |
|
if(parser.get<int>("window_size")>=0) //user provided window_size value |
|
wsize = parser.get<int>("window_size"); |
|
else |
|
{ |
|
if(algo=="sgbm") |
|
wsize = 3; //default window size for SGBM |
|
else if(!no_downscale && algo=="bm" && filter=="wls_conf") |
|
wsize = 7; //default window size for BM on downscaled views (downscaling is performed only for wls_conf) |
|
else |
|
wsize = 15; //default window size for BM on full-sized views |
|
} |
|
if(!parser.check()) |
|
{ |
|
parser.printErrors(); |
|
return -1; |
|
} |
|
|
|
cv::Mat GT_disp; |
|
bool noGT(cv::ximgproc::readGT(GT_path,GT_disp)!=0); |
|
|
|
cv::Mat left_for_matcher, right_for_matcher; |
|
cv::Mat left_disp,right_disp; |
|
cv::Mat filtered_disp; |
|
cv::Mat conf_map(left.rows,left.cols,CV_8U); |
|
conf_map = cv::Scalar(255); |
|
cv::Rect ROI; |
|
cv::Ptr<cv::ximgproc::DisparityWLSFilter> wls_filter; |
|
double matching_time, filtering_time; |
|
if(max_disp<=0 || max_disp%16!=0) |
|
{ |
|
std::cout<<"Incorrect max_disparity value: it should be positive and divisible by 16"; |
|
return -1; |
|
} |
|
if(wsize<=0 || wsize%2!=1) |
|
{ |
|
std::cout<<"Incorrect window_size value: it should be positive and odd"; |
|
return -1; |
|
} |
|
if(filter=="wls_conf") // filtering with confidence (significantly better quality than wls_no_conf) |
|
{ |
|
if(!no_downscale) |
|
{ |
|
// downscale the views to speed-up the matching stage, as we will need to compute both left |
|
// and right disparity maps for confidence map computation |
|
//! [downscale] |
|
max_disp/=2; |
|
if(max_disp%16!=0) |
|
max_disp += 16-(max_disp%16); |
|
resize(left ,left_for_matcher ,cv::Size(),0.5,0.5, cv::INTER_LINEAR_EXACT); |
|
resize(right,right_for_matcher,cv::Size(),0.5,0.5, cv::INTER_LINEAR_EXACT); |
|
//! [downscale] |
|
} |
|
else |
|
{ |
|
left_for_matcher = left.clone(); |
|
right_for_matcher = right.clone(); |
|
} |
|
|
|
if(algo=="bm") |
|
{ |
|
//! [matching] |
|
cv::Ptr<cv::StereoBM> left_matcher = cv::StereoBM::create(max_disp,wsize); |
|
wls_filter = cv::ximgproc::createDisparityWLSFilter(left_matcher); |
|
cv::Ptr<cv::StereoMatcher> right_matcher = cv::ximgproc::createRightMatcher(left_matcher); |
|
|
|
cvtColor(left_for_matcher, left_for_matcher, cv::COLOR_BGR2GRAY); |
|
cvtColor(right_for_matcher, right_for_matcher, cv::COLOR_BGR2GRAY); |
|
|
|
matching_time = (double)cv::getTickCount(); |
|
left_matcher-> compute(left_for_matcher, right_for_matcher,left_disp); |
|
right_matcher->compute(right_for_matcher,left_for_matcher, right_disp); |
|
matching_time = ((double)cv::getTickCount() - matching_time)/cv::getTickFrequency(); |
|
//! [matching] |
|
} |
|
else if(algo=="sgbm") |
|
{ |
|
cv::Ptr<cv::StereoSGBM> left_matcher = cv::StereoSGBM::create(-max_disp/2+1,max_disp,wsize); |
|
left_matcher->setP1(24*wsize*wsize); |
|
left_matcher->setP2(96*wsize*wsize); |
|
left_matcher->setPreFilterCap(63); |
|
left_matcher->setMode(cv::StereoSGBM::MODE_SGBM_3WAY); |
|
wls_filter = cv::ximgproc::createDisparityWLSFilter(left_matcher); |
|
cv::Ptr<cv::StereoMatcher> right_matcher = cv::ximgproc::createRightMatcher(left_matcher); |
|
|
|
matching_time = (double)cv::getTickCount(); |
|
left_matcher-> compute(left_for_matcher, right_for_matcher,left_disp); |
|
right_matcher->compute(right_for_matcher,left_for_matcher, right_disp); |
|
matching_time = ((double)cv::getTickCount() - matching_time)/cv::getTickFrequency(); |
|
} |
|
else |
|
{ |
|
std::cout<<"Unsupported algorithm"; |
|
return -1; |
|
} |
|
|
|
//! [filtering] |
|
wls_filter->setLambda(lambda); |
|
wls_filter->setSigmaColor(sigma); |
|
filtering_time = (double)cv::getTickCount(); |
|
wls_filter->filter(left_disp,left,filtered_disp,right_disp); |
|
filtering_time = ((double)cv::getTickCount() - filtering_time)/cv::getTickFrequency(); |
|
//! [filtering] |
|
conf_map = wls_filter->getConfidenceMap(); |
|
|
|
// Get the ROI that was used in the last filter call: |
|
ROI = wls_filter->getROI(); |
|
if(!no_downscale) |
|
{ |
|
// upscale raw disparity and ROI back for a proper comparison: |
|
resize(left_disp,left_disp,cv::Size(),2.0,2.0,cv::INTER_LINEAR_EXACT); |
|
left_disp = left_disp*2.0; |
|
ROI = cv::Rect(ROI.x*2,ROI.y*2,ROI.width*2,ROI.height*2); |
|
} |
|
} |
|
else if(filter=="wls_no_conf") |
|
{ |
|
/* There is no convenience function for the case of filtering with no confidence, so we |
|
will need to set the ROI and matcher parameters manually */ |
|
|
|
left_for_matcher = left.clone(); |
|
right_for_matcher = right.clone(); |
|
|
|
if(algo=="bm") |
|
{ |
|
cv::Ptr<cv::StereoBM> matcher = cv::StereoBM::create(max_disp,wsize); |
|
matcher->setTextureThreshold(0); |
|
matcher->setUniquenessRatio(0); |
|
cvtColor(left_for_matcher, left_for_matcher, cv::COLOR_BGR2GRAY); |
|
cvtColor(right_for_matcher, right_for_matcher, cv::COLOR_BGR2GRAY); |
|
ROI = computeROI(left_for_matcher.size(),matcher); |
|
wls_filter = cv::ximgproc::createDisparityWLSFilterGeneric(false); |
|
wls_filter->setDepthDiscontinuityRadius((int)ceil(0.33*wsize)); |
|
|
|
matching_time = (double)cv::getTickCount(); |
|
matcher->compute(left_for_matcher,right_for_matcher,left_disp); |
|
matching_time = ((double)cv::getTickCount() - matching_time)/cv::getTickFrequency(); |
|
} |
|
else if(algo=="sgbm") |
|
{ |
|
cv::Ptr<cv::StereoSGBM> matcher = cv::StereoSGBM::create(0,max_disp,wsize); |
|
matcher->setUniquenessRatio(0); |
|
matcher->setDisp12MaxDiff(1000000); |
|
matcher->setSpeckleWindowSize(0); |
|
matcher->setP1(24*wsize*wsize); |
|
matcher->setP2(96*wsize*wsize); |
|
matcher->setMode(cv::StereoSGBM::MODE_SGBM_3WAY); |
|
ROI = computeROI(left_for_matcher.size(),matcher); |
|
wls_filter = cv::ximgproc::createDisparityWLSFilterGeneric(false); |
|
wls_filter->setDepthDiscontinuityRadius((int)ceil(0.5*wsize)); |
|
|
|
matching_time = (double)cv::getTickCount(); |
|
matcher->compute(left_for_matcher,right_for_matcher,left_disp); |
|
matching_time = ((double)cv::getTickCount() - matching_time)/cv::getTickFrequency(); |
|
} |
|
else |
|
{ |
|
std::cout<<"Unsupported algorithm"; |
|
return -1; |
|
} |
|
|
|
wls_filter->setLambda(lambda); |
|
wls_filter->setSigmaColor(sigma); |
|
filtering_time = (double)cv::getTickCount(); |
|
wls_filter->filter(left_disp,left,filtered_disp,cv::Mat(),ROI); |
|
filtering_time = ((double)cv::getTickCount() - filtering_time)/cv::getTickFrequency(); |
|
} |
|
else |
|
{ |
|
std::cout<<"Unsupported filter"; |
|
return -1; |
|
} |
|
|
|
//collect and print all the stats: |
|
std::cout.precision(2); |
|
std::cout<<"Matching time: "<<matching_time<<"s"<<std::endl; |
|
std::cout<<"Filtering time: "<<filtering_time<<"s"<<std::endl; |
|
std::cout<<std::endl; |
|
|
|
double MSE_before,percent_bad_before,MSE_after,percent_bad_after; |
|
if(!noGT) |
|
{ |
|
MSE_before = cv::ximgproc::computeMSE(GT_disp,left_disp,ROI); |
|
percent_bad_before = cv::ximgproc::computeBadPixelPercent(GT_disp,left_disp,ROI); |
|
MSE_after = cv::ximgproc::computeMSE(GT_disp,filtered_disp,ROI); |
|
percent_bad_after = cv::ximgproc::computeBadPixelPercent(GT_disp,filtered_disp,ROI); |
|
|
|
std::cout.precision(5); |
|
std::cout<<"MSE before filtering: "<<MSE_before<<std::endl; |
|
std::cout<<"MSE after filtering: "<<MSE_after<<std::endl; |
|
std::cout<<std::endl; |
|
std::cout.precision(3); |
|
std::cout<<"Percent of bad pixels before filtering: "<<percent_bad_before<<std::endl; |
|
std::cout<<"Percent of bad pixels after filtering: "<<percent_bad_after<<std::endl; |
|
} |
|
|
|
if(dst_path!="None") |
|
{ |
|
cv::Mat filtered_disp_vis; |
|
cv::ximgproc::getDisparityVis(filtered_disp,filtered_disp_vis,vis_mult); |
|
cv::imwrite(dst_path,filtered_disp_vis); |
|
} |
|
if(dst_raw_path!="None") |
|
{ |
|
cv::Mat raw_disp_vis; |
|
cv::ximgproc::getDisparityVis(left_disp,raw_disp_vis,vis_mult); |
|
cv::imwrite(dst_raw_path,raw_disp_vis); |
|
} |
|
if(dst_conf_path!="None") |
|
{ |
|
cv::imwrite(dst_conf_path,conf_map); |
|
} |
|
|
|
if(!no_display) |
|
{ |
|
cv::namedWindow("left", cv::WINDOW_AUTOSIZE); |
|
cv::imshow("left", left); |
|
cv::namedWindow("right", cv::WINDOW_AUTOSIZE); |
|
cv::imshow("right", right); |
|
|
|
if(!noGT) |
|
{ |
|
cv::Mat GT_disp_vis; |
|
cv::ximgproc::getDisparityVis(GT_disp,GT_disp_vis,vis_mult); |
|
cv::namedWindow("ground-truth disparity", cv::WINDOW_AUTOSIZE); |
|
cv::imshow("ground-truth disparity", GT_disp_vis); |
|
} |
|
|
|
//! [visualization] |
|
cv::Mat raw_disp_vis; |
|
cv::ximgproc::getDisparityVis(left_disp,raw_disp_vis,vis_mult); |
|
cv::namedWindow("raw disparity", cv::WINDOW_AUTOSIZE); |
|
cv::imshow("raw disparity", raw_disp_vis); |
|
cv::Mat filtered_disp_vis; |
|
cv::ximgproc::getDisparityVis(filtered_disp,filtered_disp_vis,vis_mult); |
|
cv::namedWindow("filtered disparity", cv::WINDOW_AUTOSIZE); |
|
cv::imshow("filtered disparity", filtered_disp_vis); |
|
cv::waitKey(); |
|
//! [visualization] |
|
} |
|
|
|
return 0; |
|
} |
|
#endif |
|
/*----------------------------------------------------------------*/ |
|
|
|
|
|
// apply a gradient like filter |
|
void PrefilterXSobel(const cv::Mat& src, cv::Mat& dst, int ftzero=31) |
|
{ |
|
const int OFS = 256 * 4, TABSZ = OFS * 2 + 256; |
|
uint8_t tab[TABSZ]; |
|
for (int x = 0; x < TABSZ; x++) |
|
tab[x] = (uint8_t)(x - OFS < -ftzero ? 0 : x - OFS > ftzero ? ftzero * 2 : x - OFS + ftzero); |
|
uint8_t val0 = tab[0 + OFS]; |
|
|
|
#ifdef _USE_SSE |
|
volatile bool useSIMD = cv::checkHardwareSupport(CV_CPU_SSE2); |
|
#endif |
|
|
|
ASSERT(src.type() == CV_8U); |
|
const cv::Size size(src.size()); |
|
dst.create(size, src.type()); |
|
int y; |
|
for (y = 0; y < size.height - 1; y += 2) { |
|
const uint8_t* srow1 = src.ptr<uint8_t>(y); |
|
const uint8_t* srow0 = y > 0 ? srow1 - src.step : size.height > 1 ? srow1 + src.step : srow1; |
|
const uint8_t* srow2 = y < size.height - 1 ? srow1 + src.step : size.height > 1 ? srow1 - src.step : srow1; |
|
const uint8_t* srow3 = y < size.height - 2 ? srow1 + src.step * 2 : srow1; |
|
uint8_t* dptr0 = dst.ptr<uint8_t>(y); |
|
uint8_t* dptr1 = dptr0 + dst.step; |
|
|
|
dptr0[0] = dptr0[size.width - 1] = dptr1[0] = dptr1[size.width - 1] = val0; |
|
int x = 1; |
|
|
|
#ifdef _USE_SSE |
|
if (useSIMD) { |
|
__m128i z = _mm_setzero_si128(), ftz = _mm_set1_epi16((short)ftzero); |
|
__m128i ftz2 = _mm_set1_epi8(cv::saturate_cast<uint8_t>(ftzero * 2)); |
|
for (; x <= size.width - 9; x += 8) { |
|
__m128i c0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow0 + x - 1)), z); |
|
__m128i c1 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow1 + x - 1)), z); |
|
__m128i d0 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow0 + x + 1)), z); |
|
__m128i d1 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow1 + x + 1)), z); |
|
|
|
d0 = _mm_sub_epi16(d0, c0); |
|
d1 = _mm_sub_epi16(d1, c1); |
|
|
|
__m128i c2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow2 + x - 1)), z); |
|
__m128i c3 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow3 + x - 1)), z); |
|
__m128i d2 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow2 + x + 1)), z); |
|
__m128i d3 = _mm_unpacklo_epi8(_mm_loadl_epi64((__m128i*)(srow3 + x + 1)), z); |
|
|
|
d2 = _mm_sub_epi16(d2, c2); |
|
d3 = _mm_sub_epi16(d3, c3); |
|
|
|
__m128i v0 = _mm_add_epi16(d0, _mm_add_epi16(d2, _mm_add_epi16(d1, d1))); |
|
__m128i v1 = _mm_add_epi16(d1, _mm_add_epi16(d3, _mm_add_epi16(d2, d2))); |
|
v0 = _mm_packus_epi16(_mm_add_epi16(v0, ftz), _mm_add_epi16(v1, ftz)); |
|
v0 = _mm_min_epu8(v0, ftz2); |
|
|
|
_mm_storel_epi64((__m128i*)(dptr0 + x), v0); |
|
_mm_storel_epi64((__m128i*)(dptr1 + x), _mm_unpackhi_epi64(v0, v0)); |
|
} |
|
} |
|
#endif |
|
|
|
for (; x < size.width - 1; x++) { |
|
int d0 = srow0[x + 1] - srow0[x - 1], d1 = srow1[x + 1] - srow1[x - 1], |
|
d2 = srow2[x + 1] - srow2[x - 1], d3 = srow3[x + 1] - srow3[x - 1]; |
|
int v0 = tab[d0 + d1 * 2 + d2 + OFS]; |
|
int v1 = tab[d1 + d2 * 2 + d3 + OFS]; |
|
dptr0[x] = (uint8_t)v0; |
|
dptr1[x] = (uint8_t)v1; |
|
} |
|
} |
|
|
|
for (; y < size.height; y++) { |
|
uint8_t* dptr = dst.ptr<uint8_t>(y); |
|
for (int x = 0; x < size.width; x++) |
|
dptr[x] = val0; |
|
} |
|
} |
|
/*----------------------------------------------------------------*/ |
|
|
|
|
|
|
|
// S T R U C T S /////////////////////////////////////////////////// |
|
|
|
enum EVENT_TYPE { |
|
EVT_JOB = 0, |
|
EVT_CLOSE, |
|
}; |
|
|
|
class EVTClose : public Event |
|
{ |
|
public: |
|
EVTClose() : Event(EVT_CLOSE) {} |
|
}; |
|
class EVTPixelProcess : public Event |
|
{ |
|
public: |
|
typedef std::function<void (int,int,int)> FncPixel; |
|
const cv::Size size; |
|
volatile Thread::safe_t& idxPixel; |
|
const FncPixel fncPixel; |
|
bool Run(void*) override { |
|
const int numPixels(size.area()); |
|
int idx; |
|
while ((idx=(int)Thread::safeInc(idxPixel)) < numPixels) |
|
fncPixel(idx, idx/size.width, idx%size.width); |
|
return true; |
|
} |
|
EVTPixelProcess(cv::Size s, volatile Thread::safe_t& idx, FncPixel f) : Event(EVT_JOB), size(s), idxPixel(idx), fncPixel(f) {} |
|
}; |
|
class EVTPixelAccumInc : public Event |
|
{ |
|
public: |
|
typedef std::function<void (int)> FncPixel; |
|
const int numPixels; |
|
volatile Thread::safe_t& idxPixel; |
|
const FncPixel fncPixel; |
|
bool Run(void*) override { |
|
int idx; |
|
while ((idx=(int)Thread::safeInc(idxPixel)) < numPixels) |
|
fncPixel(idx); |
|
return true; |
|
} |
|
EVTPixelAccumInc(int s, volatile Thread::safe_t& idx, FncPixel f) : Event(EVT_JOB), numPixels(s), idxPixel(idx), fncPixel(f) {} |
|
}; |
|
class EVTPixelAccumDec : public Event |
|
{ |
|
public: |
|
typedef std::function<void (int)> FncPixel; |
|
volatile Thread::safe_t& idxPixel; |
|
const FncPixel fncPixel; |
|
bool Run(void*) override { |
|
int idx; |
|
while ((idx=(int)Thread::safeDec(idxPixel)) >= 0) |
|
fncPixel(idx); |
|
return true; |
|
} |
|
EVTPixelAccumDec(volatile Thread::safe_t& idx, FncPixel f) : Event(EVT_JOB), idxPixel(idx), fncPixel(f) {} |
|
}; |
|
/*----------------------------------------------------------------*/ |
|
|
|
|
|
|
|
// S T R U C T S /////////////////////////////////////////////////// |
|
|
|
// - P1 and P2s are algorithm constants very similar to those from the original SGM algorithm; |
|
// they are set to defaults according to the patch size |
|
// - alpha & beta form the final P2 as P2*(1+alpha*e^(-DI^2/(2*beta^2))) |
|
// where DI is the difference in image intensity I(x)-I(x_prev) in [0,255] range |
|
// - subpixelSteps represents how much sub-pixel accuracy is searched/stored; |
|
// if 1 no sub-pixel precision, if for example 4 a 0.25 sub-pixel accuracy is stored; |
|
// the stored value is quantized and represented as integer: val=(float)valStored/subpixelSteps |
|
SemiGlobalMatcher::SemiGlobalMatcher(SgmSubpixelMode _subpixelMode, Disparity _subpixelSteps, AccumCost _P1, AccumCost P2, float P2alpha, float P2beta) |
|
: |
|
subpixelMode(_subpixelMode), |
|
subpixelSteps(_subpixelSteps), |
|
P1(_P1), P2s(GenerateP2s(P2, P2alpha, P2beta)) |
|
{ |
|
} |
|
|
|
SemiGlobalMatcher::~SemiGlobalMatcher() |
|
{ |
|
} |
|
|
|
CLISTDEF0IDX(SemiGlobalMatcher::AccumCost,int) SemiGlobalMatcher::GenerateP2s(AccumCost P2, float P2alpha, float P2beta) |
|
{ |
|
CLISTDEF0IDX(AccumCost,int) P2s(256); |
|
FOREACH(i, P2s) |
|
P2s[i] = (AccumCost)ROUND2INT(P2*(1.f+P2alpha*EXP(-SQUARE((float)i)/(2.f*SQUARE(P2beta))))); |
|
return P2s; |
|
} |
|
|
|
|
|
// Compute SGM stereo for this image and each of the neighbor views: |
|
// - minResolution is the resolution of the top of the pyramid for tSGM; |
|
// can be 0 to force the standard SGM algorithm |
|
void SemiGlobalMatcher::Match(const Scene& scene, IIndex idxImage, IIndex numNeighbors, unsigned minResolution) |
|
{ |
|
const Image& leftImage = scene.images[idxImage]; |
|
const float fMinScore(MAXF(leftImage.neighbors.front().score*OPTDENSE::fViewMinScoreRatio, OPTDENSE::fViewMinScore)); |
|
FOREACH(idxNeighbor, leftImage.neighbors) { |
|
const ViewScore& neighbor = leftImage.neighbors[idxNeighbor]; |
|
// exclude neighbors that over the limit or too small score |
|
ASSERT(scene.images[neighbor.ID].IsValid()); |
|
if ((numNeighbors && idxNeighbor >= numNeighbors) || |
|
(neighbor.score < fMinScore)) |
|
break; |
|
// check if the disparity-map was already estimated for the same image pairs |
|
const Image& rightImage = scene.images[neighbor.ID]; |
|
const String pairName(MAKE_PATH(String::FormatString("%04u_%04u", leftImage.ID, rightImage.ID))); |
|
if (File::isPresent((pairName+".dimap").c_str()) || File::isPresent(MAKE_PATH(String::FormatString("%04u_%04u.dimap", rightImage.ID, leftImage.ID)))) |
|
continue; |
|
TD_TIMER_STARTD(); |
|
IndexArr points; |
|
Matrix3x3 H; Matrix4x4 Q; |
|
ViewData leftData, rightData; |
|
MaskMap leftMaskMap, rightMaskMap; { |
|
// fetch pairs of corresponding image points |
|
//TODO: use precomputed points from SelectViews() |
|
Point3fArr leftPoints, rightPoints; |
|
FOREACH(idxPoint, scene.pointcloud.points) { |
|
const PointCloud::ViewArr& views = scene.pointcloud.pointViews[idxPoint]; |
|
if (views.FindFirst(idxImage) != PointCloud::ViewArr::NO_INDEX) { |
|
points.push_back((uint32_t)idxPoint); |
|
if (views.FindFirst(neighbor.ID) != PointCloud::ViewArr::NO_INDEX) { |
|
const Point3 X(scene.pointcloud.points[idxPoint]); |
|
leftPoints.emplace_back(leftImage.camera.TransformPointW2I3(X)); |
|
rightPoints.emplace_back(rightImage.camera.TransformPointW2I3(X)); |
|
} |
|
} |
|
} |
|
// stereo-rectify image pair |
|
if (!Image::StereoRectifyImages(leftImage, rightImage, leftPoints, rightPoints, leftData.imageColor, rightData.imageColor, leftMaskMap, rightMaskMap, H, Q)) |
|
continue; |
|
ASSERT(leftData.imageColor.size() == rightData.imageColor.size()); |
|
} |
|
#ifdef _USE_FILTER_DEMO |
|
// run openCV implementation |
|
const LPCSTR argv[] = {"disparityFiltering", "-algorithm=sgbm", "-max_disparity=160", "-no-downscale"}; |
|
disparityFiltering(leftData.imageColor, rightData.imageColor, (int)SizeOfArray(argv), argv); |
|
#endif |
|
// color to gray conversion |
|
#if SGM_SIMILARITY == SGM_SIMILARITY_CENSUS |
|
leftData.imageColor.toGray(leftData.imageGray, cv::COLOR_BGR2GRAY, false, true); |
|
rightData.imageColor.toGray(rightData.imageGray, cv::COLOR_BGR2GRAY, false, true); |
|
#else |
|
leftData.imageColor.toGray(leftData.imageGray, cv::COLOR_BGR2GRAY, true, true); |
|
rightData.imageColor.toGray(rightData.imageGray, cv::COLOR_BGR2GRAY, true, true); |
|
#endif |
|
// compute scale used for the disparity estimation |
|
REAL scale(1); |
|
if (minResolution) { |
|
unsigned resolutionLevel(8); |
|
Image8U::computeMaxResolution(leftData.imageGray.width(), leftData.imageGray.height(), resolutionLevel, minResolution); |
|
scale = REAL(1)/MAXF(2,POWI(2,resolutionLevel)); |
|
} |
|
const bool tSGM(!ISEQUAL(scale, REAL(1))); |
|
DisparityMap leftDisparityMap, rightDisparityMap; AccumCostMap costMap; |
|
do { |
|
#if 0 |
|
// export the intermediate disparity-maps |
|
if (!leftDisparityMap.empty()) { |
|
const REAL _scale(scale*0.5); |
|
Matrix3x3 _H(H); Matrix4x4 _Q(Q); |
|
Image::ScaleStereoRectification(_H, _Q, _scale); |
|
ExportDisparityDataRawFull(String::FormatString("%s_%d.dimap", pairName.c_str(), LOG2I(ROUND2INT<unsigned>(REAL(1)/_scale))), leftDisparityMap, costMap, Image8U::computeResize(leftImage.GetSize(), _scale), H, _Q, 1); |
|
} |
|
#endif |
|
// initialize |
|
const ViewData leftDataLevel(leftData.GetImage(scale)); |
|
const ViewData rightDataLevel(rightData.GetImage(scale)); |
|
const cv::Size size(leftDataLevel.imageGray.size()); |
|
const cv::Size sizeValid(size.width-2*halfWindowSizeX, size.height-2*halfWindowSizeY); |
|
const bool bFirstLevel(leftDisparityMap.empty()); |
|
if (bFirstLevel) { |
|
// initialize the disparity-map with a rough estimate based on the sparse point-cloud |
|
//TODO: remove DepthData::ViewData dependency |
|
Image leftImageLevel(leftImage.GetImage(scene.platforms, scale*0.5, false)); |
|
DepthData::ViewData image; |
|
image.pImageData = &leftImageLevel; // used only for avgDepth |
|
image.image.create(leftImageLevel.GetSize()); |
|
image.camera = leftImageLevel.camera; |
|
DepthMap depthMap; |
|
Depth dMin, dMax; |
|
TriangulatePoints2DepthMap(image, scene.pointcloud, points, depthMap, dMin, dMax, true); |
|
points.Release(); |
|
Matrix3x3 H2(H); Matrix4x4 Q2(Q); |
|
Image::ScaleStereoRectification(H2, Q2, scale*0.5); |
|
const cv::Size sizeHalf(Image8U::computeResize(size, 0.5)); |
|
const cv::Size sizeValidHalf(sizeHalf.width-2*halfWindowSizeX, sizeHalf.height-2*halfWindowSizeY); |
|
leftDisparityMap.create(sizeValidHalf); |
|
Depth2DisparityMap(depthMap, H2.inv(), Q2.inv(), 1, leftDisparityMap); |
|
// resize masks |
|
cv::resize(leftMaskMap, leftMaskMap, size, 0, 0, cv::INTER_NEAREST); |
|
cv::resize(rightMaskMap, rightMaskMap, size, 0, 0, cv::INTER_NEAREST); |
|
const cv::Rect ROI(halfWindowSizeX,halfWindowSizeY, sizeValid.width,sizeValid.height); |
|
leftMaskMap(ROI).copyTo(leftMaskMap); |
|
rightMaskMap(ROI).copyTo(rightMaskMap); |
|
} else { |
|
// upscale masks |
|
UpscaleMask(leftMaskMap, sizeValid); |
|
UpscaleMask(rightMaskMap, sizeValid); |
|
} |
|
// estimate right-left disparity-map |
|
Index numCosts; |
|
if (tSGM) { |
|
// upscale the disparity-map from the previous level |
|
FlipDirection(leftDisparityMap, rightDisparityMap); |
|
numCosts = Disparity2RangeMap(rightDisparityMap, rightMaskMap, bFirstLevel?11:5, bFirstLevel?33:7); |
|
} else { |
|
// extract global min and max disparities |
|
Range range{std::numeric_limits<Disparity>::max(), std::numeric_limits<Disparity>::min()}; |
|
ASSERT(leftDisparityMap.isContinuous()); |
|
const Disparity* pd = leftDisparityMap.ptr<const Disparity>(); |
|
const Disparity* const pde = pd+leftDisparityMap.area(); |
|
do { |
|
const Disparity d(*pd); |
|
if (range.minDisp > d) |
|
range.minDisp = d; |
|
if (range.maxDisp < d) |
|
range.maxDisp = d; |
|
} while (++pd < pde); |
|
// set disparity search range to the global min/max range |
|
const Disparity numDisp(range.numDisp()+16); |
|
const Disparity disp(range.minDisp+range.maxDisp); |
|
range.minDisp = disp-numDisp; |
|
range.maxDisp = disp+numDisp; |
|
maxNumDisp = range.numDisp(); |
|
numCosts = 0; |
|
imagePixels.resize(sizeValid.area()); |
|
for (PixelData& pixel: imagePixels) { |
|
pixel.range = range; |
|
pixel.idx = numCosts; |
|
numCosts += maxNumDisp; |
|
} |
|
} |
|
imageCosts.resize(numCosts); |
|
imageAccumCosts.resize(numCosts); |
|
Match(rightDataLevel, leftDataLevel, rightDisparityMap, costMap); |
|
// estimate left-right disparity-map |
|
if (tSGM) { |
|
numCosts = Disparity2RangeMap(leftDisparityMap, leftMaskMap, bFirstLevel?11:5, bFirstLevel?33:7); |
|
imageCosts.resize(numCosts); |
|
imageAccumCosts.resize(numCosts); |
|
} else { |
|
for (PixelData& pixel: imagePixels) { |
|
const Disparity maxDisp(-pixel.range.minDisp); |
|
pixel.range.minDisp = -pixel.range.maxDisp; |
|
pixel.range.maxDisp = maxDisp; |
|
} |
|
} |
|
Match(leftDataLevel, rightDataLevel, leftDisparityMap, costMap); |
|
// check disparity-map cross-consistency |
|
#if 0 |
|
if (ISEQUAL(scale, REAL(1))) { |
|
cv::Ptr<cv::ximgproc::DisparityWLSFilter> filter = cv::ximgproc::createDisparityWLSFilterGeneric(true); |
|
const cv::Rect rcValid(halfWindowSizeX,halfWindowSizeY, sizeValid.width,sizeValid.height); |
|
Image32F leftDisparityMap32F, rightDisparityMap32F, filtered32F; |
|
leftDisparityMap.convertTo(leftDisparityMap32F, CV_32F, 16); |
|
rightDisparityMap.convertTo(rightDisparityMap32F, CV_32F, 16); |
|
filter->filter(leftDisparityMap32F, leftData.imageColor(rcValid), filtered32F, rightDisparityMap32F); |
|
filtered32F.convertTo(leftDisparityMap, CV_16S, 1.0/16, 0.5); |
|
} else |
|
#endif |
|
if (bFirstLevel) { |
|
// perform a rigorous filtering of the estimated disparity maps in order to |
|
// estimate the common region or interest and set the validity masks |
|
ConsistencyCrossCheck(leftDisparityMap, rightDisparityMap); |
|
ConsistencyCrossCheck(rightDisparityMap, leftDisparityMap); |
|
cv::filterSpeckles(leftDisparityMap, NO_DISP, OPTDENSE::nSpeckleSize, 5); |
|
cv::filterSpeckles(rightDisparityMap, NO_DISP, OPTDENSE::nSpeckleSize, 5); |
|
ExtractMask(leftDisparityMap, leftMaskMap); |
|
ExtractMask(rightDisparityMap, rightMaskMap); |
|
} else { |
|
// simply run a left-right consistency check |
|
ConsistencyCrossCheck(leftDisparityMap, rightDisparityMap); |
|
} |
|
} while ((scale*=2) < REAL(1)+ZEROTOLERANCE<REAL>()); |
|
#if 0 |
|
// remove speckles |
|
if (OPTDENSE::nSpeckleSize > 0) |
|
cv::filterSpeckles(leftDisparityMap, NO_DISP, OPTDENSE::nSpeckleSize, 5); |
|
#endif |
|
// sub-pixel disparity-map estimation |
|
RefineDisparityMap(leftDisparityMap); |
|
#if 1 |
|
// export disparity-map for the left image |
|
DEBUG_EXTRA("Disparity-map for images %3u and %3u: %dx%d (%s)", leftImage.ID, rightImage.ID, |
|
leftImage.width, leftImage.height, TD_TIMER_GET_FMT().c_str()); |
|
ExportPointCloud(pairName+".ply", leftImage, leftDisparityMap, Q, subpixelSteps); |
|
ExportDisparityMap(pairName+".png", leftDisparityMap); |
|
ExportDisparityDataRawFull(pairName+".dimap", leftDisparityMap, costMap, leftImage.GetSize(), H, Q, subpixelSteps); |
|
#else |
|
// convert disparity-map to final depth-map for the left image |
|
DepthMap depthMap(leftImage.image.size()); ConfidenceMap confMap; |
|
Disparity2DepthMap(leftDisparityMap, costMap, H, Q, subpixelSteps, depthMap, confMap); |
|
DEBUG_EXTRA("Depth-map for images %3u and %3u: %dx%d (%s)", leftImage.ID, rightImage.ID, |
|
depthMap.width(), depthMap.height(), TD_TIMER_GET_FMT().c_str()); |
|
ExportDepthMap(pairName+".png", depthMap); |
|
MVS::ExportPointCloud(pairName+".ply", leftImage, depthMap, NormalMap()); |
|
ExportDepthDataRaw(pairName+".dmap", leftImage.name, IIndexArr{leftImage.ID,rightImage.ID}, leftImage.GetSize(), leftImage.camera.K, leftImage.camera.R, leftImage.camera.C, 0, FLT_MAX, depthMap, NormalMap(), confMap); |
|
#endif |
|
} |
|
} |
|
|
|
void SemiGlobalMatcher::Fuse(const Scene& scene, IIndex idxImage, IIndex numNeighbors, unsigned minViews, DepthMap& depthMap, ConfidenceMap& confMap) |
|
{ |
|
TD_TIMER_STARTD(); |
|
const Image& leftImage = scene.images[idxImage]; |
|
const float fMinScore(MAXF(leftImage.neighbors.front().score*OPTDENSE::fViewMinScoreRatio, OPTDENSE::fViewMinScore)); |
|
struct PairData { |
|
DepthMap depthMap; |
|
DepthRangeMap depthRangeMap; |
|
ConfidenceMap confMap; |
|
PairData(const cv::Size& size) : depthMap(size) {} |
|
}; |
|
// load and put in same space all disparity-maps containing this view |
|
CLISTDEFIDX(PairData,IIndex) pairs; |
|
pairs.reserve(numNeighbors); |
|
FOREACH(idxNeighbor, leftImage.neighbors) { |
|
const ViewScore& neighbor = leftImage.neighbors[idxNeighbor]; |
|
// exclude neighbors that over the limit or too small score |
|
ASSERT(scene.images[neighbor.ID].IsValid()); |
|
if ((numNeighbors && idxNeighbor >= numNeighbors) || |
|
(neighbor.score < fMinScore)) |
|
break; |
|
// check if the disparity-map was estimated for this images pair |
|
const Image& rightImage = scene.images[neighbor.ID]; |
|
Disparity subpixelSteps; |
|
cv::Size imageSize; Matrix3x3 H; Matrix4x4 Q; |
|
DisparityMap disparityMap; AccumCostMap costMap; |
|
if (!ImportDisparityDataRawFull(MAKE_PATH(String::FormatString("%04u_%04u.dimap", leftImage.ID, rightImage.ID)), disparityMap, costMap, imageSize, H, Q, subpixelSteps)) { |
|
if (!ImportDisparityDataRawFull(MAKE_PATH(String::FormatString("%04u_%04u.dimap", rightImage.ID, leftImage.ID)), disparityMap, costMap, imageSize, H, Q, subpixelSteps)) { |
|
DEBUG("warning: no disparity-data file found for image pair (%u,%u)", leftImage.ID, rightImage.ID); |
|
continue; |
|
} |
|
// adapt Q to project the 3D point from right into the left-image |
|
RMatrix poseR; |
|
CMatrix poseC; |
|
ComputeRelativePose(rightImage.camera.R, rightImage.camera.C, leftImage.camera.R, leftImage.camera.C, poseR, poseC); |
|
Matrix4x4 P(Matrix4x4::IDENTITY); |
|
AssembleProjectionMatrix(leftImage.camera.K, poseR, poseC, reinterpret_cast<Matrix3x4&>(P)); |
|
Matrix4x4 invK(Matrix4x4::IDENTITY); |
|
cv::Mat(rightImage.camera.GetInvK()).copyTo(cv::Mat(4,4,cv::DataType<Matrix4x4::Type>::type,invK.val)(cv::Rect(0,0,3,3))); |
|
Q = P*invK*Q; |
|
} |
|
// convert disparity-map to final depth-map for the left image |
|
PairData& pair = pairs.emplace_back(leftImage.image.size()); |
|
if (!ProjectDisparity2DepthMap(disparityMap, costMap, Q, subpixelSteps, pair.depthMap, pair.depthRangeMap, pair.confMap)) { |
|
pairs.RemoveLast(); |
|
continue; |
|
} |
|
#if TD_VERBOSE != TD_VERBOSE_OFF |
|
// save pair depth-map |
|
if (VERBOSITY_LEVEL > 3) { |
|
const String pairName(MAKE_PATH(String::FormatString("depth%04u_%04u", leftImage.ID, rightImage.ID))); |
|
ExportDepthMap(pairName+".png", pair.depthMap); |
|
MVS::ExportPointCloud(pairName+".ply", leftImage, pair.depthMap, NormalMap()); |
|
} |
|
#endif |
|
} |
|
// fuse available depth-maps such that for each pixel set its depth as the average of the largest cluster of agreeing depths; |
|
// pixel depths values agree if their trust regions overlap |
|
depthMap.create(leftImage.image.size()); confMap.create(depthMap.size()); |
|
for (int r=0; r<depthMap.rows; ++r) { |
|
for (int c=0; c<depthMap.cols; ++c) { |
|
struct Cluster { |
|
IIndexArr views; |
|
DepthRange range; |
|
}; |
|
CLISTDEFIDX(Cluster,IIndex) clusters(0, pairs.size()); |
|
FOREACH(p, pairs) { |
|
const PairData& pair = pairs[p]; |
|
const Depth depth(pair.depthMap(r,c)); |
|
if (depth <= 0) |
|
continue; |
|
const DepthRange& range(pair.depthRangeMap(r,c)); |
|
unsigned numClusters(0); |
|
for (Cluster& cluster: clusters) { |
|
if (!ISINSIDE(depth, cluster.range.x, cluster.range.y)) |
|
continue; |
|
cluster.views.push_back(p); |
|
if (cluster.range.x < range.x) |
|
cluster.range.x = range.x; |
|
if (cluster.range.y > range.y) |
|
cluster.range.y = range.y; |
|
++numClusters; |
|
} |
|
if (numClusters == 0) |
|
clusters.emplace_back(Cluster{IIndexArr{p}, range}); |
|
} |
|
if (clusters.empty()) { |
|
depthMap(r,c) = Depth(0); |
|
confMap(r,c) = float(0); |
|
continue; |
|
} |
|
const Cluster& cluster = clusters.GetMax([](const Cluster& i, const Cluster& j) { return i.views.size() < j.views.size(); }); |
|
if (cluster.views.size() < minViews) { |
|
depthMap(r,c) = Depth(0); |
|
confMap(r,c) = float(0); |
|
continue; |
|
} |
|
Depth& depth = depthMap(r,c); depth = 0; |
|
float& conf = confMap(r,c); conf = 0; |
|
unsigned numDepths(0); |
|
for (IIndex p: cluster.views) { |
|
const PairData& pair = pairs[p]; |
|
depth += pair.depthMap(r,c); |
|
conf += pair.confMap(r,c); |
|
++numDepths; |
|
} |
|
depth /= numDepths; |
|
conf /= numDepths; |
|
} |
|
} |
|
DEBUG_EXTRA("Depth-map for image %3u fused: %dx%d (%s)", leftImage.ID, |
|
depthMap.width(), depthMap.height(), TD_TIMER_GET_FMT().c_str()); |
|
#if TD_VERBOSE != TD_VERBOSE_OFF |
|
// save depth-map |
|
if (VERBOSITY_LEVEL > 2) { |
|
const String fileName(MAKE_PATH(String::FormatString("depth%04u", leftImage.ID))); |
|
ExportDepthMap(fileName+".png", depthMap); |
|
MVS::ExportPointCloud(fileName+".ply", leftImage, depthMap, NormalMap()); |
|
} |
|
#endif |
|
} |
|
|
|
|
|
// Compute SGM stereo on the images |
|
void SemiGlobalMatcher::Match(const ViewData& leftImage, const ViewData& rightImage, DisparityMap& disparityMap, AccumCostMap& costMap) |
|
{ |
|
const cv::Size size(leftImage.imageGray.size()); |
|
const cv::Size sizeValid(size.width-2*halfWindowSizeX, size.height-2*halfWindowSizeY); |
|
ASSERT(leftImage.imageColor.size() == size); |
|
|
|
#if 0 && !defined(_RELEASE) |
|
// display search info (average disparity and range) |
|
DisplayState(sizeValid); |
|
#endif |
|
|
|
// compute costs |
|
{ |
|
ASSERT(!imageCosts.empty()); |
|
const float eps(1e-3f); // used suppress the effect of noise in untextured regions |
|
auto pixel = [&](int idx, int r, int c) { |
|
// ignore pixel if not valid |
|
const PixelData& pixel = imagePixels[idx]; |
|
if (!pixel.range.isValid()) |
|
return; |
|
#if SGM_SIMILARITY == SGM_SIMILARITY_CENSUS |
|
struct Compute { |
|
static inline int HammingDistance(uint64_t c1, uint64_t c2) { |
|
return PopCnt(c1 ^ c2); |
|
} |
|
}; |
|
// compute pixel cost |
|
Cost* costs = imageCosts.data()+pixel.idx; |
|
const Census lc(leftImage.imageCensus(r,c)); |
|
for (int d=pixel.range.minDisp; d<pixel.range.maxDisp; ++d) { |
|
const ImageRef x(c+d,r); |
|
if (!rightImage.imageCensus.isInside(x)) { |
|
*costs++ = 255; |
|
continue; |
|
} |
|
const Census rc(rightImage.imageCensus(x)); |
|
*costs++ = Compute::HammingDistance(lc, rc)*4; |
|
} |
|
#else |
|
struct Compute { |
|
static float NormL1Sq(const Pixel8U& a, const Pixel8U& b) { |
|
// |(a-b)| L1 norm of (a-b) when types are byte |
|
return float( |
|
SQUARE(unsigned(a[0]<b[0] ? b[0]-a[0] : a[0]-b[0])) + |
|
SQUARE(unsigned(a[1]<b[1] ? b[1]-a[1] : a[1]-b[1])) + |
|
SQUARE(unsigned(a[2]<b[2] ? b[2]-a[2] : a[2]-b[2]))); |
|
} |
|
static float WeightColor(const Image8U3& image, const Pixel8U& center, const ImageRef& x) { |
|
// color weight [0..1] |
|
static const float sigmaColor(-1.f/(2.f*SQUARE(0.3f*255))); |
|
return Compute::NormL1Sq(image(x), center) * sigmaColor; |
|
} |
|
static float WeightSpatial(int x, int y) { |
|
// spatial weight [0..1] |
|
static const float sigmaSpatial(-1.f/(2.f*SQUARE(0.4f*MAXF<int>(windowSizeX,windowSizeY)))); |
|
return float(SQUARE(x) + SQUARE(y)) * sigmaSpatial; |
|
} |
|
}; |
|
const ImageRef u(c+halfWindowSizeX,r+halfWindowSizeY); |
|
// initialize pixel patch weights |
|
WeightedPatch w; |
|
w.normSq0 = 0; |
|
w.sumWeights = 0; |
|
int n = 0; |
|
const Pixel8U& colCenter = leftImage.imageColor(u); |
|
for (int i=-halfWindowSizeY; i<=halfWindowSizeY; ++i) { |
|
for (int j=-halfWindowSizeX; j<=halfWindowSizeX; ++j) { |
|
const ImageRef x(u.x+j,u.y+i); |
|
WeightedPatch::Pixel& pw = w.weights[n++]; |
|
w.normSq0 += |
|
(pw.tempWeight = leftImage.imageGray(x)) * |
|
(pw.weight = EXP(Compute::WeightColor(leftImage.imageColor, colCenter, x)+Compute::WeightSpatial(j,i))); |
|
w.sumWeights += pw.weight; |
|
} |
|
} |
|
ASSERT(n == numTexels); |
|
const float tm(w.normSq0/w.sumWeights); |
|
w.normSq0 = 0; |
|
n = 0; |
|
do { |
|
WeightedPatch::Pixel& pw = w.weights[n]; |
|
const float t(pw.tempWeight - tm); |
|
w.normSq0 += (pw.tempWeight = pw.weight * t) * t; |
|
} while (++n < numTexels); |
|
// compute pixel cost |
|
Cost* costs = imageCosts.data()+pixel.idx; |
|
for (int d=pixel.range.minDisp; d<pixel.range.maxDisp; ++d) { |
|
float sum(0), sumSq(0), nom(0); |
|
for (int i=-halfWindowSizeY, n=0; i<=halfWindowSizeY; ++i) { |
|
for (int j=-halfWindowSizeX; j<=halfWindowSizeX; ++j) { |
|
const ImageRef x(u.x+j+d,u.y+i); |
|
if (!rightImage.imageGray.isInside(x)) { |
|
*costs++ = 255; |
|
goto NEXT_COST; |
|
} |
|
const float f(rightImage.imageGray(x)); |
|
const WeightedPatch::Pixel& pw = w.weights[n++]; |
|
const float fw(f*pw.weight); |
|
sum += fw; |
|
sumSq += f*fw; |
|
nom += f*pw.tempWeight; |
|
} |
|
} |
|
{ |
|
const float normSq1(sumSq-SQUARE(sum)/w.sumWeights); |
|
const float ncc(nom/SQRT(w.normSq0*normSq1+eps)); |
|
*costs++ = (ncc <= 0 ? Cost(255) : (Cost)ROUND2INT((1.f-MINF(ncc,1.f))*255.f)); |
|
} |
|
NEXT_COST:; |
|
} |
|
#endif |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(sizeValid, idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<sizeValid.height; ++r) |
|
for (int c=0; c<sizeValid.width; ++c) |
|
pixel(r*sizeValid.width+c, r, c); |
|
} |
|
|
|
// accumulate costs |
|
{ |
|
ASSERT(!imageAccumCosts.empty()); |
|
imageAccumCosts.Memset(0); |
|
#if SGM_SIMILARITY == SGM_SIMILARITY_CENSUS |
|
const ImageGray::Type Igray(127); |
|
#else |
|
const ImageGray::Type Igray(0.5f); |
|
#endif |
|
struct LineData { |
|
AccumCost* L; |
|
Range R; |
|
~LineData() { delete[] L; } |
|
AccumCost operator[] (int i) const { return L[i]; } |
|
AccumCost& operator[] (int i) { return L[i]; } |
|
}; |
|
auto pixelAccum = [&](const Cost* costs, const LineData& Lp, LineData& Ls, AccumCost* accums, ImageGray::Type DI) { |
|
struct Compute { |
|
static inline void MINS(AccumCost& m, AccumCost v) { if (m > v) m = v; } |
|
}; |
|
ASSERT(Ls.R.isValid()); |
|
#if SGM_SIMILARITY == SGM_SIMILARITY_CENSUS |
|
const AccumCost P2(P2s[DI]); |
|
#else |
|
const AccumCost P2(P2s[ABS(ROUND2INT(255.f*DI))]); |
|
#endif |
|
const Disparity minDisp(MAXF(Lp.R.minDisp, Ls.R.minDisp)); |
|
const Disparity maxDisp(MINF(Lp.R.maxDisp, Ls.R.maxDisp)); |
|
if (minDisp >= maxDisp) { |
|
// the disparity ranges for the two pixels do not intersect; |
|
// fill all accumulated costs with L(d)=C(d)+P2 |
|
const Disparity numDisp(Ls.R.numDisp()); |
|
for (int idxDisp=0; idxDisp<numDisp; ++idxDisp) |
|
accums[idxDisp] += (Ls[idxDisp] = costs[idxDisp]+P2); |
|
} else { |
|
// accumulate cost as L(d)=C(d)+min(Lp(d)+V(d,dp))-min(Lp) |
|
// where V(d,dp) is: |
|
// 0 if d=dp |
|
// P1 if |d-dp|=1 |
|
// P2 if |d-dp|>1 |
|
AccumCost minLp(std::numeric_limits<AccumCost>::max()); |
|
for (const AccumCost *L=Lp.L+(minDisp-Lp.R.minDisp), *endL=L+(maxDisp-minDisp); L<endL; ++L) |
|
Compute::MINS(minLp, *L); |
|
for (Disparity d=Ls.R.minDisp; d<Ls.R.maxDisp; ++d) { |
|
const int idxDisp(d-Ls.R.minDisp); |
|
AccumCost& L = Ls[idxDisp]; |
|
L = std::numeric_limits<AccumCost>::max(); |
|
for (Disparity dp=minDisp; dp<maxDisp; ++dp) { |
|
const int idxDispp(dp-Lp.R.minDisp); |
|
if (dp == d) |
|
Compute::MINS(L, Lp[idxDispp]); |
|
else if (dp == d-1 || dp == d+1) |
|
Compute::MINS(L, Lp[idxDispp]+P1); |
|
else |
|
Compute::MINS(L, Lp[idxDispp]+P2); |
|
} |
|
accums[idxDisp] += (L = costs[idxDisp]+L-minLp); |
|
} |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
struct AccumLines { |
|
LineData linesBuffer[2]; |
|
LineData* lines[2]; |
|
AccumLines(Disparity maxNumDisp) { |
|
for (LineData& line: linesBuffer) { |
|
line.L = new AccumCost[maxNumDisp]; |
|
memset(line.L, 0, sizeof(AccumCost)*maxNumDisp); |
|
line.R.minDisp = line.R.maxDisp = 0; |
|
} |
|
lines[0] = linesBuffer+0; |
|
lines[1] = linesBuffer+1; |
|
} |
|
void NextLine() { std::swap(lines[0], lines[1]); } |
|
const LineData& operator() (int i) const { return *lines[i]; } |
|
LineData& operator() (int i) { return *lines[i]; } |
|
}; |
|
#define ACCUM_PIXELS(cond) \ |
|
AccumLines lines(maxNumDisp); \ |
|
ImageGray::Type Ip(Igray); \ |
|
do { \ |
|
const int idx(u.y*sizeValid.width+u.x); \ |
|
const PixelData& pixel = imagePixels[idx]; \ |
|
if (!pixel.range.isValid()) \ |
|
continue; \ |
|
const Cost* costs = imageCosts.cdata()+pixel.idx; \ |
|
AccumCost* accums = imageAccumCosts.data()+pixel.idx; \ |
|
const LineData& Lp = lines(0); \ |
|
LineData& Ls = lines(1); \ |
|
Ls.R = pixel.range; \ |
|
const ImageGray::Type I(leftImage.imageGray(u)); \ |
|
pixelAccum(costs, Lp, Ls, accums, I-Ip); \ |
|
Ip = I; \ |
|
lines.NextLine(); \ |
|
} while (cond) |
|
{ // width-down |
|
auto pixels = [&](int x) { |
|
ImageRef u(x,0); |
|
ACCUM_PIXELS(++u.y < sizeValid.height); |
|
}; |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.width, idxPixel, pixels)); |
|
WaitThreadWorkers(threads.size()); |
|
} |
|
{ // height-right |
|
auto pixels = [&](int y) { |
|
ImageRef u(0,y); |
|
ACCUM_PIXELS(++u.x < sizeValid.width); |
|
}; |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.height, idxPixel, pixels)); |
|
WaitThreadWorkers(threads.size()); |
|
} |
|
{ // width-up |
|
auto pixels = [&](int x) { |
|
ImageRef u(x,sizeValid.height-1); |
|
ACCUM_PIXELS(--u.y >= 0); |
|
}; |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.width, idxPixel, pixels)); |
|
WaitThreadWorkers(threads.size()); |
|
} |
|
{ // height-left |
|
auto pixels = [&](int y) { |
|
ImageRef u(sizeValid.width-1,y); |
|
ACCUM_PIXELS(--u.x >= 0); |
|
}; |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.height, idxPixel, pixels)); |
|
WaitThreadWorkers(threads.size()); |
|
} |
|
if (numDirs == 4) { |
|
{ // width-right-down |
|
auto pixels = [&](int x) { |
|
ImageRef u(x,0); |
|
ACCUM_PIXELS(++u.x < sizeValid.width && ++u.y < sizeValid.height); |
|
}; |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.width, idxPixel, pixels)); |
|
} |
|
{ // height-right-down |
|
auto pixels = [&](int y) { |
|
ImageRef u(0,y); |
|
ACCUM_PIXELS(++u.x < sizeValid.width && ++u.y < sizeValid.height); |
|
}; |
|
volatile Thread::safe_t idxPixel(0); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.height, idxPixel, pixels)); |
|
} |
|
WaitThreadWorkers(threads.size()*2); |
|
{ // width-left-down |
|
auto pixels = [&](int x) { |
|
ImageRef u(x,0); |
|
ACCUM_PIXELS(--u.x >= 0 && ++u.y < sizeValid.height); |
|
}; |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.width-1, idxPixel, pixels)); |
|
} |
|
{ // height-left-down |
|
auto pixels = [&](int y) { |
|
ImageRef u(sizeValid.width-1,y); |
|
ACCUM_PIXELS(--u.x >= 0 && ++u.y < sizeValid.height); |
|
}; |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.height, idxPixel, pixels)); |
|
} |
|
WaitThreadWorkers(threads.size()*2); |
|
{ // width-right-up |
|
auto pixels = [&](int x) { |
|
ImageRef u(x,sizeValid.height-1); |
|
ACCUM_PIXELS(++u.x < sizeValid.width && --u.y >= 0); |
|
}; |
|
volatile Thread::safe_t idxPixel(0); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.width, idxPixel, pixels)); |
|
} |
|
{ // height-right-up |
|
auto pixels = [&](int y) { |
|
ImageRef u(0,y); |
|
ACCUM_PIXELS(++u.x < sizeValid.width && --u.y >= 0); |
|
}; |
|
volatile Thread::safe_t idxPixel(sizeValid.height); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumDec(idxPixel, pixels)); |
|
} |
|
WaitThreadWorkers(threads.size()*2); |
|
{ // width-left-up |
|
auto pixels = [&](int x) { |
|
ImageRef u(x,sizeValid.height-1); |
|
ACCUM_PIXELS(--u.x >= 0 && --u.y >= 0); |
|
}; |
|
volatile Thread::safe_t idxPixel(sizeValid.width); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumDec(idxPixel, pixels)); |
|
} |
|
{ // height-left-up |
|
auto pixels = [&](int y) { |
|
ImageRef u(sizeValid.width-1,y); |
|
ACCUM_PIXELS(--u.x >= 0 && --u.y >= 0); |
|
}; |
|
volatile Thread::safe_t idxPixel(sizeValid.height-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumDec(idxPixel, pixels)); |
|
} |
|
WaitThreadWorkers(threads.size()*2); |
|
} |
|
#undef ACCUM_PIXELS |
|
} else { |
|
const ImageRef dirs[] = {{-1,0}, {0,-1}, {-1,-1}, {1,-1}}; |
|
struct AccumLines { |
|
const Disparity maxNumDisp; |
|
LineData* linesBuffer; |
|
LineData* lines[numDirs][2]; |
|
AccumLines(Disparity _maxNumDisp) : maxNumDisp(_maxNumDisp), linesBuffer(NULL) {} |
|
~AccumLines() { delete[] linesBuffer; } |
|
void Init(int w) { |
|
const int linewidth(w+2); |
|
const int buffersize(2*linewidth*numDirs); |
|
if (linesBuffer == NULL) { |
|
linesBuffer = new LineData[buffersize]; |
|
for (int i=0; i<buffersize; ++i) |
|
linesBuffer[i].L = new AccumCost[maxNumDisp]; |
|
LineData* line(linesBuffer-linewidth+1); |
|
for (int idxDir=0; idxDir<numDirs; ++idxDir) { |
|
lines[idxDir][0] = (line+=linewidth); |
|
lines[idxDir][1] = (line+=linewidth); |
|
} |
|
} |
|
for (int i=0; i<buffersize; ++i) { |
|
LineData& line = linesBuffer[i]; |
|
memset(line.L, 0, sizeof(AccumCost)*maxNumDisp); |
|
line.R.minDisp = line.R.maxDisp = 0; |
|
} |
|
} |
|
void NextLine() { |
|
for (int idxDir=0; idxDir<numDirs; ++idxDir) |
|
std::swap(lines[idxDir][0], lines[idxDir][1]); |
|
} |
|
const LineData& operator() (int idxDir, int r, int c) const { return lines[idxDir][r][c]; } |
|
LineData& operator() (int idxDir, int r, int c) { return lines[idxDir][r][c]; } |
|
}; |
|
AccumLines lines(maxNumDisp); |
|
#define ACCUM_PIXELS(dx, dy, _x) \ |
|
const int idx(r*sizeValid.width+c); \ |
|
const PixelData& pixel = imagePixels[idx]; \ |
|
if (!pixel.range.isValid()) \ |
|
continue; \ |
|
const Cost* costs = imageCosts.cdata()+pixel.idx; \ |
|
AccumCost* accums = imageAccumCosts.data()+pixel.idx; \ |
|
for (int idxDir=0; idxDir<numDirs; ++idxDir) { \ |
|
const ImageRef& dir = dirs[idxDir]; \ |
|
const LineData& Lp = lines(idxDir,1+dir.y,_x+dir.x); \ |
|
LineData& Ls = lines(idxDir,1,_x); \ |
|
Ls.R = pixel.range; \ |
|
const ImageRef xp(c+dx, r+dy); \ |
|
const ImageGray::Type DI(leftImage.imageGray(r,c)-(leftImage.imageGray.isInside(xp)?leftImage.imageGray(xp):Igray)); \ |
|
pixelAccum(costs, Lp, Ls, accums, DI); \ |
|
} |
|
lines.Init(sizeValid.width); |
|
for (int r=0; r<sizeValid.height; ++r) { |
|
for (int c=0; c<sizeValid.width; ++c) { |
|
ACCUM_PIXELS(dir.x, dir.y, c); |
|
} |
|
lines.NextLine(); |
|
} |
|
lines.Init(sizeValid.width); |
|
for (int r=sizeValid.height; --r>=0; ) { |
|
for (int c=sizeValid.width; --c>=0; ) { |
|
ACCUM_PIXELS(-dir.x, -dir.y, sizeValid.width-1-c); |
|
} |
|
lines.NextLine(); |
|
} |
|
#undef ACCUM_PIXELS |
|
} |
|
} |
|
|
|
// select best disparity and cost |
|
{ |
|
disparityMap.create(sizeValid); |
|
costMap.create(sizeValid); |
|
auto pixel = [&](int idx) { |
|
const PixelData& pixel = imagePixels[idx]; |
|
if (pixel.range.isValid()) { |
|
const AccumCost* accums = imageAccumCosts.cdata()+pixel.idx; |
|
const AccumCost* bestAccum = accums; |
|
for (const AccumCost *accum=accums+1, *accumEnd=accums+pixel.range.numDisp(); accum<accumEnd; ++accum) { |
|
if (*bestAccum > *accum) |
|
bestAccum = accum; |
|
} |
|
disparityMap(idx) = pixel.range.minDisp+(Disparity)(bestAccum-accums); |
|
costMap(idx) = *bestAccum; |
|
} else { |
|
disparityMap(idx) = pixel.range.minDisp; |
|
costMap(idx) = NO_ACCUMCOST; |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(sizeValid.area(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<sizeValid.height; ++r) |
|
for (int c=0; c<sizeValid.width; ++c) |
|
pixel(r*sizeValid.width+c); |
|
} |
|
} |
|
|
|
#if SGM_SIMILARITY == SGM_SIMILARITY_CENSUS |
|
// Compute the census bit-mask for all the pixels of the image |
|
void SemiGlobalMatcher::CensusTransform(const Image8U& imageGray, CensusMap& imageCensus) |
|
{ |
|
ASSERT(!imageGray.empty()); |
|
const cv::Size size(imageGray.size()); |
|
const cv::Size sizeValid(size.width-2*halfWindowSizeX, size.height-2*halfWindowSizeY); |
|
imageCensus.create(sizeValid); |
|
|
|
#if 0 |
|
// pre-process input image to easy CENSUS task |
|
ImageGray image; |
|
PrefilterXSobel(imageGray, image); |
|
#else |
|
const Image8U& image = imageGray; |
|
#endif |
|
|
|
auto pixel = [&](int, int r, int c) { |
|
const ImageRef u(c+halfWindowSizeX, r+halfWindowSizeY); |
|
const uint8_t g(image(u)); |
|
Census& cs = imageCensus(r,c); |
|
cs = 0; |
|
for (int i=-halfWindowSizeY; i<=halfWindowSizeY; ++i) { |
|
for (int j=-halfWindowSizeX; j<=halfWindowSizeX; ++j) { |
|
cs <<= 1; |
|
if (g <= image(u.y+i, u.x+j)) |
|
cs += 1; |
|
} |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(imageCensus.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<imageCensus.rows; ++r) |
|
for (int c=0; c<imageCensus.cols; ++c) |
|
pixel(-1, r, c); |
|
} |
|
#endif |
|
|
|
// Compute search range from the given disparity-map and setup pixel-map at twice the scale; |
|
// the validity mask-map is considered as well and upscaled in the same time; |
|
// return the total size of the disparities searched |
|
SemiGlobalMatcher::Index SemiGlobalMatcher::Disparity2RangeMap(const DisparityMap& disparityMap, const MaskMap& maskMap, Disparity minNumDisp, Disparity minNumDispInvalid) |
|
{ |
|
ASSERT(!disparityMap.empty() && disparityMap.width()<maskMap.width() && disparityMap.height()<maskMap.height()); |
|
const cv::Size size2x(maskMap.size()); |
|
imagePixels.resize(size2x.area()); |
|
Index numCosts(0); |
|
maxNumDisp = 0; |
|
CLISTDEF0IDX(Disparity,Disparity) disps(31*31); |
|
for (int r=0; r<disparityMap.rows; ++r) { |
|
const int r2(r == 0 ? 0 : r*2+halfWindowSizeY); |
|
ASSERT(r2 < size2x.height); |
|
const int offset(r2*size2x.width); |
|
int c2e(halfWindowSizeX); |
|
const Mask* pm(maskMap.ptr<const Mask>(r*2+halfWindowSizeY, halfWindowSizeX)); |
|
for (int c=0, c2=0; c<disparityMap.cols; ++c, pm+=2) { |
|
Disparity numDisp; Range range; |
|
if (*pm == INVALID) { |
|
// set empty range |
|
range = Range{NO_DISP,NO_DISP}; |
|
numDisp = 0; |
|
} else { |
|
// set range based on the estimates around this location |
|
const bool bInvalid(disparityMap(r,c) == NO_DISP); |
|
// search range around 41x41 or 7x7 window |
|
disps.Empty(); |
|
const int hw(bInvalid ? 20 : 3); |
|
for (int i=-hw; i<=hw; ++i) { |
|
for (int j=-hw; j<=hw; ++j) { |
|
const ImageRef u(c+j,r+i); |
|
if (disparityMap.isInside(u)) { |
|
const Disparity d(disparityMap(u)); |
|
if (d != NO_DISP) |
|
disps.push_back(d); |
|
} |
|
} |
|
} |
|
// set search range |
|
if (disps.size() < 3) { |
|
range.maxDisp = MINF((Disparity)(disparityMap.width()*2/3), minNumDispInvalid); |
|
range.minDisp = -range.maxDisp; |
|
numDisp = range.numDisp(); |
|
} else { |
|
const Disparity disp(disps.GetMedian<Disparity>()*2); |
|
const auto minmax(disps.GetMinMax()); |
|
numDisp = (minmax.second-minmax.first)*2; |
|
if (numDisp < minNumDisp) { |
|
numDisp = minNumDisp; |
|
range.minDisp = disp-numDisp/2; |
|
range.maxDisp = disp+(numDisp+1)/2; |
|
} else { |
|
const Disparity maxNumDisp(bInvalid ? 64 : 32); |
|
if (numDisp > maxNumDisp) { |
|
range.minDisp = disp-(maxNumDisp*(disp-minmax.first*2)+1)/numDisp; |
|
range.maxDisp = disp+(maxNumDisp*(minmax.second*2+1-disp)+1)/numDisp; |
|
numDisp = range.numDisp(); |
|
} else { |
|
range.minDisp = disp-numDisp/2; |
|
range.maxDisp = disp+(numDisp+1)/2; |
|
} |
|
} |
|
} |
|
ASSERT(range.numDisp() == numDisp); |
|
if (maxNumDisp < numDisp) |
|
maxNumDisp = numDisp; |
|
} |
|
c2e += 2; |
|
do { |
|
ASSERT(c2 < size2x.width); |
|
PixelData& pixel = imagePixels[offset+c2]; |
|
pixel.range = range; |
|
pixel.idx = numCosts; |
|
numCosts += numDisp; |
|
} while (++c2 < c2e); |
|
} |
|
ASSERT(c2e < size2x.width); |
|
do { |
|
const PixelData& pixel = imagePixels[offset+c2e-1]; |
|
PixelData& _pixel = imagePixels[offset+c2e]; |
|
_pixel.range = pixel.range; |
|
_pixel.idx = numCosts; |
|
numCosts += pixel.range.numDisp(); |
|
} while (++c2e < size2x.width); |
|
const int _offsete((r+1 == disparityMap.rows ? size2x.height : r*2+halfWindowSizeY+2)*size2x.width); |
|
for (int _offset=offset+size2x.width; _offset<_offsete; _offset+=size2x.width) { |
|
for (int c2=0; c2<size2x.width; ++c2) { |
|
const PixelData& pixel = imagePixels[offset+c2]; |
|
PixelData& _pixel = imagePixels[_offset+c2]; |
|
_pixel.range = pixel.range; |
|
_pixel.idx = numCosts; |
|
numCosts += pixel.range.numDisp(); |
|
} |
|
} |
|
} |
|
return numCosts; |
|
} |
|
|
|
// Check for consistency between a left-to-right and right-to-left pair of stereo results; |
|
// the results are expected to be opposite in sign but equal in magnitude; |
|
// the valid disparities are returned in the left map |
|
void SemiGlobalMatcher::ConsistencyCrossCheck(DisparityMap& l2r, const DisparityMap& r2l, Disparity thCross) |
|
{ |
|
ASSERT(thCross >= 0); |
|
ASSERT(!l2r.empty() && !r2l.empty()); |
|
ASSERT(l2r.height() == r2l.height()); |
|
|
|
auto pixel = [&](int, int r, int c) { |
|
Disparity& ld = l2r(r,c); |
|
if (ld == NO_DISP) |
|
return; |
|
// compute the corresponding disparity pixel according to the disparity value |
|
const ImageRef v(c+ld,r); |
|
// check image bounds |
|
if (v.x < 0 || v.x >= r2l.width()) { |
|
ld = NO_DISP; |
|
return; |
|
} |
|
// check right disparity is valid |
|
const Disparity rd = r2l(v); |
|
if (r2l(v) == NO_DISP) { |
|
ld = NO_DISP; |
|
return; |
|
} |
|
// check disparity consistency: |
|
// since the left and right disparities are opposite in sign, |
|
// we determine their similarity by *summing* them, rather |
|
// than differencing them as you might expect |
|
if (ABS(ld + rd) > thCross) |
|
ld = NO_DISP; |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(l2r.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<l2r.rows; ++r) |
|
for (int c=0; c<l2r.cols; ++c) |
|
pixel(-1, r, c); |
|
} |
|
|
|
// Discard disparities that have a high similarity score |
|
void SemiGlobalMatcher::FilterByCost(DisparityMap& disparityMap, const AccumCostMap& costMap, AccumCost th) |
|
{ |
|
ASSERT(th > 0); |
|
ASSERT(!disparityMap.empty() && disparityMap.size() == costMap.size()); |
|
|
|
auto pixel = [&](int, int r, int c) { |
|
Disparity& d = disparityMap(r,c); |
|
if (d == NO_DISP) |
|
return; |
|
if (costMap(r,c) > th) |
|
d = NO_DISP; |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(disparityMap.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<disparityMap.rows; ++r) |
|
for (int c=0; c<disparityMap.cols; ++c) |
|
pixel(-1, r, c); |
|
} |
|
|
|
// Mark empty regions on the border of the disparity-map as invalid; |
|
// thValid is the number of valid disparities encountered to consider the valid region starts |
|
void SemiGlobalMatcher::ExtractMask(const DisparityMap& disparityMap, MaskMap& maskMap, int thValid) |
|
{ |
|
ASSERT(!disparityMap.empty()); |
|
ASSERT(maskMap.empty() || disparityMap.size() == maskMap.size()); |
|
if (disparityMap.size() != maskMap.size()) { |
|
maskMap.create(disparityMap.size()); |
|
maskMap.setTo(VALID); |
|
} |
|
|
|
#define MASK_PIXEL() \ |
|
Mask& m = maskMap(r,c); \ |
|
if (m == INVALID) \ |
|
continue; \ |
|
m = INVALID; \ |
|
if (disparityMap(r,c) == NO_DISP) \ |
|
continue; \ |
|
if (++numValid >= thValid) \ |
|
break |
|
|
|
// left-right direction |
|
{ |
|
auto pixel = [&](int r) { |
|
int numValid(0); |
|
for (int c=0; c<disparityMap.cols; ++c) { |
|
MASK_PIXEL(); |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(disparityMap.height(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<disparityMap.rows; ++r) |
|
pixel(r); |
|
} |
|
|
|
// right-left direction |
|
{ |
|
auto pixel = [&](int r) { |
|
int numValid(0); |
|
for (int c=disparityMap.cols; --c>=0; ) { |
|
MASK_PIXEL(); |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(disparityMap.height(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<disparityMap.rows; ++r) |
|
pixel(r); |
|
} |
|
|
|
#undef MASK_PIXEL |
|
|
|
#if 0 |
|
#define MASK_PIXEL() \ |
|
Mask& m = maskMap(r,c); \ |
|
if (m == INVALID) \ |
|
continue; \ |
|
if (disparityMap(r,c) != NO_DISP) \ |
|
break; \ |
|
m = INVALID |
|
|
|
// top-bottom direction |
|
{ |
|
auto pixel = [&](int c) { |
|
for (int r=0; r<disparityMap.rows; ++r) { |
|
MASK_PIXEL(); |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(disparityMap.width(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int c=0; c<disparityMap.cols; ++c) |
|
pixel(c); |
|
} |
|
|
|
// bottom-top direction |
|
{ |
|
auto pixel = [&](int c) { |
|
for (int r=disparityMap.rows; --r>=0; ) { |
|
MASK_PIXEL(); |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(disparityMap.width(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int c=0; c<disparityMap.cols; ++c) |
|
pixel(c); |
|
} |
|
|
|
#undef MASK_PIXEL |
|
#endif |
|
} |
|
|
|
// Translate disparity-map between left-to-right and right-to-left stereo pair |
|
// by translating to the x coordinated dictated by the disparity and negating the sign; |
|
// the sub-pixel steps is assumed to be one and |
|
// the input disparity-map has been cross-checked for consistency |
|
void SemiGlobalMatcher::FlipDirection(const DisparityMap& l2r, DisparityMap& r2l) |
|
{ |
|
ASSERT(!l2r.empty()); |
|
ASSERT(r2l.empty() || l2r.height() == r2l.height()); |
|
if (r2l.empty()) |
|
r2l.create(l2r.size()); |
|
r2l.setTo(NO_DISP); |
|
|
|
auto pixel = [&](int r) { |
|
for (int c=0; c<l2r.cols; ++c) { |
|
const Disparity d = l2r(r,c); |
|
if (d == NO_DISP) |
|
continue; |
|
// compute the corresponding disparity pixel according to the disparity value and set right disparity value |
|
for (int x=MAXF(c+d-1,0), xe=MINF(c+d+2,r2l.width()); x<xe; ++x) |
|
r2l(r,x) = -d; |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(l2r.rows, idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<l2r.rows; ++r) |
|
pixel(r); |
|
} |
|
|
|
// Translate disparity-map between left-to-right and right-to-left stereo pair |
|
// by translating to the x coordinated dictated by the disparity and negating the sign; |
|
// the input disparity-map has been cross-checked for consistency |
|
void SemiGlobalMatcher::UpscaleMask(MaskMap& maskMap, const cv::Size& size2x) |
|
{ |
|
ASSERT(!maskMap.empty()); |
|
MaskMap maskMap2x(size2x, INVALID); |
|
|
|
auto pixel = [&](int, int r, int c) { |
|
const int r2(r*2+halfWindowSizeY), c2(c*2+halfWindowSizeX); |
|
const Mask m(maskMap(r,c)); |
|
for (int i=0; i<2; ++i) { |
|
for (int j=0; j<2; ++j) { |
|
const ImageRef u(c2+j,r2+i); |
|
if (maskMap2x.isInside(u)) |
|
maskMap2x(u) = m; |
|
} |
|
} |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(maskMap.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<maskMap.rows; ++r) |
|
for (int c=0; c<maskMap.cols; ++c) |
|
pixel(-1, r, c); |
|
|
|
cv::swap(maskMap, maskMap2x); |
|
} |
|
|
|
// Sub-pixel disparity-map refinement based on the accumulated-cost volume |
|
void SemiGlobalMatcher::RefineDisparityMap(DisparityMap& disparityMap) const |
|
{ |
|
ASSERT(!disparityMap.empty()); |
|
if (subpixelSteps <= 1) |
|
return; |
|
if (subpixelMode == SUBPIXEL_NA) { |
|
// simply multiply disparity to the sub-pixel steps |
|
auto pixel = [&](int, int r, int c) { |
|
Disparity& d = disparityMap(r,c); |
|
if (d == NO_DISP) |
|
return; |
|
ASSERT((int)d*subpixelSteps > (int)std::numeric_limits<Disparity>::min()); |
|
ASSERT((int)d*subpixelSteps < (int)std::numeric_limits<Disparity>::max()); |
|
d *= subpixelSteps; |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(disparityMap.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<disparityMap.rows; ++r) |
|
for (int c=0; c<disparityMap.cols; ++c) |
|
pixel(-1, r, c); |
|
return; |
|
} |
|
// proposed subpixelMode algorithms |
|
typedef float real; |
|
struct Fit { |
|
static real linear(real x) { |
|
return x/real(2); |
|
} |
|
static real poly4(real x) { |
|
return (x*x*x*x + x)/real(4); |
|
} |
|
static real parabola(real x) { |
|
return x/(x+real(1)); |
|
} |
|
static real sine(real x) { |
|
return real(0.5) * (SIN((x-real(1))*real(HALF_PI)) + real(1)); |
|
} |
|
static real cosine(real x) { |
|
return (real(1) - COS(x*(real)(PI/3.0))); |
|
} |
|
static real lcBlend(real x) { |
|
const real factor(real(1.195) - COS(x*(real)(PI/2.3))); |
|
return cosine(x)*factor + linear(x)*(real(1)-factor); |
|
} |
|
// subpixelMode interpolation when only two values are available |
|
// returns fraction of distance from the primary to the other value |
|
static real semisubpixel(AccumCost primary, AccumCost other) { |
|
return real(0.5)*(static_cast<real>(primary) / static_cast<real>(other)); |
|
} |
|
// compute the offset to be added to the integer disparity to get the final result |
|
static real subpixelMode(AccumCost prev, AccumCost center, AccumCost next, SgmSubpixelMode subpixelMode) { |
|
ASSERT(prev != NO_ACCUMCOST && center != NO_ACCUMCOST && next != NO_ACCUMCOST); |
|
// use a lower quality two value interpolation if only two values are available |
|
if (prev == center) |
|
return center == next ? real(0) : semisubpixel(center, next); |
|
if (center == next) |
|
return prev == center ? real(0) : -semisubpixel(center, prev); |
|
// pick which direction to interpolate in |
|
const AccumCost ld(prev-center); |
|
const AccumCost rd(next-center); |
|
real x, mult; |
|
if (ld < rd) { |
|
x = static_cast<real>(ld) / static_cast<real>(rd); |
|
mult = real(1); |
|
} else { |
|
x = static_cast<real>(rd) / static_cast<real>(ld); |
|
mult = real(-1); |
|
} |
|
// use the selected subpixelMode function |
|
real value(0); |
|
switch (subpixelMode) { |
|
case SUBPIXEL_LINEAR: value = linear(x); break; |
|
case SUBPIXEL_POLY4: value = poly4(x); break; |
|
case SUBPIXEL_PARABOLA: value = parabola(x); break; |
|
case SUBPIXEL_SINE: value = sine(x); break; |
|
case SUBPIXEL_COSINE: value = cosine(x); break; |
|
case SUBPIXEL_LC_BLEND: value = lcBlend(x); break; |
|
}; |
|
// complete computation |
|
return (value - real(0.5))*mult; |
|
} |
|
}; |
|
// estimate sub-pixel disparity based on the cost values |
|
auto pixel = [&](int idx) { |
|
const PixelData& pixel = imagePixels[idx]; |
|
if (pixel.range.numDisp() < 2) |
|
return; |
|
Disparity& d = disparityMap(idx); |
|
if (d == NO_DISP) |
|
return; |
|
const AccumCost* accums = imageAccumCosts.cdata()+pixel.idx; |
|
const int idxDisp(d-pixel.range.minDisp); |
|
real disparity((real)d); |
|
if (d == pixel.range.minDisp) |
|
disparity += Fit::semisubpixel(accums[idxDisp], accums[idxDisp+1]); |
|
else if (d+1 == pixel.range.maxDisp) |
|
disparity -= Fit::semisubpixel(accums[idxDisp], accums[idxDisp-1]); |
|
else |
|
disparity += Fit::subpixelMode(accums[idxDisp-1], accums[idxDisp], accums[idxDisp+1], subpixelMode); |
|
ASSERT(ROUND2INT(disparity*subpixelSteps) > (int)std::numeric_limits<Disparity>::min()); |
|
ASSERT(ROUND2INT(disparity*subpixelSteps) < (int)std::numeric_limits<Disparity>::max()); |
|
d = (Disparity)ROUND2INT(disparity*subpixelSteps); |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelAccumInc(disparityMap.size().area(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<disparityMap.rows; ++r) |
|
for (int c=0; c<disparityMap.cols; ++c) |
|
pixel(r*disparityMap.cols+c); |
|
} |
|
|
|
|
|
#ifndef _RELEASE |
|
// extract disparity and range from the pixel-map |
|
void SemiGlobalMatcher::DisplayState(const cv::Size& size) const |
|
{ |
|
Image8U disparity(size); |
|
Image8U range(size); |
|
for (int idx=0; idx<size.area(); ++idx) { |
|
const PixelData& pixel = imagePixels[idx]; |
|
disparity(idx) = (uint8_t)(CLAMP(pixel.range.avgDisp(), Disparity(-128), Disparity(127))+Disparity(128)); |
|
range(idx) = (uint8_t)CLAMP(pixel.range.numDisp(), Disparity(0), Disparity(255)); |
|
} |
|
|
|
disparity.Show("Disparity", -1, false); |
|
range.Show("Range", -1, false); |
|
|
|
char c = 'a'; |
|
while (std::tolower(c) != 'q') |
|
c = cv::waitKey(); |
|
|
|
cv::destroyAllWindows(); |
|
} |
|
#endif |
|
|
|
// Compute the disparity-map for the rectified image from the given depth-map of the un-rectified image; |
|
// the disparity map needs to be already constructed at the desired size (valid size, excluding the border) |
|
void SemiGlobalMatcher::Depth2DisparityMap(const DepthMap& depthMap, const Matrix3x3& invH, const Matrix4x4& invQ, Disparity subpixelSteps, DisparityMap& disparityMap) |
|
{ |
|
auto pixel = [&](int, int r, int c) { |
|
const ImageRef x(c+halfWindowSizeX,r+halfWindowSizeY); Point2f u; |
|
ProjectVertex_3x3_2_2(invH.val, x.ptr(), u.ptr()); |
|
float depth, disparity; |
|
if (!depthMap.sampleSafe(depth, u, [](Depth d) { return d > 0; }) || !Image::Depth2Disparity(invQ, u, depth, disparity)) |
|
disparityMap(r,c) = NO_DISP; |
|
else |
|
disparityMap(r,c) = (Disparity)ROUND2INT(disparity*subpixelSteps); |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(disparityMap.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<disparityMap.rows; ++r) |
|
for (int c=0; c<disparityMap.cols; ++c) |
|
pixel(-1, r, c); |
|
} |
|
|
|
// Compute the depth-map for the un-rectified image from the given disparity-map of the rectified image |
|
void SemiGlobalMatcher::Disparity2DepthMap(const DisparityMap& disparityMap, const AccumCostMap& costMap, const Matrix3x3& H, const Matrix4x4& Q, Disparity subpixelSteps, DepthMap& depthMap, ConfidenceMap& confMap) |
|
{ |
|
ASSERT(costMap.empty() || costMap.size() == disparityMap.size()); |
|
ASSERT(!depthMap.empty()); |
|
ASSERT(confMap.empty() || confMap.size() == depthMap.size()); |
|
|
|
if (!costMap.empty()) { |
|
confMap.create(depthMap.size()); |
|
auto pixel = [&](int, int r, int c) { |
|
const ImageRef x(c,r); Point2f u; |
|
ProjectVertex_3x3_2_2(H.val, x.ptr(), u.ptr()); |
|
u.x -= (float)halfWindowSizeX; |
|
u.y -= (float)halfWindowSizeY; |
|
float disparity; |
|
if (!disparityMap.sampleSafe(disparity, u, [](Disparity d) { return d != NO_DISP; })) { |
|
depthMap(x) = 0; |
|
confMap(x) = 0; |
|
return; |
|
} |
|
float cost; |
|
costMap.sampleSafe(cost, u, [](AccumCost c) { return c != NO_ACCUMCOST; }); |
|
depthMap(x) = Image::Disparity2Depth(Q, u, disparity/subpixelSteps); |
|
confMap(x) = 1.f/(cost+1); |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(depthMap.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<depthMap.rows; ++r) |
|
for (int c=0; c<depthMap.cols; ++c) |
|
pixel(-1, r, c); |
|
} else { |
|
auto pixel = [&](int, int r, int c) { |
|
const ImageRef x(c,r); Point2f u; |
|
ProjectVertex_3x3_2_2(H.val, x.ptr(), u.ptr()); |
|
u.x -= (float)halfWindowSizeX; |
|
u.y -= (float)halfWindowSizeY; |
|
float disparity; |
|
if (!disparityMap.sampleSafe(disparity, u, [](Disparity d) { return d != NO_DISP; })) |
|
depthMap(x) = 0; |
|
else |
|
depthMap(x) = Image::Disparity2Depth(Q, u, disparity/subpixelSteps); |
|
}; |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
volatile Thread::safe_t idxPixel(-1); |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTPixelProcess(depthMap.size(), idxPixel, pixel)); |
|
WaitThreadWorkers(threads.size()); |
|
} else |
|
for (int r=0; r<depthMap.rows; ++r) |
|
for (int c=0; c<depthMap.cols; ++c) |
|
pixel(-1, r, c); |
|
} |
|
} |
|
|
|
// Compute the depth-map for the un-rectified image from the given disparity-map of the rectified image |
|
// by projecting the point-cloud to the image and setting the pixel depth as the average of the closest 4 points; |
|
// return false if the disparity map is completely empty |
|
bool SemiGlobalMatcher::ProjectDisparity2DepthMap(const DisparityMap& disparityMap, const AccumCostMap& costMap, const Matrix4x4& Q, Disparity subpixelSteps, DepthMap& depthMap, DepthRangeMap& depthRangeMap, ConfidenceMap& confMap) |
|
{ |
|
ASSERT(costMap.empty() || costMap.size() == disparityMap.size()); |
|
ASSERT(!depthMap.empty()); |
|
ASSERT(confMap.empty() || confMap.size() == depthMap.size()); |
|
|
|
// store 4 depths per pixel, the closest one from each side, ordered as: |
|
// - left, right, top, bottom (x pointing right and y pointing down) |
|
// with a small overlap border |
|
const float overlapBorder(0.5f+0.25f); |
|
struct DepthData { |
|
Image32F distMap; |
|
DepthMap depthMap; |
|
DepthRangeMap depthRangeMap; |
|
ConfidenceMap confMap; |
|
void Init(const cv::Size& size) { |
|
distMap.create(size); |
|
depthMap.create(size); |
|
depthRangeMap.create(size); |
|
confMap.create(size); |
|
depthMap.memset(0); |
|
} |
|
} depthDatas[4]; |
|
for (int i=0; i<4; ++i) |
|
depthDatas[i].Init(depthMap.size()); |
|
for (int r=0; r<disparityMap.rows; ++r) { |
|
for (int c=0; c<disparityMap.cols; ++c) { |
|
const Disparity disparityInt(disparityMap(r,c)); |
|
if (disparityInt == NO_DISP) |
|
continue; |
|
const float disparity((float)disparityInt/subpixelSteps); |
|
Point2f u, v; |
|
const ImageRef dx(c+halfWindowSizeX,r+halfWindowSizeY); |
|
const Depth depth(Image::Disparity2Depth(Q, dx, disparity, u)); |
|
if (depth <= 0) |
|
continue; |
|
const Disparity disparityCenter((Disparity)FLOOR2INT(disparity)); |
|
const DepthRange depthRange( |
|
Image::Disparity2Depth(Q, dx, (float)(disparityCenter-1)), |
|
Image::Disparity2Depth(Q, dx, (float)(disparityCenter+1)) |
|
); |
|
ASSERT(ISINSIDE(depth, depthRange.x, depthRange.y)); |
|
const float cost(costMap.empty()?0.f:1.f/(costMap(r,c)+1)); |
|
const ImageRef x(FLOOR2INT(u)); |
|
u.x -= 0.5f; u.y -= 0.5f; |
|
for (int i=-1; i<=1; ++i) { |
|
for (int j=-1; j<=1; ++j) { |
|
const ImageRef nx(x.x+j,x.y+i); |
|
if (!depthMap.isInside(nx)) |
|
continue; |
|
const Point2f dist((float)nx.x-u.x,(float)nx.y-u.y); |
|
const Point2f absDist(ABS(dist)); |
|
if (absDist.x > overlapBorder || absDist.y > overlapBorder) |
|
continue; |
|
const int idx((dist.x<0?1:0)+(dist.y<0?2:0)); |
|
DepthData& depthData = depthDatas[idx]; |
|
const float deistSq(normSq(dist)); |
|
float& ndeistSq = depthData.distMap(nx); |
|
Depth& ndepth = depthData.depthMap(nx); |
|
if (ndepth > 0 && ndeistSq <= deistSq) |
|
continue; |
|
ndeistSq = deistSq; |
|
ndepth = depth; |
|
depthData.depthRangeMap(nx) = depthRange; |
|
depthData.confMap(nx) = cost; |
|
} |
|
} |
|
} |
|
} |
|
typedef TAccumulator<Vec4f> ValueAccumulator; |
|
depthRangeMap.create(depthMap.size()); |
|
confMap.create(depthMap.size()); |
|
const float thDist(SQUARE(0.75f)); |
|
const Depth thDepth(0.02f); |
|
unsigned numDepths(0); |
|
for (int r=0; r<depthMap.rows; ++r) { |
|
for (int c=0; c<depthMap.cols; ++c) { |
|
float distCenter(std::numeric_limits<float>::max()); |
|
Depth depthCenter; |
|
for (int i=0; i<4; ++i) { |
|
const DepthData& depthData = depthDatas[i]; |
|
const Depth depth(depthData.depthMap(r,c)); |
|
if (depth <= 0) |
|
continue; |
|
const float dist(depthData.distMap(r,c)); |
|
if (distCenter > dist) { |
|
distCenter = dist; |
|
depthCenter = depth; |
|
} |
|
} |
|
if (distCenter > thDist) { |
|
depthMap(r,c) = Depth(0); |
|
continue; |
|
} |
|
ValueAccumulator acc(Vec4f::ZERO, 0.f); |
|
for (int i=0; i<4; ++i) { |
|
const DepthData& depthData = depthDatas[i]; |
|
const Depth depth(depthData.depthMap(r,c)); |
|
if (depth <= 0 || !IsDepthSimilar(depthCenter, depth, thDepth)) |
|
continue; |
|
const DepthRange& depthRange(depthData.depthRangeMap(r,c)); |
|
acc.Add(Vec4f((float)depth,depthRange.x,depthRange.y,depthData.confMap(r,c)), SQRT(depthData.distMap(r,c))); |
|
} |
|
ASSERT(!acc.IsEmpty()); |
|
const Vec4f value(acc.Normalized()); |
|
depthMap(r,c) = value(0); |
|
depthRangeMap(r,c) = DepthRange(value(1),value(2)); |
|
confMap(r,c) = value(3); |
|
++numDepths; |
|
} |
|
} |
|
if (costMap.empty()) |
|
confMap.release(); |
|
return numDepths > 0; |
|
} |
|
|
|
|
|
EventThreadPool SemiGlobalMatcher::threads; |
|
Semaphore SemiGlobalMatcher::sem; |
|
|
|
// start worker threads |
|
void SemiGlobalMatcher::CreateThreads(unsigned nMaxThreads) |
|
{ |
|
ASSERT(nMaxThreads > 0); |
|
ASSERT(threads.IsEmpty() && threads.empty()); |
|
if (nMaxThreads > 1) { |
|
threads.resize(nMaxThreads); |
|
threads.start(ThreadWorker); |
|
} |
|
} |
|
// destroy worker threads |
|
void SemiGlobalMatcher::DestroyThreads() |
|
{ |
|
ASSERT(threads.IsEmpty()); |
|
if (!threads.empty()) { |
|
FOREACH(i, threads) |
|
threads.AddEvent(new EVTClose()); |
|
threads.Release(); |
|
} |
|
} |
|
|
|
void* SemiGlobalMatcher::ThreadWorker(void*) { |
|
while (true) { |
|
CAutoPtr<Event> evt(threads.GetEvent()); |
|
switch (evt->GetID()) { |
|
case EVT_JOB: |
|
evt->Run(); |
|
break; |
|
case EVT_CLOSE: |
|
return NULL; |
|
default: |
|
ASSERT("Should not happen!" == NULL); |
|
} |
|
sem.Signal(); |
|
} |
|
return NULL; |
|
} |
|
void SemiGlobalMatcher::WaitThreadWorkers(unsigned nJobs) |
|
{ |
|
while (nJobs-- > 0) |
|
sem.Wait(); |
|
ASSERT(threads.IsEmpty()); |
|
} |
|
/*----------------------------------------------------------------*/ |
|
|
|
|
|
|
|
// S T R U C T S /////////////////////////////////////////////////// |
|
|
|
bool SemiGlobalMatcher::ExportDisparityDataRaw(const String& fileName, const DisparityMap& disparityMap, const AccumCostMap& costMap, const cv::Size& imageSize, const Matrix3x3& H, const Matrix4x4& Q, Disparity subpixelSteps) |
|
{ |
|
ASSERT(!disparityMap.empty()); |
|
ASSERT(costMap.empty() || disparityMap.size() == costMap.size()); |
|
|
|
FILE *f = fopen(fileName, "wb"); |
|
if (f == NULL) |
|
return false; |
|
|
|
// write info |
|
fwrite(&imageSize.width, sizeof(int), 2, f); |
|
fwrite(H.val, sizeof(REAL), 9, f); |
|
fwrite(Q.val, sizeof(REAL), 16, f); |
|
fwrite(&subpixelSteps, sizeof(Disparity), 1, f); |
|
|
|
// write resolution |
|
fwrite(&disparityMap.cols, sizeof(int), 1, f); |
|
fwrite(&disparityMap.rows, sizeof(int), 1, f); |
|
|
|
// write disparity-map |
|
fwrite(disparityMap.getData(), sizeof(Disparity), disparityMap.area(), f); |
|
|
|
// write cost-map |
|
if (!costMap.empty()) |
|
fwrite(costMap.getData(), sizeof(AccumCost), costMap.area(), f); |
|
|
|
fclose(f); |
|
return true; |
|
} // ExportDisparityDataRaw |
|
// same as above, but exports also the empty border |
|
bool SemiGlobalMatcher::ExportDisparityDataRawFull(const String& fileName, const DisparityMap& disparityMap, const AccumCostMap& costMap, const cv::Size& imageSize, const Matrix3x3& H, const Matrix4x4& Q, Disparity subpixelSteps) |
|
{ |
|
ASSERT(!disparityMap.empty()); |
|
ASSERT(costMap.empty() || disparityMap.size() == costMap.size()); |
|
|
|
const cv::Size size(disparityMap.width()+2*halfWindowSizeX,disparityMap.height()+2*halfWindowSizeY); |
|
const cv::Rect ROI(halfWindowSizeX,halfWindowSizeY, disparityMap.width(),disparityMap.height()); |
|
DisparityMap disparityMapFull(size, NO_DISP); |
|
disparityMap.copyTo(disparityMapFull(ROI)); |
|
if (costMap.empty()) |
|
return ExportDisparityDataRaw(fileName, disparityMapFull, costMap, imageSize, H, Q, subpixelSteps); |
|
AccumCostMap costMapFull(size, NO_ACCUMCOST); |
|
costMap.copyTo(costMapFull(ROI)); |
|
return ExportDisparityDataRaw(fileName, disparityMapFull, costMapFull, imageSize, H, Q, subpixelSteps); |
|
} // ExportDisparityDataRawFull |
|
|
|
bool SemiGlobalMatcher::ImportDisparityDataRaw(const String& fileName, DisparityMap& disparityMap, AccumCostMap& costMap, cv::Size& imageSize, Matrix3x3& H, Matrix4x4& Q, Disparity& subpixelSteps) |
|
{ |
|
FILE *f = fopen(fileName, "rb"); |
|
if (f == NULL) |
|
return false; |
|
|
|
// read info |
|
fread(&imageSize.width, sizeof(int), 2, f); |
|
fread(H.val, sizeof(REAL), 9, f); |
|
fread(Q.val, sizeof(REAL), 16, f); |
|
fread(&subpixelSteps, sizeof(Disparity), 1, f); |
|
ASSERT(imageSize.width > 0 && imageSize.height > 0); |
|
|
|
// read resolution |
|
int w, h; |
|
fread(&w, sizeof(int), 1, f); |
|
fread(&h, sizeof(int), 1, f); |
|
ASSERT(w > 0 && h > 0); |
|
|
|
// read disparity-map |
|
disparityMap.create(h,w); |
|
fread(disparityMap.getData(), sizeof(Disparity), w*h, f); |
|
|
|
// read cost-map |
|
if (fgetc(f) != EOF) { |
|
fseek(f, -1, SEEK_CUR); |
|
costMap.create(h,w); |
|
fread(costMap.getData(), sizeof(AccumCost), w*h, f); |
|
} |
|
|
|
fclose(f); |
|
return true; |
|
} // ImportDisparityDataRaw |
|
// same as above, but imports also the empty border |
|
bool SemiGlobalMatcher::ImportDisparityDataRawFull(const String& fileName, DisparityMap& disparityMap, AccumCostMap& costMap, cv::Size& imageSize, Matrix3x3& H, Matrix4x4& Q, Disparity& subpixelSteps) |
|
{ |
|
if (!ImportDisparityDataRaw(fileName, disparityMap, costMap, imageSize, H, Q, subpixelSteps)) |
|
return false; |
|
const cv::Size sizeValid(disparityMap.width()-2*halfWindowSizeX,disparityMap.height()-2*halfWindowSizeY); |
|
const cv::Rect ROI(halfWindowSizeX,halfWindowSizeY, sizeValid.width,sizeValid.height); |
|
disparityMap = disparityMap(ROI).clone(); |
|
if (!costMap.empty()) |
|
costMap = costMap(ROI).clone(); |
|
return true; |
|
} // ImportDisparityDataRawFull |
|
|
|
|
|
// export disparity-map as an image (red - maximum disparity, blue - minimum disparity) |
|
Image8U3 SemiGlobalMatcher::DisparityMap2Image(const DisparityMap& disparityMap, Disparity minDisparity, Disparity maxDisparity) |
|
{ |
|
ASSERT(!disparityMap.empty()); |
|
// find min and max values |
|
if (minDisparity == NO_DISP || maxDisparity == NO_DISP) { |
|
CLISTDEF0(Disparity) disparities(0, disparityMap.area()); |
|
for (int i=disparityMap.area(); i-- > 0; ) { |
|
const Disparity disparity = disparityMap[i]; |
|
if (disparity != NO_DISP) |
|
disparities.emplace_back(disparity); |
|
} |
|
if (!disparities.empty()) { |
|
const std::pair<float,float> th(ComputeX84Threshold<Disparity,float>(disparities.data(), disparities.size(), 5.2f)); |
|
minDisparity = (Disparity)ROUND2INT(th.first-th.second); |
|
maxDisparity = (Disparity)ROUND2INT(th.first+th.second); |
|
} |
|
DEBUG_ULTIMATE("\tdisparity range: [%d, %d]", minDisparity, maxDisparity); |
|
} |
|
const float sclDepth(1.f/(float)(maxDisparity - minDisparity)); |
|
// create color image |
|
Image8U3 img(cv::Size(disparityMap.width()+2*halfWindowSizeX,disparityMap.height()+2*halfWindowSizeY), Pixel8U::BLACK); |
|
for (int r=0; r<disparityMap.rows; ++r) { |
|
for (int c=0; c<disparityMap.cols; ++c) { |
|
const Disparity disparity(disparityMap(r,c)); |
|
if (disparity == NO_DISP) |
|
continue; |
|
img(r+halfWindowSizeY,c+halfWindowSizeX) = Pixel8U::gray2color(CLAMP((float)(maxDisparity-disparity)*sclDepth, 0.f, 1.f)); |
|
} |
|
} |
|
return img; |
|
} // DisparityMap2Image |
|
bool SemiGlobalMatcher::ExportDisparityMap(const String& fileName, const DisparityMap& disparityMap, Disparity minDisparity, Disparity maxDisparity) |
|
{ |
|
if (disparityMap.empty()) |
|
return false; |
|
return DisparityMap2Image(disparityMap, minDisparity, maxDisparity).Save(fileName); |
|
} // ExportDisparityMap |
|
|
|
|
|
// export point cloud |
|
bool SemiGlobalMatcher::ExportPointCloud(const String& fileName, const Image& imageData, const DisparityMap& disparityMap, const Matrix4x4& Q, Disparity subpixelSteps) |
|
{ |
|
ASSERT(!disparityMap.empty()); |
|
|
|
// vertex definition |
|
struct Vertex { |
|
float x,y,z; |
|
uint8_t r,g,b; |
|
}; |
|
// list of property information for a vertex |
|
static PLY::PlyProperty vert_props[] = { |
|
{"x", PLY::Float32, PLY::Float32, offsetof(Vertex,x), 0, 0, 0, 0}, |
|
{"y", PLY::Float32, PLY::Float32, offsetof(Vertex,y), 0, 0, 0, 0}, |
|
{"z", PLY::Float32, PLY::Float32, offsetof(Vertex,z), 0, 0, 0, 0}, |
|
{"red", PLY::Uint8, PLY::Uint8, offsetof(Vertex,r), 0, 0, 0, 0}, |
|
{"green", PLY::Uint8, PLY::Uint8, offsetof(Vertex,g), 0, 0, 0, 0}, |
|
{"blue", PLY::Uint8, PLY::Uint8, offsetof(Vertex,b), 0, 0, 0, 0}, |
|
}; |
|
// list of the kinds of elements in the PLY |
|
static const char* elem_names[] = { |
|
"vertex" |
|
}; |
|
|
|
// create PLY object |
|
ASSERT(!fileName.empty()); |
|
Util::ensureFolder(fileName); |
|
const size_t memBufferSize(disparityMap.area()*(8*3/*pos*/+3*3/*color*/+7/*space*/+2/*eol*/) + 2048/*extra size*/); |
|
PLY ply; |
|
if (!ply.write(fileName, 1, elem_names, PLY::BINARY_LE, memBufferSize)) |
|
return false; |
|
|
|
// describe what properties go into the vertex elements |
|
ply.describe_property("vertex", 6, vert_props); |
|
|
|
// export the array of 3D points |
|
Vertex vertex; |
|
for (int r=0; r<disparityMap.rows; ++r) { |
|
for (int c=0; c<disparityMap.cols; ++c) { |
|
const Disparity& disparity = disparityMap(r,c); |
|
if (disparity == NO_DISP) |
|
continue; |
|
Point2f u; |
|
Depth depth(Image::Disparity2Depth(Q, ImageRef(c+halfWindowSizeX,r+halfWindowSizeY), (float)disparity/subpixelSteps, u)); |
|
if (depth <= 0) |
|
continue; |
|
if (!imageData.image.isInsideWithBorder<float,1>(u)) |
|
continue; |
|
const Point3f X(imageData.camera.TransformPointI2W(Point3(u,depth))); |
|
vertex.x = X.x; vertex.y = X.y; vertex.z = X.z; |
|
const Pixel8U C(imageData.image.empty() ? Pixel8U::WHITE : imageData.image.sample(u)); |
|
vertex.r = C.r; vertex.g = C.g; vertex.b = C.b; |
|
ply.put_element(&vertex); |
|
} |
|
} |
|
if (ply.get_current_element_count() == 0) |
|
return false; |
|
|
|
// write to file |
|
return ply.header_complete(); |
|
} // ExportPointCloud |
|
|
|
// imports a DIMAP file and converts it to point-cloud |
|
bool SemiGlobalMatcher::ImportPointCloud(const String& fileName, const ImageArr& images, PointCloud& pointcloud) |
|
{ |
|
// load disparity-map |
|
Disparity subpixelSteps; |
|
cv::Size imageSize; Matrix3x3 H; Matrix4x4 Q; |
|
DisparityMap disparityMap; AccumCostMap costMap; |
|
if (!ImportDisparityDataRawFull(fileName, disparityMap, costMap, imageSize, H, Q, subpixelSteps)) |
|
return false; |
|
// parse image index from the file name |
|
const String name(Util::getFileName(fileName)); |
|
IIndex idxImage(NO_ID), idxImagePair(NO_ID); |
|
if (sscanf(name, "%u_%u", &idxImage, &idxImagePair) != 2 || idxImage == NO_ID || idxImagePair == NO_ID) |
|
return false; |
|
const Image& imageData = images[idxImage]; |
|
ASSERT(imageData.image.size() == imageSize); |
|
// import the array of 3D points |
|
for (int r=0; r<disparityMap.rows; ++r) { |
|
for (int c=0; c<disparityMap.cols; ++c) { |
|
const Disparity& disparity = disparityMap(r,c); |
|
if (disparity == NO_DISP) |
|
continue; |
|
Point2f u; |
|
Depth depth(Image::Disparity2Depth(Q, ImageRef(c+halfWindowSizeX,r+halfWindowSizeY), (float)disparity/subpixelSteps, u)); |
|
if (depth <= 0) |
|
continue; |
|
if (!imageData.image.isInsideWithBorder<float,1>(u)) |
|
continue; |
|
pointcloud.points.emplace_back(Cast<PointCloud::Point::Type>(imageData.camera.TransformPointI2W(Point3(u,depth)))); |
|
pointcloud.colors.emplace_back(imageData.image.empty() ? Pixel8U::WHITE : imageData.image.sample(u)); |
|
} |
|
} |
|
return true; |
|
} // ImportPointCloud |
|
/*----------------------------------------------------------------*/ |
|
|
|
|
|
bool MVS::STEREO::ExportCamerasEngin(const Scene& scene, const String& fileName) |
|
{ |
|
ASSERT(!scene.IsEmpty()); |
|
|
|
File f(fileName, File::WRITE, File::CREATE | File::TRUNCATE); |
|
if (!f.isOpen()) |
|
return false; |
|
|
|
// write header |
|
f.print("n_cameras %u\n", scene.images.size()); |
|
f.print("n_points %u\n", 0); |
|
|
|
// write cameras |
|
for (const Image& image: scene.images) { |
|
if (!image.IsValid()) |
|
continue; |
|
const Point3 t(image.camera.GetT()); |
|
f.print("%u %u %u %s " |
|
"%g %g %g %g " |
|
"%g %g %g %g %g %g %g %g %g " |
|
"%g %g %g " |
|
"%g %g " |
|
"%u", |
|
image.ID, image.width, image.height, image.name.c_str(), |
|
image.camera.K(0,0), image.camera.K(1,1), image.camera.K(0,2), image.camera.K(1,2), |
|
image.camera.R(0,0), image.camera.R(0,1), image.camera.R(0,2), |
|
image.camera.R(1,0), image.camera.R(1,1), image.camera.R(1,2), |
|
image.camera.R(2,0), image.camera.R(2,1), image.camera.R(2,2), |
|
t.x, t.y, t.z, |
|
0, 0, |
|
image.neighbors.size() |
|
); |
|
for (const auto& neighbor: image.neighbors) |
|
f.print(" %u", neighbor.ID); |
|
f.print("\n"); |
|
} |
|
|
|
return true; |
|
} // ExportCamerasEngin |
|
/*----------------------------------------------------------------*/
|
|
|