1.对极几何

从2张图片中,得到若干个配对好的2d特征点,就可以运用对极几何来恢复出两帧之间的运动.

设P的空间坐标为: \(P=[X,Y,Z]^{T}\)

两个像素点\(p_{1},p_{2}\)的像素坐标为: \(s_{1}p_{1}=KP, s_{2}p_{2}=K(RP+t)\)

K为相机内参,R,t为图像1到图像2的旋转矩阵和平移矩阵.

  • 取\(x_{1}=k^{-1}p_{1}, x_{2}=k^{-1}p_{2}\) (x1,x2是两个像素坐标在归一化平面上的坐标)
  • \(x_{2}=Rx_{1}+t\),两侧同时左乘\(x^{T}_{2}\)t^
  • \(x^{T}_{2}\)t</sup>$x_{2}$=$x^{T}_{2}$t<sup>\(Rx_{1}\),等式左边为0
  • \(x^{T}_{2}\)t^\(Rx_{1}=0\)
  • 带入\(p_{1},p_{2}\)得\(p_{2}^{T}K^{-T}\)t^\(RK^{-1}p_{1} = 0\)
  • 取基础矩阵\(F=K^{-T}EK^{-1}\),取本质矩阵\(E=\)t^\(R\)
  • \(x_{2}^{T}Ex_{1} = p_{2}^{T}Fp_{1} = 0\)

相机姿态估计问题变成以下两步:

  • 根据配对点的像素位置求出R或者F
  • 根据E或F求出R,t

2.本质矩阵

根据本质矩阵\(E=\)t^\(R\)定义,这是一个3*3的矩阵,经典是使用8点法来求解.求解出E后,可通过奇异值分解得到相机的运动R和t.

注意:求出的E和t具有尺度一致性,通常把t进行归一化.

3.尺度不确定性

对t的长度归一化,直接导致单目视觉的尺度不确定性.解决办法可以通过SLAM的初始化来解决,初始时,使机器人平移一段距离,然后以此距离作为平移的单位.初始化之后,就可以使用3D-2D来计算相机运动了

工程中,通常匹配的点比较多,这时可以通过构造最小二乘法来进行求解E,但是由于存在误匹配的情况,所以更多的是使用随机采样一致性(RANSAC)来求解

4.三角测距来测量深度

根据对极几何的定义,\(x_{1},x_{2}\)为两个特征点归一化的坐标,则满足:

  • \(s_{1}x_{1}=s_{2}Rx_{2}+t\),两边同时左乘\(x_{1}\)^
  • \(s_{2}\)\(x_{1}\)</sup>$Rx_{2}+$$x_{1}$<sup>t = 0
  • 其中R和t在上面已经求出,故该式为\(s_{2}的\)方程.
  • 由于噪声存在,通常可以使用最小二乘法来求解\(s_{2}\),从而\(s_{1}\)也能求出
