From 9ad27d104faa1645f85449400d1e5f0ea523cb67 Mon Sep 17 00:00:00 2001
From: Nicolas Pope <nicolas.pope@utu.fi>
Date: Tue, 18 Jun 2019 10:50:52 +0300
Subject: [PATCH] Add automated matching to registration app

---
 applications/registration/CMakeLists.txt      |   1 +
 .../registration/src/correspondances.cpp      |  80 +++++++++
 .../registration/src/correspondances.hpp      |   8 +-
 applications/registration/src/manual.cpp      | 131 +++++++++++++-
 applications/registration/src/sfm.cpp         | 164 ++++++++++++++++++
 applications/registration/src/sfm.hpp         |  44 +++++
 components/rgbd-sources/src/local.cpp         |   6 +
 7 files changed, 430 insertions(+), 4 deletions(-)
 create mode 100644 applications/registration/src/sfm.cpp
 create mode 100644 applications/registration/src/sfm.hpp

diff --git a/applications/registration/CMakeLists.txt b/applications/registration/CMakeLists.txt
index f3bde2653..6597192ca 100644
--- a/applications/registration/CMakeLists.txt
+++ b/applications/registration/CMakeLists.txt
@@ -2,6 +2,7 @@ set(REGSRC
 	src/main.cpp
 	src/manual.cpp
 	src/correspondances.cpp
+	src/sfm.cpp
 )
 
 add_executable(ftl-register ${REGSRC})
diff --git a/applications/registration/src/correspondances.cpp b/applications/registration/src/correspondances.cpp
index 58eb1c6e2..134b9088a 100644
--- a/applications/registration/src/correspondances.cpp
+++ b/applications/registration/src/correspondances.cpp
@@ -1,5 +1,7 @@
 #include "correspondances.hpp"
 #include <nlohmann/json.hpp>
+#include <random>
+#include <chrono>
 
 using ftl::registration::Correspondances;
 using std::string;
@@ -54,6 +56,15 @@ bool Correspondances::add(int tx, int ty, int sx, int sy) {
 	return true;
 }
 
