之所以把SLAM初始化和三角测量放在一起是因为它们之间有一定的关系,理解了三角测量之后才能理解初始化。
三角测量
三角测量(三角化)的方法来估计地图点的深度。
先来看一些什么是三角测量?三角测量是指通过在两处观察同一个点的夹角,来确定该点的距离。以下图为例:

考虑图像

,相机的光心为

,以左图为参考,右图的变换矩阵为

。假设在

中有特征点

,对应到

中的特征点

,理论上讲

和

会相交于某一点

,该点即是两个特征点所对应的地图点在三维场景中的位置,但是由于噪声的影响,这两条直线往往无法相交,因此可以通过最小二乘法来求解出距离最近的那个点作为相交点。按照对极几何中的定义,如果设

为两个特征点的归一化坐标,于是有下列关系式:

ps: 我对上面这个公式不是很理解或者说看法不一样。

是归一化坐标,所以

可以认为是去归一化的坐标(也就是相机光心为

时的相机坐标),同理

为

点在相机光心为

时的相机坐标,所以我认为是

。但是计算

的思路还是不变的。上面公式中的R、t、

都是知道的,现在要求的就是两个特征点的深度

。我们可以分开来求,首先求

:上式的两边同时左乘

,得到:

等式的左边可以看成是一个方程,只有

是未知的,根据它可以直接求解出

。有了

,反代入也可以很简单的求出

。但是,由于噪声的存在,我们求出来的

不一定能够使得上式精确等于0,因此在实际情况中,更常见的做法是求最小二乘解而不是零解。
实践部分:如何使用OpenCV进行三角测量
下面是进行三角化的简单思路:

