视觉三维重建,是指使用相机采集的图片、根据相关知识推导目标物体三维信息的过程。这里借鉴《视觉SLAM14讲》中的分类方法,将视觉三维重建分为基于特征点和非提取特征点的重建。本文旨在使用基于特征点的重建方法完成相邻两张图片间的三维重建。
基于特征点的视觉三维重建方法主要包括如下流程:
按照我的理解,上述流程可以概括为如下两部分:求解相机内外参、三维重建。
这里只是进行简单的介绍,并不涉及复杂的原理,有兴趣的可参考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);
}
}
使用的原图:
重建结果:
// 使用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);