SfM两视图三维点云重建--【VS2015+OpenCV3.4+PCL1.8】

概述

视觉三维重建,是指使用相机采集的图片、根据相关知识推导目标物体三维信息的过程。这里借鉴《视觉SLAM14讲》中的分类方法,将视觉三维重建分为基于特征点和非提取特征点的重建。本文旨在使用基于特征点的重建方法完成相邻两张图片间的三维重建。

步骤

基于特征点的视觉三维重建方法主要包括如下流程:

  1. 提取单张图片特征点;
  2. 多张图片间特征点的匹配;
  3. 利用对极几何约束,使用SfM恢复两相机间的三维变换关系;
  4. 使用三角量测法(或称三角化法),重构二维图像对应点的三维点信息。

按照我的理解,上述流程可以概括为如下两部分:求解相机内外参、三维重建。

  • 求解相机内外参:
    提取特征点并进行匹配只是为了组成匹配对,以方便计算基本矩阵F/本质矩阵E(对极几何约束),而计算出矩阵E或F后,即可进行SVD分解得到两相机间的三维变换关系(旋转矩阵R、平移矩阵t)。至此,是为求解相机内外参的过程。当然,也可以使用相机标定的方式获取相机内外参数,但多张重建会比较繁琐。
  • 三维重建
    在已知两图片间变换矩阵的情况下,就可以使用三角化进行三维重建了。重建后的三维点是以第一张图片对应的相机坐标系为世界坐标系。

这里只是进行简单的介绍,并不涉及复杂的原理,有兴趣的可参考14讲中的内容,网上也有许多优秀博文可供参考。

代码

环境:Win10+VS2015+OpenCV3.4+PCL1.8

#include 
#include 

#include  
#include 

#include 
#include 
#include 

using namespace std;
using namespace cv;
using namespace pcl;
using namespace cv::xfeatures2d;

// ratio & symmetry test
void ratioTest(vector<vector<DMatch>> &matches, vector<DMatch> &goodMatches);
void symmetryTest(const vector<DMatch>& matches1, const vector<DMatch>& matches2, vector<DMatch>& symMatches);

// 从匹配对中提取特征点
void fillPoints(vector<DMatch> goodMatches, vector<KeyPoint> keypoints1, vector<KeyPoint> keypoints2, vector<Point2f>& points1, vector<Point2f>& points2);

// 三维重建
void reconstruct(Mat& K, Mat& fundamentalMatrix, vector<Point2f>& points1, vector<Point2f>& points2, vector<Point3f>& points3D, Mat_<double>& R, Mat_<double>& t);

// 获取关键点RGB
void getPointColor(vector<Point2f> points1, Mat BaseImageLeft, vector<Vec3b>& colors);