+PointXYZ makePCL(Source *s, int x, int y) {
+	Eigen::Vector4f p1 = s->point(x,y);
+	PointXYZ pcl_p1;
+	pcl_p1.x = p1[0];
+	pcl_p1.y = p1[1];
+	pcl_p1.z = p1[2];
+	return pcl_p1;
+}
+
 void Correspondances::removeLast() {
 	uptodate_ = false;
 	targ_cloud_->erase(targ_cloud_->end()-1);
@@ -76,6 +87,75 @@ double Correspondances::estimateTransform() {
 	return validate.validateTransformation(src_cloud_, targ_cloud_, transform_);
 }
 
+static std::default_random_engine generator;
+
+void Correspondances::convertToPCL(const std::vector<std::tuple<int,int,int,int>> &p, std::vector<std::pair<pcl::PointXYZ, pcl::PointXYZ>> &pp) {
+	for (size_t i=0; i<p.size(); i++) {
+		auto [sx,sy,tx,ty] = p[i];
+		//LOG(INFO) << "POINT " << sx << "," << sy;
+		auto p1 = makePCL(src_, sx, sy);
+		auto p2 = makePCL(targ_, tx, ty);
+		pp.push_back(std::make_pair(p1,p2));
+	}
+}
+
+double Correspondances::findBest(Eigen::Matrix4f &tr, const std::vector<std::tuple<int,int,int,int>> &p, const std::vector<std::pair<pcl::PointXYZ, pcl::PointXYZ>> &pp, int K, int N) {
+	double score = 10000.0f;
+	vector<tuple<int,int,int,int>> best;
+	Eigen::Matrix4f bestT;
+
+	pcl::registration::TransformationValidationEuclidean<PointXYZ, PointXYZ> validate;
+	pcl::registration::TransformationEstimationSVD<PointXYZ,PointXYZ> svd;
+
+	std::mt19937 rng(std::chrono::steady_clock::now().time_since_epoch().count());
+	std::uniform_int_distribution<int> distribution(0,p.size()-1);
+	//int dice_roll = distribution(generator);
+	auto dice = std::bind ( distribution, rng );
+
+	vector<int> idx(K);
+	for (int i = 0; i < K; i++) { idx[i] = i; }
+
+	for (int n=0; n<N; n++) {
+		pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_t(new PointCloud<PointXYZ>);
+		pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_s(new PointCloud<PointXYZ>);
+
+		vector<tuple<int,int,int,int>> ps;
+
+		for (int k=0; k<K; k++) {
+			int r = dice();
+			//LOG(INFO) << "DICE " << r << "  -  " << K;
+			//ps.push_back(p[r]);
+			auto [sx,sy,tx,ty] = p[r];
+			//LOG(INFO) << "POINT " << sx << "," << sy;
+			//auto p1 = makePCL(src_, sx, sy);
+			//auto p2 = makePCL(targ_, tx, ty);
+
+			auto [p1,p2] = pp[r];
+
+			if (p1.z >= 30.0f || p2.z >= 30.0f) { k--; continue; }
+			ps.push_back(std::make_tuple(tx,ty,sx,sy));
+			cloud_s->push_back(p1);
+			cloud_t->push_back(p2);
+		}
+
+		Eigen::Matrix4f T;
+		svd.estimateRigidTransformation(*cloud_s, idx, *cloud_t, idx, T);
+		float scoreT = validate.validateTransformation(cloud_s, cloud_t, T);
+
+		if (scoreT < score) {
+			score = scoreT;
+			best = ps;
+			bestT = T;
+		}
+	}
+
+	// TODO Add these best points to actual clouds.
+	log_ = best;
+	tr = bestT;
+	//uptodate_ = true;
+	return score;
+}
+
 Eigen::Matrix4f Correspondances::transform() {
 	if (!uptodate_) estimateTransform();
 	return (parent_) ? parent_->transform() * transform_ : transform_;
diff --git a/applications/registration/src/correspondances.hpp b/applications/registration/src/correspondances.hpp
index ee519bbc1..bd08dd6dd 100644
--- a/applications/registration/src/correspondances.hpp
+++ b/applications/registration/src/correspondances.hpp
@@ -48,6 +48,8 @@ class Correspondances {
 
 	const std::vector<std::tuple<int,int,int,int>> &screenPoints() const { return log_; }
 
+	void setPoints(const std::vector<std::tuple<int,int,int,int>> &p) { log_ = p; }
+
 	/**
 	 * Calculate a transform using current set of correspondances.
 	 * 
@@ -55,13 +57,17 @@ class Correspondances {
 	 */
 	double estimateTransform();
 
+	double findBest(Eigen::Matrix4f &tr, const std::vector<std::tuple<int,int,int,int>> &p, const std::vector<std::pair<pcl::PointXYZ, pcl::PointXYZ>> &p2, int K, int N);
+
+	void convertToPCL(const std::vector<std::tuple<int,int,int,int>> &p, std::vector<std::pair<pcl::PointXYZ, pcl::PointXYZ>> &p2);
+
 	/**
 	 * Get the estimated transform. This includes any parent transforms to make
 	 * it relative to root camera.
 	 */
 	Eigen::Matrix4f transform();
 
-	void setTransform(Eigen::Matrix4f &t) { transform_ = t; }
+	void setTransform(Eigen::Matrix4f &t) { uptodate_ = true; transform_ = t; }
 
 	private:
 	Correspondances *parent_;
diff --git a/applications/registration/src/manual.cpp b/applications/registration/src/manual.cpp
index 6269b0830..ca7f1cbf5 100644
--- a/applications/registration/src/manual.cpp
+++ b/applications/registration/src/manual.cpp
@@ -1,5 +1,6 @@
 #include "manual.hpp"
 #include "correspondances.hpp"
+#include "sfm.hpp"
 
 #include <loguru.hpp>
 
@@ -13,6 +14,7 @@
 #include <opencv2/imgcodecs.hpp>
 #include <opencv2/videoio.hpp>
 #include <opencv2/highgui.hpp>
+#include <opencv2/xphoto.hpp>
 
 #include <map>
 
@@ -20,6 +22,7 @@ using std::string;
 using std::vector;
 using std::pair;
 using std::map;
+using std::tuple;
 
 using ftl::net::Universe;
 using ftl::rgbd::Source;
@@ -125,6 +128,32 @@ static void build_correspondances(const vector<Source*> &sources, map<string, Co
 	}
 }
 
+void averageDepth(vector<Mat> &in, Mat &out) {
+	const int rows = in[0].rows;
+	const int cols = in[0].cols;
+
+	// todo: create new only if out is empty (or wrong shape/type)
+	out = Mat(rows, cols, CV_32FC1);
+
+	for (int i = 0; i < rows * cols; ++i) {
+		float sum = 0;
+		int good_values = 0;
+		for (int i_in = 0; i_in < in.size(); ++i_in) {
+			float d = in[i_in].at<float>(i);
+			if (d < 40) {
+				good_values++;
+				sum += d;
+			}
+		}
+
+		if (good_values > 0) {
+			out.at<float>(i) = sum / (float) good_values;
+		} else {
+			out.at<float>(i) = 41.0f;
+		}
+	}
+}
+
 void ftl::registration::manual(ftl::Configurable *root) {
 	using namespace cv;
 	
@@ -134,7 +163,7 @@ void ftl::registration::manual(ftl::Configurable *root) {
 	net->waitConnections();
 
 	auto sources = ftl::createArray<Source>(root, "sources", net);
-
+ 
 	int curtarget = 0;
 	bool freeze = false;
 
@@ -179,8 +208,13 @@ void ftl::registration::manual(ftl::Configurable *root) {
 
 	bool depthtoggle = false;
 	double lastScore = 0.0;
+	bool insearch = false;
 
 	Correspondances *current = corrs[sources[curtarget]->getURI()];
+	
+	// Features for current frame...
+	vector<tuple<int,int,int,int>> feat;
+	vector<std::pair<pcl::PointXYZ, pcl::PointXYZ>> pclfeat;
 
 	while (ftl::running) {
 		if (!freeze) {
@@ -207,6 +241,7 @@ void ftl::registration::manual(ftl::Configurable *root) {
 		} else if (current->source()->isReady() && current->target()->isReady()) {
 			current->source()->getFrames(tsrgb, tsdepth);
 			current->target()->getFrames(ttrgb, ttdepth);
+
 			tsrgb.copyTo(srgb);
 			ttrgb.copyTo(trgb);
 			ttdepth.convertTo(tdepth, CV_8U, 255.0f / 10.0f);
@@ -214,6 +249,11 @@ void ftl::registration::manual(ftl::Configurable *root) {
 			tsdepth.convertTo(sdepth, CV_8U, 255.0f / 10.0f);
 			applyColorMap(sdepth, sdepth, cv::COLORMAP_JET);
 
+			Ptr<xphoto::WhiteBalancer> wb;
+			wb = xphoto::createSimpleWB();
+			wb->balanceWhite(tsrgb, srgb);
+			wb->balanceWhite(ttrgb, trgb);
+
 			// Apply points and labels...
 			auto &points = current->screenPoints();
 			for (auto &p : points) {
@@ -247,8 +287,16 @@ void ftl::registration::manual(ftl::Configurable *root) {
 			curtarget = key-48;
 			if (curtarget >= sources.size()) curtarget = 0;
 			current = corrs[sources[curtarget]->getURI()];
-		} else if (key == 'd') depthtoggle = !depthtoggle;
-		else if (key == 'a') {
+			feat.clear();
+			pclfeat.clear();
+			lastScore = 1000.0;
+		} else if (key == 'd') {
+			depthtoggle = !depthtoggle;
+		} else if (key == 'c') {
+			feat.clear();
+			pclfeat.clear();
+			lastScore = 1000.0;
+		}else if (key == 'a') {
 			if (current->add(lastTX,lastTY,lastSX,lastSY)) {
 				lastTX = lastTY = lastSX = lastSY = 0;
 				lastScore = current->estimateTransform();
@@ -274,6 +322,83 @@ void ftl::registration::manual(ftl::Configurable *root) {
 			LOG(INFO) << "Saved!";
 		} else if (key == 't') {
 			current->source()->setPose(current->transform());
+		} else if (key == 'g') {
+			if (!insearch) {
+				insearch = true;
+				LOG(INFO) << "Features matched = " << feat.size();
+
+				ftl::pool.push([&lastScore,&insearch,current,&feat,&pclfeat](int id) {
+					LOG(INFO) << "START";
+					lastScore = 1000.0;
+					do {
+						Eigen::Matrix4f tr;
+						float s = current->findBest(tr, feat, pclfeat, 20, 100);
+						//LOG(INFO) << "SCORE = " << s;
+						if (s < lastScore) {
+							lastScore = s;
+							current->setTransform(tr);
+							current->source()->setPose(current->transform());
+						}
+					} while (ftl::running && insearch && lastScore > 0.000001);
+					insearch = false;
+					LOG(INFO) << "DONE: " << lastScore;
+				});
+				/*for (int i=0; i<feat.size(); i++) {
+					auto [sx,sy,tx,ty] = feat[i];
+					current->add(tx,ty,sx,sy);
+				}
+				lastScore = current->estimateTransform();*/
+			} else {
+				insearch = false;
+			}
+		} else if (key == 'f') {
+			Mat f1, d1, f2, d2;
+
+			size_t buffer_size = 10;
+			size_t buffer_i = 0;
+			vector<vector<Mat>> buffer(2, vector<Mat>(buffer_size));
+
+			for (size_t i = 0; i < buffer_size; ++i) {
+				current->source()->grab();
+				current->target()->grab();
+				current->source()->getFrames(f1, d1);
+				current->target()->getFrames(f2, d2);
+				buffer[0][i] = d1;
+				buffer[1][i] = d2;
+
+				std::this_thread::sleep_for(std::chrono::milliseconds(20));
+			}
+			averageDepth(buffer[0], d1);
+			averageDepth(buffer[1], d2);
+			Mat d1_v, d2_v;
+			d1.convertTo(d1_v, CV_8U, 255.0f / 10.0f);
+			d2.convertTo(d2_v, CV_8U, 255.0f / 10.0f);
+			applyColorMap(d1_v, d1_v, cv::COLORMAP_JET);
+			applyColorMap(d2_v, d2_v, cv::COLORMAP_JET);
+
+			cv::imshow("smooth d1", d1_v);
+			cv::imshow("smooth d2", d2_v);
+
+			Ptr<xphoto::WhiteBalancer> wb;
+			wb = xphoto::createSimpleWB();
+			wb->balanceWhite(f1, f1);
+			wb->balanceWhite(f2, f2);
+
+			vector<tuple<int,int,int,int>> tfeat;
+			if (!insearch && ftl::registration::featuresSIFT(f1, f2, tfeat, 10)) {
+				vector<tuple<int,int,int,int>> tfeat2;
+				for (int j=0; j<tfeat.size(); j++) {
+					auto [sx,sy,tx,ty] = tfeat[j];
+					tfeat2.push_back(std::make_tuple(tx,ty,sx,sy));
+				}
+				current->setPoints(tfeat2);
+				feat.insert(feat.end(), tfeat.begin(), tfeat.end());
+
+				//vector<std::pair<pcl::PointXYZ, pcl::PointXYZ>> tpclfeat;
+				current->convertToPCL(tfeat,pclfeat);
+			} else {
+				LOG(ERROR) << "Can't add features whilst searching...";
+			}
 		} else if (key == 32) freeze = !freeze;
 	}
 
diff --git a/applications/registration/src/sfm.cpp b/applications/registration/src/sfm.cpp
new file mode 100644
index 000000000..5d5e7cc9a
--- /dev/null
+++ b/applications/registration/src/sfm.cpp
@@ -0,0 +1,164 @@
+#include "sfm.hpp"
+#include <opencv2/sfm.hpp>
+#include <opencv2/viz.hpp>
+#include <opencv2/calib3d.hpp>
+#include <opencv2/core.hpp>
+#include <opencv2/features2d.hpp>
+#include <opencv2/xfeatures2d.hpp>
+
+using std::vector;
+using std::tuple;
+
+using namespace cv;
+using namespace cv::sfm;
+using namespace cv::xfeatures2d;
+using ftl::registration::CalibrationChessboard;
+
+CalibrationChessboard::CalibrationChessboard(ftl::Configurable *root) {
+	pattern_size_ = Size(9, 6);
+	image_size_ = Size(1280, 720);
+	pattern_square_size_ = 36.0;//0.036;
+	// CALIB_CB_NORMALIZE_IMAfE | CALIB_CB_EXHAUSTIVE | CALIB_CB_ACCURACY 
+	chessboard_flags_ = cv::CALIB_CB_NORMALIZE_IMAGE | cv::CALIB_CB_ACCURACY | cv::CALIB_CB_EXHAUSTIVE;
+}
+
+void CalibrationChessboard::objectPoints(vector<Vec3f> &out) {
+	out.reserve(pattern_size_.width * pattern_size_.height);
+	for (int row = 0; row < pattern_size_.height; ++row) {
+	for (int col = 0; col < pattern_size_.width; ++col) {
+		out.push_back(Vec3f(col * pattern_square_size_, row * pattern_square_size_, 0));
+	}}
+}
+
+bool CalibrationChessboard::findPoints(Mat &img, vector<Vec2f> &points) {
+	return cv::findChessboardCornersSB(img, pattern_size_, points, chessboard_flags_);
+}
+
+void CalibrationChessboard::drawPoints(Mat &img, const vector<Vec2f> &points) {
+	cv::drawChessboardCorners(img, pattern_size_, points, true);
+}
+
+static void get_tracks(const vector<vector<DMatch>> &matches, const vector<KeyPoint> &kp1,
+		const vector<KeyPoint> &kp2, vector<vector<Vec2d>> &tracks, int k) {
+		
+	for (size_t i=0; i<matches.size(); i++) {
+		// TODO Generalise to any number of keypoint frames
+		if (k >= matches[i].size()) continue;
+		Point2f point1 = kp1[matches[i][k].queryIdx].pt;
+    	Point2f point2 = kp2[matches[i][k].trainIdx].pt;
+    	
+    	vector<Vec2d> track;
+    	track.push_back(Vec2d(point1.x,point1.y));
+    	track.push_back(Vec2d(point2.x,point2.y));
+    	tracks.push_back(track);
+    }
+}
+
+static void get_tracks(const vector<DMatch> &matches, const vector<KeyPoint> &kp1,
+		const vector<KeyPoint> &kp2, vector<vector<Vec2d>> &tracks) {
+		
+	for (size_t i=0; i<matches.size(); i++) {
+		// TODO Generalise to any number of keypoint frames
+		//if (k >= matches[i].size()) continue;
+		Point2f point1 = kp1[matches[i].queryIdx].pt;
+    	Point2f point2 = kp2[matches[i].trainIdx].pt;
+    	
+    	vector<Vec2d> track;
+    	track.push_back(Vec2d(point1.x,point1.y));
+    	track.push_back(Vec2d(point2.x,point2.y));
+    	tracks.push_back(track);
+    }
+}
+
+static void convert_tracks(const vector<vector<Vec2d>> &tracks, vector<tuple<int,int,int,int>> &points2d) {
+	//int n_frames = 2;
+	int n_tracks = tracks.size();
+	
+	//for (int i = 0; i < n_frames; ++i)
+	//{
+		//Mat_<double> frame(2, n_tracks);
+		for (int j = 0; j < n_tracks; ++j)
+		{
+			//frame(0,j) = tracks[j][i][0];
+			//frame(1,j) = tracks[j][i][1];
+			points2d.push_back(std::make_tuple(tracks[j][0][0], tracks[j][0][1], tracks[j][1][0], tracks[j][1][1]));
+		}
+	//}
+}
+
+bool ftl::registration::featuresChess(cv::Mat &frame1, cv::Mat &frame2, std::vector<std::tuple<int,int,int,int>> &points) {
+	CalibrationChessboard cb(nullptr);
+
+	vector<Vec2f> points1, points2;
+	if (!cb.findPoints(frame1, points1)) {
+		LOG(ERROR) << "Could not find chessboard (1)";
+		return false;
+	}
+	if (!cb.findPoints(frame2, points2)) {
+		LOG(ERROR) << "Could not find chessboard (2)";
+		return false;
+	}
+
+	if (points1.size() != points2.size()) return false;
+
+	for (size_t i=0; i<points1.size(); i++) {
+		points.push_back(std::make_tuple(points1[i][0], points1[i][1], points2[i][0], points2[i][1]));
+	}
+	return true;
+}
+
+bool ftl::registration::featuresSIFT(cv::Mat &frame1, cv::Mat &frame2, std::vector<std::tuple<int,int,int,int>> &points, int K) {
+	int minHessian = 400;
+	Ptr<SIFT> detector = SIFT::create();
+	//detector->setHessianThreshold(minHessian);
+
+	vector<vector<KeyPoint>> keypoints;
+	vector<Mat> descriptors;
+	
+	{
+		vector<KeyPoint> kps;
+		Mat desc;
+		detector->detectAndCompute(frame1, Mat(), kps, desc);
+		keypoints.push_back(kps);
+		descriptors.push_back(desc);
+	}
+
+	{
+		vector<KeyPoint> kps;
+		Mat desc;
+		detector->detectAndCompute(frame2, Mat(), kps, desc);
+		keypoints.push_back(kps);
+		descriptors.push_back(desc);
+	}
+	
+	// TODO This can be done on node machines...
+	
+	//-- Step 2: Matching descriptor vectors using FLANN matcher
+	// Match between each sequential pair of images in the feed.
+	FlannBasedMatcher matcher;
+	std::vector<std::vector< DMatch >> matches;
+	matcher.knnMatch( descriptors[0], descriptors[1], matches, K );
+	
+	const float ratio_thresh = 0.7f;
+    std::vector<DMatch> good_matches;
+    for (size_t i = 0; i < matches.size(); i++)
+    {
+		for (int k=0; k<K-1; k++) {
+			if (matches[i][k].distance < ratio_thresh * matches[i][k+1].distance)
+			{
+				good_matches.push_back(matches[i][k]);
+			} else break;
+		}
+    }
+	
+	// --- SFM ---
+
+	//for (int i=0; i<K; i++) {
+		vector<vector<Vec2d>> tracks;
+		get_tracks(good_matches, keypoints[0], keypoints[1], tracks);
+		convert_tracks(tracks, points);
+	//}
+
+	//cout << "Tracks: " << tracks.size() << endl;
+	return true;
+}
diff --git a/applications/registration/src/sfm.hpp b/applications/registration/src/sfm.hpp
new file mode 100644
index 000000000..97e27da10
--- /dev/null
+++ b/applications/registration/src/sfm.hpp
@@ -0,0 +1,44 @@
+#ifndef _FTL_REGISTRATION_SFM_HPP_
+#define _FTL_REGISTRATION_SFM_HPP_
+
+#include <ftl/configurable.hpp>
+#include <opencv2/opencv.hpp>
+#include <vector>
+#include <tuple>
+
+namespace ftl {
+namespace registration {
+
+/**
+ * @brief	Chessboard calibration pattern. Uses OpenCV's
+ * 			findChessboardCornersSB function.
+ * @todo	Parameters hardcoded in constructor
+ *
+ * All parameters:
+ * 	- pattern size (inner corners)
+ * 	- square size, millimeters (TODO: meters)
+ * 	- image size, pixels
+ * 	- flags, see ChessboardCornersSB documentation
+ */
+class CalibrationChessboard {
+public:
+	CalibrationChessboard(ftl::Configurable *root);
+	void objectPoints(std::vector<cv::Vec3f> &out);
+	bool findPoints(cv::Mat &in, std::vector<cv::Vec2f> &out);
+	void drawPoints(cv::Mat &img, const std::vector<cv::Vec2f> &points);
+
+private:
+	int chessboard_flags_ = 0;
+	float pattern_square_size_;
+	cv::Size pattern_size_;
+	cv::Size image_size_;
+};
+
+bool featuresSIFT(cv::Mat &frame1, cv::Mat &frame2, std::vector<std::tuple<int,int,int,int>> &points, int);
+
+bool featuresChess(cv::Mat &frame1, cv::Mat &frame2, std::vector<std::tuple<int,int,int,int>> &points);
+
+}
+}
+
+#endif  // _FTL_REGISTRATION_SFM_HPP_
diff --git a/components/rgbd-sources/src/local.cpp b/components/rgbd-sources/src/local.cpp
index 63257bad5..a0d6334b3 100644
--- a/components/rgbd-sources/src/local.cpp
+++ b/components/rgbd-sources/src/local.cpp
@@ -11,6 +11,7 @@
 #include "local.hpp"
 #include <opencv2/core.hpp>
 #include <opencv2/opencv.hpp>
+#include <opencv2/xphoto.hpp>
 
 using ftl::rgbd::detail::LocalSource;
 using cv::Mat;
@@ -269,6 +270,11 @@ bool LocalSource::get(cv::Mat &l, cv::Mat &r) {
 				0, 0, cv::INTER_LINEAR);
 	}
 
+	cv::Ptr<cv::xphoto::WhiteBalancer> wb;
+	wb = cv::xphoto::createSimpleWB();
+	wb->balanceWhite(l, l);
+	wb->balanceWhite(r, r);
+
 	if (flip_v_) {
 		Mat tl, tr;
 		cv::flip(l, tl, 0);
-- 
GitLab