#include <iostream>
#include <opencv2/opencv.hpp>
// #include "extra.h" // used in opencv2
using namespace std;
using namespace cv; 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); void pose_estimation_2d2d(
const std::vector<KeyPoint> &keypoints_1,
const std::vector<KeyPoint> &keypoints_2,
const std::vector<DMatch> &matches,
Mat &R, Mat &t); void triangulation(
const vector<KeyPoint> &keypoint_1,
const vector<KeyPoint> &keypoint_2,
const std::vector<DMatch> &matches,
const Mat &R, const Mat &t,
vector<Point3d> &points
); /// 作图用
inline cv::Scalar get_color(float depth) {
float up_th = 50, low_th = 10, th_range = up_th - low_th;
if (depth > up_th) depth = up_th;
if (depth < low_th) depth = low_th;
return cv::Scalar(255 * depth / th_range, 0, 255 * (1 - depth / th_range));
} // 像素坐标转相机归一化坐标
Point2f pixel2cam(const Point2d &p, const Mat &K); int main(int argc, char **argv) {
if (argc != 3) {
cout << "usage: triangulation 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); //-- 三角化
vector<Point3d> points;
triangulation(keypoints_1, keypoints_2, matches, R, t, points); //-- 验证三角化点与特征点的重投影关系
Mat K = (Mat_<double>(3, 3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1);
Mat img1_plot = img_1.clone();
Mat img2_plot = img_2.clone();
for (int i = 0; i < matches.size(); i++) {
// 第一个图
float depth1 = points[i].z;
cout << "depth: " << depth1 << endl;
Point2d pt1_cam = pixel2cam(keypoints_1[matches[i].queryIdx].pt, K);
cv::circle(img1_plot, keypoints_1[matches[i].queryIdx].pt, 2, get_color(depth1), 2); // 第二个图
Mat pt2_trans = R * (Mat_<double>(3, 1) << points[i].x, points[i].y, points[i].z) + t;
float depth2 = pt2_trans.at<double>(2, 0);
cv::circle(img2_plot, keypoints_2[matches[i].trainIdx].pt, 2, get_color(depth2), 2);
}
cv::imshow("img 1", img1_plot);
cv::imshow("img 2", img2_plot);
cv::waitKey(); return 0;
} 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;
// used in OpenCV3
Ptr<FeatureDetector> detector = ORB::create();
Ptr<DescriptorExtractor> descriptor = ORB::create();
// use this if you are in OpenCV2
// Ptr<FeatureDetector> detector = FeatureDetector::create ( "ORB" );
// Ptr<DescriptorExtractor> descriptor = DescriptorExtractor::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;
// BFMatcher matcher ( NORM_HAMMING );
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]);
}
}
} void pose_estimation_2d2d(
const std::vector<KeyPoint> &keypoints_1,
const std::vector<KeyPoint> &keypoints_2,
const std::vector<DMatch> &matches,
Mat &R, Mat &t) {
// 相机内参,TUM Freiburg2
Mat K = (Mat_<double>(3, 3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 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);
} //-- 计算本质矩阵
Point2d principal_point(325.1, 249.7); //相机主点, TUM dataset标定值
int focal_length = 521; //相机焦距, TUM dataset标定值
Mat essential_matrix;
essential_matrix = findEssentialMat(points1, points2, focal_length, principal_point); //-- 从本质矩阵中恢复旋转和平移信息.
recoverPose(essential_matrix, points1, points2, R, t, focal_length, principal_point);
} void triangulation(
const vector<KeyPoint> &keypoint_1,
const vector<KeyPoint> &keypoint_2,
const std::vector<DMatch> &matches,
const Mat &R, const Mat &t,
vector<Point3d> &points) {
Mat T1 = (Mat_<float>(3, 4) <<
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0);
Mat T2 = (Mat_<float>(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) << 520.9, 0, 325.1, 0, 521.0, 249.7, 0, 0, 1);
vector<Point2f> pts_1, pts_2;
for (DMatch m:matches) {
// 将像素坐标转换至相机坐标
pts_1.push_back(pixel2cam(keypoint_1[m.queryIdx].pt, K));
pts_2.push_back(pixel2cam(keypoint_2[m.trainIdx].pt, K));
} Mat pts_4d;
//得到深度点,为4维齐次方程
//输入是两个图片的位姿,以及特征点在两个相机中的坐标,归一化坐标
//输出是第一个图片的特征点在相机中的坐标
cv::triangulatePoints(T1, T2, pts_1, pts_2, pts_4d); // 转换成非齐次坐标
for (int i = 0; i < pts_4d.cols; i++) {
Mat x = pts_4d.col(i); //取列信息
x /= x.at<float>(3, 0); // 归一化
Point3d p(
x.at<float>(0, 0), //得到非齐次的3D点
x.at<float>(1, 0),
x.at<float>(2, 0)
);
points.push_back(p);
}
} Point2f pixel2cam(const Point2d &p, const Mat &K) {
return Point2f
(
(p.x - K.at<double>(0, 2)) / K.at<double>(0, 0),
(p.y - K.at<double>(1, 2)) / K.at<double>(1, 1)
);
}

CMakeLists.txt:

cmake_minimum_required(VERSION 2.8)
project(orb) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
include_directories(inc)
aux_source_directory(src DIR_SRCS)
SET(SOUR_FILE ${DIR_SRCS})
find_package(OpenCV 3 REQUIRED)
find_package(G2O REQUIRED)
find_package(Sophus REQUIRED) include_directories(
${OpenCV_INCLUDE_DIRS}
${G2O_INCLUDE_DIRS}
${Sophus_INCLUDE_DIRS}
"/usr/include/eigen3/"
) add_executable(orb ${SOUR_FILE})
target_link_libraries(orb ${OpenCV_LIBS})