int main(int argc, char* argv[])
{
	// PCL可视化
	PointCloud<PointXYZRGB>::Ptr cloud(new PointCloud<PointXYZRGB>);
	boost::shared_ptr<visualization::PCLVisualizer> viewer(new visualization::PCLVisualizer("3D viewer")); // 实例化PCLVisualizer对象,窗口命名为3D viewer

	Mat K;			// 内参数矩阵
	Matx34d P, P1;  // 两图片的相机坐标

	// 1.读入图片
	Mat BaseImageLeft = imread(".\\images\\004.png", -1);
	Mat BaseImageRight = imread(".\\images\\006.png", -1);
	if (BaseImageLeft.empty() || BaseImageRight.empty())
	{
		cout << "ERROR! NO IMAGE LOADED!" << endl;
		return -1;
	}
	cout << "processing..." << endl;
	// 2.SIFT提取特征点
	Ptr<Feature2D> detector = xfeatures2d::SIFT::create(0, 3, 0.04, 10);

	vector<KeyPoint> keypoints_1, keypoints_2; // 关键点
	Mat descriptors_1, descriptors_2;		   // 描述符

	detector->detectAndCompute(BaseImageLeft, noArray(), keypoints_1, descriptors_1);
	detector->detectAndCompute(BaseImageRight, noArray(), keypoints_2, descriptors_2);

	// 3.Flann匹配特征点
	vector<vector<DMatch>> matches1, matches2;
	vector<DMatch> goodMatches1, goodMatches2, goodMatches, outMatches;

	FlannBasedMatcher matcher;
	// 1个descriptors_1[i]对应1个matches[i],1个matches[i]对应2个DMatch(2 nearest)
	matcher.knnMatch(descriptors_1, descriptors_2, matches1, 2);// find 2 nearest neighbours, match.size() = query.rowsize()
	matcher.knnMatch(descriptors_2, descriptors_1, matches2, 2);

	// 4.使用Ratio Test和Symmetry Test消除误匹配点,提高重建精度
	ratioTest(matches1, goodMatches1);
	ratioTest(matches2, goodMatches2);

	symmetryTest(goodMatches1, goodMatches2, goodMatches);// Symmetry Test

	// 5.计算基本矩阵F
	Mat fundamentalMatx;
	vector<Point2f> points1, points2;

	if (goodMatches.size() < 30)
	{
		cerr << "ERROR: NOT ENOUGH MATCHES" << endl;
	}
	else
	{
		// 使用RANSAC消除误匹配(保留inliers值为1的特征点)
		fillPoints(goodMatches, keypoints_1, keypoints_2, points1, points2);
		vector<uchar> inliers(points1.size(), 0); // 内点为1,外点为0
		fundamentalMatx = findFundamentalMat(points1, points2, inliers, CV_FM_RANSAC, 3.0, 0.99);

		vector<DMatch>::const_iterator itM = goodMatches.begin();
		for (vector<uchar>::const_iterator itIn = inliers.begin(); itIn != inliers.end(); ++itIn, ++itM)
		{
			if (*itIn) // 若为内点
				outMatches.push_back(*itM); // 用于计算F的最终匹配对
		}
		points1.clear();
		points2.clear();

		// 计算基本矩阵F
		fillPoints(outMatches, keypoints_1, keypoints_2, points1, points2);
		fundamentalMatx = findFundamentalMat(points1, points2, CV_FM_8POINT);
	}

	// 6.三角化三维重建
	K = (Mat_<double>(3, 3) << 2759.48, 0, 1520.69, 0, 2764.16, 1006.81, 0, 0, 1); // Fountain的内参数矩阵

	vector<Point3f> points3D;
	Mat_<double> R, t; // 用于显示坐标轴的位置
	reconstruct(K, fundamentalMatx, points1, points2, points3D, R, t);

	cout << "\t3D Reconstruction was Finished." << endl;

	// 7.可视化
	vector<Vec3b> points_Colors;
	getPointColor(points1, BaseImageLeft, points_Colors);

	for (size_t i = 0; i < points3D.size(); i++)
	{
		PointXYZRGB p;
		p.x = points3D[i].x;
		p.y = points3D[i].y;
		p.z = points3D[i].z;

		p.b = points_Colors[i][0];
		p.g = points_Colors[i][1];
		p.r = points_Colors[i][2];

		cloud->points.push_back(p);
	}
	// 在每个相机处显示坐标轴
	// cam1(世界坐标系)
	Eigen::Matrix4f transformationMatrix;
	transformationMatrix <<
		1, 0, 0, 0,
		0, 1, 0, 0,
		0, 0, 1, 0,
		0, 0, 0, 1;

	Eigen::Affine3f postion1;
	postion1.matrix() = transformationMatrix.inverse();

	// cam2
	transformationMatrix <<
		R(0, 0), R(0, 1), R(0, 2), t(0),
		R(1, 0), R(1, 1), R(1, 2), t(1),
		R(2, 0), R(2, 1), R(2, 2), t(2),
		0, 0, 0, 1;
	Eigen::Affine3f postion2;
	postion2.matrix() = transformationMatrix.inverse();

	viewer->addCoordinateSystem(0.4, postion1);
	viewer->addCoordinateSystem(0.4, postion2);

	viewer->setBackgroundColor(0, 0, 0); // 设置背景颜色
	viewer->addPointCloud<PointXYZRGB>(cloud, "sample cloud");
	viewer->setPointCloudRenderingProperties(visualization::PCL_VISUALIZER_OPACITY, 2, "sample cloud");

	while (!viewer->wasStopped()) {
		viewer->spinOnce(100);
		boost::this_thread::sleep(boost::posix_time::microseconds(1000));
	}

	waitKey();
	return 0;
}
// 比率检测
void ratioTest(vector<vector<DMatch>> &matches, vector<DMatch> &goodMatches)
{
	for (vector<vector<DMatch>>::iterator matchIterator = matches.begin(); matchIterator != matches.end(); ++matchIterator)
		if (matchIterator->size() > 1)
			if ((*matchIterator)[0].distance < (*matchIterator)[1].distance *0.8)
				goodMatches.push_back((*matchIterator)[0]);
}
// 对称性检测
void symmetryTest(const vector<DMatch>& matches1, const vector<DMatch>& matches2, vector<DMatch>& symMatches)
{
	symMatches.clear();
	for (vector<DMatch>::const_iterator matchIterator1 = matches1.begin(); matchIterator1 != matches1.end(); ++matchIterator1)
		for (vector<DMatch>::const_iterator matchIterator2 = matches2.begin(); matchIterator2 != matches2.end(); ++matchIterator2)
			if ((*matchIterator1).queryIdx == (*matchIterator2).trainIdx && (*matchIterator1).trainIdx == (*matchIterator2).queryIdx)
				symMatches.push_back(*matchIterator1);
}
// 由匹配对提取特征点对
void fillPoints(vector<DMatch> goodMatches, vector<KeyPoint> keypoints1, vector<KeyPoint> keypoints2, vector<Point2f>& points1, vector<Point2f>& points2)
{
	for (vector<DMatch>::const_iterator it = goodMatches.begin(); it != goodMatches.end(); ++it)
	{
		points1.push_back(keypoints1[it->queryIdx].pt); // Get the position of left keypoints
		points2.push_back(keypoints2[it->trainIdx].pt); // Get the position of right keypoints
	}
}