下面是用OpenCV进行实现的代码:
triangulation.hpp
#include "feature_extract_match.hpp"
#include "estimation.hpp"
//像素坐标转换为归一化坐标,返回的是(X/Z, Y/Z), 真正的归一化坐标为(X/Z, Y/Z, 1)
Point2d pixel_to_camera(Point2d p, Mat K)
{
return Point2d(
(p.x - K.at<double>(0, 2)) / K.at<double>(0, 0),
(p.y - K.at<double>(1, 2)) / K.at<double>(1, 1)
);
}
//返回的是空间三维点
void triangulation(vector<KeyPoint> key_points_1, vector<KeyPoint> key_points_2, vector<DMatch> matches, Mat R, Mat t, vector<Point3d> &space_points)
{
Mat T1 = (Mat_<double>(3, 4) <<
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0
);
//注意Mat_类的使用
Mat T2 = (Mat_<double> (3, 4) <<
R.at<double>(0, 0), R.at<double>(0, 1), R.at<double>(0, 2), t.at<double>(0, 0),
R.at<double>(1, 0), R.at<double>(1, 1), R.at<double>(1, 2), t.at<double>(1, 0),
R.at<double>(2, 0), R.at<double>(2, 1), R.at<double>(2, 2), t.at<double>(2, 0)
);
//定义相机的内参矩阵
Mat K = (Mat_<double>(3, 3) <<
529.0, 0, 325.1,
0, 521.0, 249.9,
0, 0, 1
);
//将所有匹配的特征点转化为归一化坐标
//KeyPoints ---> Point2f
vector<Point2d> points_1, points_2;
for(int i=0; i< matches.size(); i++)
{
points_1.push_back( pixel_to_camera(key_points_1[matches[i].queryIdx].pt, K) );
points_2.push_back( pixel_to_camera(key_points_2[matches[i].trainIdx].pt, K) );
}
/*
调用cv::triangulatePoints(InputArray projMatr1, InputArray projMatr2, InputArray projPoints1, InputArray projPoints2, OutputArray points4D)进行三角测量
参数1: 第一张图像的[R, t]组成的3*4矩阵
参数2: 第二张图像的[R, t]组成的3*4矩阵
参数3: 第一张图像的匹配的特征点的归一化坐标, 类型为vector<Point2d>
参数4: 第二张图像的匹配的特征点的归一化坐标, 类型为vector<Point2d>
参数5: 输出的3d坐标, 是一个4*N矩阵表示的齐次坐标(每一列都是一个点的坐标), 因此要将所有的元素除以最后一维的数得到非齐次坐标XYZ
*/
Mat pts_4d;
cv::triangulatePoints(T1, T2, points_1, points_2, pts_4d);
// cout << "pts_4d = " << endl;
// cout << pts_4d << endl;
//转换为非齐次坐标
for(int i = 0; i < pts_4d.cols; i++)
{
Mat x = pts_4d.col(i); //获取第i列
x /= x.at<double>(3, 0); //归一化
space_points.push_back(
Point3d(x.at<double>(0, 0), x.at<double>(1, 0), x.at<double>(2, 0))
);
}
}test_triangulation.cpp
#include "feature_extract_match.hpp"
#include "estimation.hpp"
#include "triangulation.hpp"
int main(int argc, char **argv)
{
Mat img1, img2;
img1 = imread("../datas/1.png");
img2 = imread("../datas/2.png");
vector<KeyPoint> key_points_1, key_points_2;
vector<DMatch> matches;
feature_extract_match(img1, img2, key_points_1, key_points_2, matches);
cout << "一共找到了: " << matches.size() << "个匹配点" << endl;
Mat R, t;
pose_estimation_2d2d(key_points_1, key_points_2, matches, R, t);
cout << "R = " << R << endl;
cout << "t = " << t << endl;
//进行三角测量
vector<Point3d> space_points;
triangulation(key_points_1, key_points_2, matches, R, t, space_points);
cout << "space_points.size() = " << space_points.size() << endl;
//内参矩阵
Mat K = (Mat_<double>(3, 3) <<
529.0, 0, 325.1,
0, 521.0, 249.9,
0, 0, 1
);
//验证三角化点和特征点的重投影关系
//验证方法: 三角化点进行归一化, 特征点重投影到归一化平面
for(int i = 0; i < matches.size(); i++)
{
//将像素点投影到归一化平面上
Point2d pix_cam_1 = pixel_to_camera(key_points_1[ matches[i].queryIdx ].pt, K);
//将空间点投影到归一化平面上
Point2d space_cam_1 (
space_points[i].x / space_points[i].z,
space_points[i].y / space_points[i].z
);
cout << "pix_cam_1 in first frame is : " << pix_cam_1 << endl;
cout << "space_cam_1 in first frame is : " << space_cam_1 << ", d = " << space_points[i].z << endl;
//the second image
//像素坐标直接可以转换为归一化坐标
Point2d pix_cam_2 = pixel_to_camera( key_points_2[ matches[i].trainIdx ].pt, K );
//R * 世界坐标 + t ----> I2的相机坐标系下的坐标,然后再投影到归一化平面
Mat point_cam_frame2 = R * (Mat_<double>(3, 1) << space_points[i].x, space_points[i].y, space_points[i].z) + t;
point_cam_frame2 /= point_cam_frame2.at<double>(2, 0); //归一化
// Point2d space_cam_2 (
// point_cam_frame2.at<double>(0, 0),
// point_cam_frame2.at<double>(1, 0)
// );
cout << "pix_cam_2 in two frame is : " << pix_cam_2 << endl;
cout << "space_cam_2 in two frame is : " << point_cam_frame2.t() << endl;
cout << endl;
}
return 0;
}讨论:
由于E(本质矩阵)本身具有尺度等价性,它分解得到的R、t也有一个尺度等价性。而

本身具有约束,所以我们认为t具有一个尺度。换言之,在分解过程中,对t乘以任意非零常数,分解都是成立的。因此,我们通常把t进行归一化,让它的长度等于1.
单目视觉的尺度不确定性。因为对t乘以任意一个比例常数后,对极约束仍然是成立的。换言之,在单目SLAM中,对轨迹和地图同时缩放任意倍数,我们得到的图仍然是一样的。
单目视觉的初始化问题
单目SLAM的初始化。
在初始化之后,就可以用3D-2D来计算相机的运动了,初始化之后的轨迹和地图的单位,就是初始化时固定的尺度。因此,初始化也是单目SLAM中不可避免的一个步骤。
单目初始化不能只有纯旋转,必须要有一定程度的平移,如果没有平移,单目将无法初始化。
这里还要提一个三角化的相互矛盾的地方
在同样的相机分辨率下,平移越大,三角化测量就会越精确;但是平移越大,将会导致图像的外观发生明确变化,这会使得特征的提取和匹配变得更困难---这就是三角化的矛盾。
