视觉十四讲:第七讲_2D-2D:对极几何估计姿态的更多相关文章

  1. 视觉slam十四讲第七章课后习题6

    版权声明:本文为博主原创文章,转载请注明出处: http://www.cnblogs.com/newneul/p/8545450.html 6.在PnP优化中,将第一个相机的观测也考虑进来,程序应如何 ...

  2. 视觉slam十四讲第七章课后习题7

    版权声明:本文为博主原创文章,转载请注明出处:http://www.cnblogs.com/newneul/p/8544369.html  7.题目要求:在ICP程序中,将空间点也作为优化变量考虑进来 ...

  3. 视觉slam学习之路(一)看高翔十四讲所遇到的问题

      目前实验室做机器人,主要分三个方向,定位导航,建图,图像识别,之前做的也是做了下Qt上位机,后面又弄红外识别,因为这学期上课也没怎么花时间在项目,然后导师让我们确定一个方向来,便于以后发论文什么. ...

  4. 浅读《视觉SLAM十四讲:从理论到实践》--操作1--初识SLAM

    下载<视觉SLAM十四讲:从理论到实践>源码:https://github.com/gaoxiang12/slambook 第二讲:初识SLAM 2.4.2 Hello SLAM(书本P2 ...

  5. 高翔《视觉SLAM十四讲》从理论到实践

    目录 第1讲 前言:本书讲什么:如何使用本书: 第2讲 初始SLAM:引子-小萝卜的例子:经典视觉SLAM框架:SLAM问题的数学表述:实践-编程基础: 第3讲 三维空间刚体运动 旋转矩阵:实践-Ei ...

  6. 高博-《视觉SLAM十四讲》

    0 讲座 (1)SLAM定义 对比雷达传感器和视觉传感器的优缺点(主要介绍视觉SLAM) 单目:不知道尺度信息 双目:知道尺度信息,但测量范围根据预定的基线相关 RGBD:知道深度信息,但是深度信息对 ...

  7. 《视觉SLAM十四讲》第2讲

    目录 一 视觉SLAM中的传感器 二 经典视觉SLAM框架 三 SLAM问题的数学表述 注:原创不易,转载请务必注明原作者和出处,感谢支持! 本讲主要内容: (1) 视觉SLAM中的传感器 (2) 经 ...

  8. 《视觉SLAM十四讲》第1讲

    目录 一 视觉SLAM 注:原创不易,转载请务必注明原作者和出处,感谢支持! 一 视觉SLAM 什么是视觉SLAM? SLAM是Simultaneous Localization and Mappin ...

  9. 视觉SLAM十四讲:从理论到实践 两版 PDF和源码

    视觉SLAM十四讲:从理论到实践 第一版电子版PDF 链接:https://pan.baidu.com/s/1SuuSpavo_fj7xqTYtgHBfw提取码:lr4t 源码github链接:htt ...

  10. 视觉slam十四讲个人理解(ch7视觉里程计1)

    参考博文::https://blog.csdn.net/david_han008/article/details/53560736 https://blog.csdn.net/n66040927/ar ...

随机推荐

  1. Java-(array)数组的基本概念 及 Java内存划分

    (array)数组的基本概念 数组的概念:是一种容器,可同时存放多个数据值 数组的特点: 1.数组是一种引用数据类型 2.数组当中的多个数据,类型必须统一 3.数组的长度在程序运行期间不可改变 数组的 ...

  2. Centos7 mysql网络源安装范例(其他系统也可参考)

    1. 以下是一个通配的el7系列的yum源,可适应aarch64,x86_64,i386内核,但是可能会慢一点 # cat > /etc/yum.repos.d/mysql-community. ...

  3. 云原生之旅 - 14)遵循 GitOps 实践的好工具 ArgoCD

    前言 Argo CD 是一款基于 kubernetes 的声明式的Gitops 持续部署工具. 应用程序定义.配置和环境都是声明式的,并受版本控制 应用程序部署和生命周期管理都是自动化的.可审计的,并 ...

  4. 树莓派(香橙派)通过.NET IoT 操作SPI编写屏幕驱动 顺手做个四足机器人(一)

    摘要 这片文章主要是记录自己的整活过程,涉及到的技术包括.NET IoT, .NET Web, .NET MAUI,框架采用的也是最新的.NET 7. 本人是用的树莓派Zero 2 W(ubuntu- ...

  5. vue3.0使用tui.image-editor图片编辑组件报错TypeError: Cannot convert undefined or null to object

    在vue3.0的项目中使用tui.image-editor组件.一直都是报错.查看报错位置发现代码 addEventListener() { Object.keys(this.$listeners). ...

  6. 【PostgreSQL】PG通过SQL语句读取二进制bytea类型并进行二进制和十六进制转换

    1.将二进制编码为十六进制 select encode("AUUID_0",'hex'),"AUUID_0" from wxf_test."ABANK ...

  7. 1分钟理清楚C++类模板和模板类区别

    1.定义区别 类模板和模板类主要关注点是后一个单词. 类模板:主要描述的是模板,这个模板是类的模板.可以理解为一个通用的类,这个类中的数据成员,成员函数的形参类型以及成员函数的返回值类型不用具体的指定 ...

  8. TabControl控件的简单使用-添加tab

    1.首先创建一个MFC对话框框架,在对话框资源上从工具箱中添加上一个Tab Control 控件,根据需要修改一下属性,然后右击控件,为这个控件添加一个变量,将此控件跟一个CTabCtrl类变量绑定在 ...

  9. python之字典(dict)创建与使用

    字典(dict) 在其他语言中被称为哈希映射(hash map)或者相关数组,它是一种大小可变的键值对集,其中的key.value都是python对象. 特别注意: 1.字典中的key不能重复,key ...

  10. nuxt.js实现页面刷新功能

    key 属性Key:String 或者Function key属性赋值到<router-view>,这对于在动态页面和不同路径中进行转换很有用.不同的key会使页面组件重新渲染. 设置ke ...