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

/*
* 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
/*----------------------------------------------------------------*/