视觉SLAM笔记(33) 对极约束求解相机运动
1. 本质矩阵求解相机运动
通过 Essential 矩阵求解相机运动
视觉SLAM笔记(31) 特征提取和匹配 的程序提供了特征匹配
而这次就使用匹配好的特征点来计算
,
和
进而分解
得到
,
创建/VSLAM_note/033/pose_estimation_2d2d.cpp
文件
2. 特征提取函数
整个程序使用 OpenCV 提供的算法进行求解
把 视觉SLAM笔记(31) 特征提取和匹配 的特征提取封装成函数 find_feature_matches
void find_feature_matches(const Mat& img_1, const Mat& img_2,
std::vector<KeyPoint>& keypoints_1,
std::vector<KeyPoint>& keypoints_2,
std::vector< DMatch >& matches)
{
//-- 初始化
Mat descriptors_1, descriptors_2; // 创建描述子
Ptr<FeatureDetector> detector = ORB::create(); // 创建ORB特征点检测
Ptr<DescriptorExtractor> descriptor = ORB::create(); // 使用ORB特征来描述特征点
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming"); // 设置特征匹配使用汉明距离计算
//-- 第一步:检测 Oriented FAST 角点位置
detector->detect(img_1, keypoints_1);
detector->detect(img_2, keypoints_2);
//-- 第二步:根据角点位置计算 BRIEF 描述子
descriptor->compute(img_1, keypoints_1, descriptors_1);
descriptor->compute(img_2, keypoints_2, descriptors_2);
//-- 第三步:对两幅图像中的BRIEF描述子进行匹配,使用 Hamming 距离
vector<DMatch> match;
matcher->match(descriptors_1, descriptors_2, match);
//-- 第四步:匹配点对筛选
double min_dist = 10000, max_dist = 0;
// 找出所有匹配之间的最小距离和最大距离, 即是最相似的和最不相似的两组点之间的距离
for (int i = 0; i < descriptors_1.rows; i++)
{
double dist = match[i].distance;
if (dist < min_dist) min_dist = dist;
if (dist > max_dist) max_dist = dist;
}
// 显示所有匹配之间的最大距离和最小距离
printf("-- Max dist : %f \n", max_dist);
printf("-- Min dist : %f \n", min_dist);
// 当描述子之间的距离大于两倍的最小距离时,即认为匹配有误
// 但有时候最小距离会非常小,设置一个经验值30作为下限
for (int i = 0; i < descriptors_1.rows; i++)
{
if (match[i].distance <= max(2 * min_dist, 30.0))
{
matches.push_back(match[i]);
}
}
}
以供后面使用
3. 2D-2D姿态估计函数
2D-2D姿态估计函数pose_estimation_2d2d
,提供了从特征点求解相机运动的部分
然后,在主函数中调用它,就能得到相机的运动
void pose_estimation_2d2d(std::vector<KeyPoint> keypoints_1,
std::vector<KeyPoint> keypoints_2,
std::vector< DMatch > matches,
Mat& R, Mat& t)
{
// 相机内参,TUM Freiburg2
double cx = 325.1; // 像素坐标系与成像平面之间的原点平移
double cy = 249.7;
double fx = 520.9; // 焦距
double fy = 521.0;
Mat K = (Mat_<double>(3, 3) << fx, 0, cx, 0, fy, cy, 0, 0, 1);
//-- 把匹配点转换为vector<Point2f>的形式
vector<Point2f> points1;
vector<Point2f> points2;
for (int i = 0; i < (int)matches.size(); i++)
{
points1.push_back(keypoints_1[matches[i].queryIdx].pt);
points2.push_back(keypoints_2[matches[i].trainIdx].pt);
}
//-- 计算基础矩阵
Mat fundamental_matrix;
fundamental_matrix = findFundamentalMat(points1, points2, CV_FM_8POINT);
cout << "fundamental_matrix is " << endl << fundamental_matrix << endl;
//-- 计算本质矩阵
Point2d principal_point(cx, cy); //相机光心, TUM dataset标定值
double focal_length = fy; //相机焦距, TUM dataset标定值
Mat essential_matrix;
essential_matrix = findEssentialMat(points1, points2, focal_length, principal_point);
cout << "essential_matrix is " << endl << essential_matrix << endl;
//-- 计算单应矩阵
Mat homography_matrix;
homography_matrix = findHomography(points1, points2, RANSAC, 3);
cout << "homography_matrix is " << endl << homography_matrix << endl;
//-- 从本质矩阵中恢复旋转和平移矩阵
recoverPose(essential_matrix, points1, points2, R, t, focal_length, principal_point);
cout << "R is " << endl << R << endl;
cout << "t is " << endl << t << endl;
}
在特征提取后得到匹配点
1,
2
根据下列简化对极约束中的第三等式:
通过findFundamentalMat()
函数可以计算出基础矩阵F
再确认内参K,根据第二等式,可通过findEssentialMat
函数得出本质矩阵E
通过findHomography
函数得出单应矩阵H,不过特征点不在一个平面,所以意义不大
最后通过recoverPose
函数从本质矩阵中恢复旋转和平移矩阵
4. 归一化坐标函数
像素坐标转相机归一化坐标函数pixel2cam
Point2d pixel2cam(const Point2d& p, const 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)
);
}
5. 求解相机运动
int main(int argc, char** argv)
{
if (argc != 3)
{
cout << "usage: pose_estimation_2d2d img1 img2" << endl;
return 1;
}
//-- 读取图像
Mat img_1 = imread(argv[1], CV_LOAD_IMAGE_COLOR);
Mat img_2 = imread(argv[2], CV_LOAD_IMAGE_COLOR);
//-- 特征提取
vector<KeyPoint> keypoints_1, keypoints_2;
vector<DMatch> matches;
find_feature_matches(img_1, img_2, keypoints_1, keypoints_2, matches);
cout << "一共找到了" << matches.size() << "组匹配点" << endl;
//-- 估计两张图像间运动
Mat R, t;
pose_estimation_2d2d(keypoints_1, keypoints_2, matches, R, t);
//-- 验证E=t^R*scale
Mat t_x = (Mat_<double>(3, 3) <<
0, -t.at<double>(2, 0), t.at<double>(1, 0),
t.at<double>(2, 0), 0, -t.at<double>(0, 0),
-t.at<double>(1.0), t.at<double>(0, 0), 0);
cout << "t^R=" << endl << t_x * R << endl;
//-- 验证对极约束
// 相机内参,TUM Freiburg2
double cx = 325.1; // 像素坐标系与成像平面之间的原点平移
double cy = 249.7;
double fx = 520.9; // 焦距
double fy = 521.0;
Mat K = (Mat_<double>(3, 3) << fx, 0, cx, 0, fy, cy, 0, 0, 1);
// 计算对极约束
for (DMatch m : matches)
{
Point2d pt1 = pixel2cam(keypoints_1[m.queryIdx].pt, K);
Mat y1 = (Mat_<double>(3, 1) << pt1.x, pt1.y, 1);
Point2d pt2 = pixel2cam(keypoints_2[m.trainIdx].pt, K);
Mat y2 = (Mat_<double>(3, 1) << pt2.x, pt2.y, 1);
Mat d = y2.t() * t_x * R * y1; // 对极约束方程
cout << "epipolar constraint = " << d << endl; // 应该无限接近0
}
return 0;
}
读取图像,然后特征提取,然后估计两张图像间运动
得出R, t
根据 x2 = Rx1 + t 就可以估计现在的位姿
在函数中输出了 E, F 和 H 的数值
接着验证 E=t^ R,通过 t^ R的结果比较
然后验证对极约束是否成立
在程序的输出结果中可以看出,对极约束的满足精度约在 10−3 量级
6. 尺度不确定性
对平移矩阵 t 长度的归一化,直接导致了单目视觉的尺度不确定性(Scale Ambiguity)
例如,程序中输出的 t 第一维约 0.883
这个 0.883 究竟是指 0.883m,还是 0.883cm,是没法确定的
因为对 t 乘以任意比例常数后,对极约束依然是成立的
换言之,在单目 SLAM 中,对轨迹和地图同时缩放任意倍数,得到的图像依然是一样的
这在 视觉SLAM笔记(2) 相机 介绍过了
在单目视觉中,对两张图像的 t 归一化,相当于固定了尺度
虽然不知道它的实际长度为多少,但以这时的 t 为单位 1,计算相机运动和特征点的 3D 位置
这被称为单目 SLAM 的初始化
在初始化之后,就可以用 3D-2D 来计算相机运动了
初始化之后的轨迹和地图的单位,就是初始化时固定的尺度
因此,单目 SLAM 有一步不可避免的初始化
初始化的两张图像必须有一定程度的平移,而后的轨迹和地图都将以此步的平移为单位
除了对 t 进行归一化之外,另一种方法是令初始化时所有的特征点平均深度为 1,也可以固定一个尺度
相比于令 t 长度为 1 的做法,把特征点深度归一化可以控制场景的规模大小,使计算在数值上更稳定些
不过这并没有理论上的差别
7. 多于八对点的情况
当给定的匹配点数多于八对时(例程找到了 81 对匹配),可以计算一个最小二乘解
在 视觉SLAM笔记(32) 2D-2D: 对极几何 中八点法提到线性化后的对极约束
把左侧的系数矩阵记为 A:Ae = 0
对于八点法, A 的大小为 8 × 9
如果给定的匹配点多于 8,该方程构成一个超定方程,即不一定存在 e 使得上式成立
因此,可以通过最小化一个二次型来求:
于是就求出了在最小二乘意义下的 E 矩阵
不过,当可能存在误匹配的情况时
会更倾向于使用 随机采样一致性(Random Sample Concensus,RANSAC) 来求,而不是最小二乘
RANSAC 是一种通用的做法,适用于很多带错误数据的情况,可以处理带有错误匹配的数据
8. 初始化的纯旋转问题
从 E 分解到 R, t 的过程中,如果相机发生的是纯旋转,导致 t 为零
那么,得到的 E 也将为零,这将导致无从求解 R
不过,此时可以依靠 H 求取旋转
但仅有旋转时,无法用三角测量估计特征点的空间位置
于是,另一个结论是, 单目初始化不能只有纯旋转,必须要有一定程度的平移
如果没有平移,单目将无法初始化
在实践当中,如果初始化时平移太小,会使得位姿求解与三角化结果不稳定,从而导致失败
相对的,如果把相机左右移动而不是原地旋转,就容易让单目 SLAM 初始化
因而有经验的 SLAM 研究人员
在单目 SLAM 情况下,经常选择让相机进行左右平移以顺利地进行初始化
参考:
相关推荐:
视觉SLAM笔记(32) 2D-2D: 对极几何
视觉SLAM笔记(31) 特征提取和匹配
视觉SLAM笔记(30) 特征点法
视觉SLAM笔记(29) g2o
视觉SLAM笔记(28) Ceres