void reconstruct(Mat& K, Mat& fundamentalMatrix, vector<Point2f>& points1, vector<Point2f>& points2, vector<Point3f>& points3D, Mat_<double>& R, Mat_<double>& t)
{

	Mat_<double> E = K.t() * fundamentalMatrix * K; // E = (K').transpose() * F * K

	// 使用recoverPose解算R、T

	// 根据内参数矩阵获取相机的焦距和光心坐标(主点坐标)
	double focal_length = 0.5 * (K.at<double>(0) + K.at<double>(4));
	Point2d principle_point(K.at<double>(2), K.at<double>(5));

	int pass_count = recoverPose(E, points1, points2, R, t, focal_length, principle_point, noArray());

	// P、P1为外参数矩阵,即相机坐标系到世界坐标系的转换关系
	Mat P = (Mat_<double>(3, 4) << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0); // 视为世界坐标系,投影矩阵为标准型矩阵:[I 0]
	Mat P1 = (Mat_<double>(3, 4) << R(0, 0), R(0, 1), R(0, 2), t(0), R(1, 0), R(1, 1), R(1, 2), t(1), R(2, 0), R(2, 1), R(2, 2), t(2));

	// 归一化,将像素坐标转换到相机坐标
	/*
	方式1:归一化变换矩阵,triangulatePoints()中P为:内参*外参矩阵,points为像素坐标:
	P = K * P
	方式2:归一化坐标,triangulatePoints()中P为外参数矩阵,points为相机坐标
	X = K.inv() * x;
	*/
	P = K*P;
	P1 = K*P1;

	Mat points4D; // 齐次坐标形式:4*N:[x y z w].t()
	triangulatePoints(P, P1, points1, points2, points4D);

	// 转成非齐次坐标
	for (size_t i = 0; i < points4D.cols; i++)
	{
		Mat_<float> c = points4D.col(i);
		c /= c(3);

		Point3f p(c(0), c(1), c(2));
		points3D.push_back(p);
	}
}
// 获取关键点的RGB值
void getPointColor(vector<Point2f> points1, Mat BaseImageLeft, vector<Vec3b>& colors)
{
	colors.resize(points1.size());
	for (size_t i = 0; i < points1.size(); i++)
	{
		Point2f& p = points1[i];
		if (p.x <= BaseImageLeft.cols && p.y <= BaseImageLeft.rows)
			colors[i] = BaseImageLeft.at<Vec3b>(p.y, p.x);
	}
}

结果

使用的原图:

重建结果:

SfM两视图三维点云重建--【VS2015+OpenCV3.4+PCL1.8】_第1张图片 SfM两视图三维点云重建--【VS2015+OpenCV3.4+PCL1.8】_第2张图片

注意事项

  1. 使用任意两张相邻图片进行重建时,内参数K略有不同,但不影响重建效果。但要尽量选择相邻的图片,否则可能会因为匹配的特征点较少而重建失败。
  2. 本文代码在分解本质矩阵E时,使用的是recoverPose(),当然也可以使用SVD分解本质矩阵。但是使用SVD分解会得到4个可能的解,只有选择正确的R、T才能完成重建,否则归一化的坐标之后无法正常显示;而使用recoverPose则能直接得到正确的R、t。
// 使用SVD分解本质矩阵E,
// 但要确认使用4个解中的哪个正确:把任意一点代入四种解中,检测该点在两个相机下的深度,当两相机下深度值均为正时的解,即为正确的解--《视觉SLAM14讲P148》
SVD svd(E, SVD::MODIFY_A);

Matx33d W(0, -1, 0, 1, 0, 0, 0, 0, 1); // equation $9.13 page 258(MVG)
Matx33d Wt(0, 1, 0, -1, 0, 0, 0, 0, 1); // W的转置矩阵

// SVD分解的4个解
Mat_<double> R1 = svd.u * Mat(W) * svd.vt; // equation $9.14 page 258,SVD分解:R=UW(Vt)或R=U(Wt)(Vt)
Mat_<double> R2 = svd.u * Mat(Wt) * svd.vt;

Mat_<double> t1 = svd.u.col(2); // t = U(0, 0, 1).transpose() = u3 page 259
Mat_<double> t2 = -svd.u.col(2);
  • 在使用triangulatePoints()进行三维重建时,有两种归一化方式,即归一化变换矩阵和归一化点坐标,具体操作方法见代码。这两种方式本质是相同的,只不过是归一化的先后顺序而已。

参考

  • OpenCV实现SfM(二):双目三维重建
  • 对极几何-本质矩阵-基本矩阵 --推导本质矩阵时,在左叉乘T之后,左点乘的应该是P’的转置,而不是P’。
  • 完整工程下载:SfM稀疏三维点云重建【VS2015+OpenCV3.4+PLC1.8】–完整工程文件

你可能感兴趣的:(三维重建,SfM三维重建,PCL,Opencv)