视觉SLAM十四讲——ch7

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

视觉SLAM十四讲——ch7

ch7视觉里程计

本章目标:

1.理解图像特征点的意义,并掌握在单副图像中提取出特征点及多副图像中匹配特征点的方法
2.理解对极几何的原理,利用对极几何的约束,恢复出图像之间的摄像机的三维运动
3.理解PNP问题,以及利用已知三维结构与图像的对应关系求解摄像机的三维运动
4.理解ICP问题,以及利用点云的匹配关系求解摄像机的三维运动
5.理解如何通过三角化获得二维图像上对应点的三维结构

本章目的:基于特征点法的vo,将介绍什么是特征点,如何提取和匹配特征点,以及如何根据配对的特征点估计运动和场景结构,从而实现两帧间视觉里程计。

1 特征点

角点、SIFT(尺度不变特征变换)、SURF、ORB(后三者为人为设计,具有更多优点)。

1.1 特征点的组成

1 关键点:指特征点在图像里的位置

2 描述子:通常是一个向量,按照某种人为设计的方式,描述了该关键点周围像素的信息。相似的特征应该有相似的描述子(即当两个特征点的描述子在向量空间上的距离相近,认为这两个特征点是一样的)。

1.2 ORB特征

ORB特征:OrientedFAST关键点+BRIEF描述子。提取ORB特征的步骤:

1 .2.1提取FAST角点

找出图中的角点,计算特征点的主方向,为后续BRIEF描述子增加了旋转不变特性。(FAST角点主要检测局部像素灰度变化明显的地方)。它的检测过程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C6zkqAV4-1647311685760)(视觉SLAM十四讲——ch7.assets/14d5abad89ab841faeebd7534c42e42-16462760426531.png)]

特点:速度快
缺点: 1).FAST特征点数量很大且不确定,但是我们希望对图像提取固定数量的特征
2).FAST角点不具有方向信息,并且存在尺度问题

解决方式:1).指定要提取的角点数量N,对原始FAST角点分别计算Harris响应值,然后选取前N个具有最大响应值的角点作为最终的角点集合
2).添加尺度和旋转的描述

​ 尺度不变性的实现:构建图像金字塔,并在金字塔的每一层上检测角点(金字塔:指对图像进行不同层次的降采样,以获得不同分辨率的图像)
​ 特征旋转的实现:灰度质心法(质心:指以图像块灰度值作为权重的中心)

1.2.2.计算BRIEF描述子

对前一步提取出的特征点周围图像区域进行扫描
特点:使用随机选点的比较,速度非常快,由于使用了二进制表达,存储起来也十分方便,适用于实时的图像匹配。

在不同图像之间进行特征匹配的方法:
1.暴力匹配:浮点类型的描述子,使用欧式距离度量
二进制类型的描述子(比如本例中的BRIEF描述子),使用汉明距离度量
缺点:当特征点数量很大时,暴力匹配法的运算量会变得很大

2.快速近似最近邻(FLANN):适合匹配特征点数量极多的情况。

1.2.3 图像特征提取代码详解
#include<iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/features2d/features2d.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<chrono>

using namespace std;
using namespace cv;

int main(int argc, char **argv)
{
   
   /*
    if (argc != 3)
    {
        cout <<"usage:feature_extraction img1 img2 "<< endl;
        return 1;
    }
    */
    //读取图像
    Mat img_1 = imread("/home/tgc/slambook2/CH7/1.png",1);
    Mat img_2 = imread("/home/tgc/slambook2/CH7/2.png",1);

    //断言,用于代码的检查
    assert(img_1.data != nullptr && img_2.data != nullptr); 
    
    //初始化
    std::vector<KeyPoint> keypoint_1, keypoint_2; //声明关键点名字和类型
    Mat description_1, description_2;//声明描述子的名字和类型
    Ptr<FeatureDetector> detector = ORB::create();  //ORB创建关键点类
    Ptr<DescriptorExtractor> descriptor = ORB::create();//ORB创建描述子类
    Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming");//用于匹配关键点描述符的抽象基类

    //第一步,检测oriented fast角点位置
    chrono::steady_clock::time_point t1 = chrono::steady_clock::now();
    detector->detect(img_1, keypoint_1);
    detector->detect(img_2, keypoint_2);

    //第二步,根据角点位置计算brief描述子
    descriptor->compute(img_1, keypoint_1, description_1);
    descriptor->compute(img_2, keypoint_2, description_2);
    chrono::steady_clock::time_point t2 = chrono::steady_clock::now();
    chrono::duration<double> time_used = chrono::duration_cast<chrono::duration<double>>(t2-t1);
    cout<<"time cost:"<< time_used.count()<<endl;

    Mat outimg1;
    drawKeypoints(img_1,keypoint_1,outimg1,Scalar::all(-1),DrawMatchesFlags::DEFAULT);
    imshow("orb_features",outimg1);

    //第三步。对两幅图像中的brief描述子进行匹配,使用hamming距离
    vector<DMatch> matches;
    chrono::steady_clock::time_point t3 = chrono::steady_clock::now();
    matcher->match(description_1,description_2,matches);//描述子匹配,
    chrono::steady_clock::time_point t4 = chrono::steady_clock::now();
    chrono::duration<double> time_used_match = chrono::duration_cast<chrono::duration<double>>(t4-t3);
    cout<<"match time cost :"<< time_used_match.count()<<endl;

    //第四步,匹配点对筛选
    //计算最小和最大距离
    auto min_max = minmax_element(matches.begin(),matches.end());//指向最小和最大的迭代器,begin返回指向的第一个读/写迭代器
    double min_dist = min_max.first->distance;
    double max_dist = min_max.second->distance;

    printf("--max dist:%f\n",max_dist);
    printf("--min dist:%f\n",min_dist);

    //当描述子之间的距离大于两倍最小的距离时,即认为匹配有误、
    //但有时候最小距离会非常小。设置一个经验值30作为下限
    vector<DMatch> good_match;
    for (int i = 0; i < description_1.rows; i++)
    {
   
        if (matches[i].distance <= max(2* min_dist,30.0))
        {
   
            good_match.push_back(matches[i]);
        }
    }

    //第五步。绘制匹配结果
    Mat img_match;
    Mat img_goodmatch;
    drawMatches(img_1,keypoint_1,img_2,keypoint_2,matches,img_match);
    drawMatches(img_1,keypoint_1,img_2,keypoint_2,good_match,img_goodmatch);
    imshow("match",img_match);
    imshow("good_match",img_goodmatch);
    waitKey(0);
    
    return 0;
    
}

1.2.4 结果分析

图片1:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UecP7jPi-1647311685761)(视觉SLAM十四讲——ch7.assets/1372381-20180727153341182-1525732735-16462766616633.png)]

图片2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gR0Rrr8H-1647311685762)(视觉SLAM十四讲——ch7.assets/1372381-20180727153414203-226154175-16462766676635.png)]

暴力匹配:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V4DLqjl6-1647311685762)(视觉SLAM十四讲——ch7.assets/1372381-20180727153506919-387915385-16462767832818.png)]

设置30为下限的经验值匹配:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-370Tx8F1-1647311685762)(视觉SLAM十四讲——ch7.assets/1372381-20180727153635299-359755985-16462767875569.png)]

结果分析:筛选的依据是汉明距离小于最小距离的两倍,这是工程经验方法,但还是有误匹配,所以后面在估计运动时,需要去除误匹配的算法。

2 2D-2D :对极几何约束

从两张图片中得到一对匹配好的特征点,如果有若干对这样的匹配点,就可以通过二维图像对应的点,计算出两帧之间相机的运动(R、t)。

2.1单目相机——对极几何求解

如果是单目相机:只能得到2D的像素坐标->根据两组2D点估计运动,方法:对极几何

步骤如下: 1).根据配对点的像素位置求出本质矩阵E或基础矩阵F

2).根据E或F求出R,t。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y4DJKzn0-1647311685763)(视觉SLAM十四讲——ch7.assets/c1407a05f0d0e6a9ed05d717a2ab762-164627824431811-164627824612812.png)]

2.1.1本质矩阵E:

1).E在不同尺度下是等价的

2).本质矩阵的内在性质:E的奇异值一定是[σ,σ,0]T的形式

3).由于尺度等价性,E实际上有5个自由度。

​ 求解方法:八点法(Eight-point -algorithm)。求出E后,对其分解->R,t

2.1.2 单应矩阵(Homograohy)H:

描述了两个平面之间的映射关系。如果场景中的特征点都落在同一个平面上(墙、地面等),可通过单应性来估计运动

求解方法:直接线性变换法(Direct Linear Transform)。求出H后,对其分解(分解方法:数值法、解析法)->R,t

2.2 对极约束求解相机运动代码详解

#include<iostream>
#include<opencv2/core/core.hpp>
#include<opencv2/features2d/features2d.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/calib3d/calib3d.hpp>

using namespace std;
using namespace cv;
//声明特征匹配函数
void find_feature_matches(
    const Mat &img_1, const Mat &img_2,
    std::vector<KeyPoint> &keyponts_1,
    std::vector<KeyPoint> &keyponts_2,
    std::vector<DMatch> &matches
);
//声明位姿估计函数
void pose_estimation_2d2d(
    std::vector<KeyPoint> keypoints_1,
    std::vector<KeyPoint> keypoints_2,
    std::vector<DMatch> matches,
    Mat &R, Mat &t
);
//像素坐标公式,公式5.5变换一下就ok
// 像素坐标转相机归一化坐标
Point2d pixel2cam(const Point2d &p, const Mat &K); //p是像素坐标,k是相机内参

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);
  assert(img_1.data && img_2.data && "Can not load images!");
//定义要用到的变量 ,如keypoints_1 matches
  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);//计算旋转和平移
  
  //当然还需要验证以下R和t是否满足对极约束
  //-- 验证E=t^R*scale
  Mat t_x =//t_x是反对称矩阵,公式3.3
    (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;//E=t^R,公式7.10

  //-- 验证对极约束
  //queryIdx是查询图像的描述子和特征点的下标,
  //trainIdx是训练图像的描述子和特征点的下标。
  Mat K = (Mat_<double>(3, 3) << 520.9, 0, 325.1, 0, 521.0, 249.7, 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;//y2.t()是转置矩阵,公式7.8,若d很趋近于零,则说明没啥问题
    cout << "epipolar constraint = " << d << endl;
  }
  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) {
   
  //先初始化,创建咱们要用到的对象
    //定义两个关键点对应的描述子,同时创建检测keypoints的检测器
  Mat descriptors_1, descriptors_2;
  Ptr<FeatureDetector> detector = ORB::create();//keypoints检测器
  Ptr<DescriptorExtractor> descriptor = ORB::create();//描述子提取器
  Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming");//描述子匹配器(方法:暴力匹配)
  //-- 第一步:检测 Oriented FAST 角点位置
  detector->detect(img_1, keypoints_1);//得到图1的关键点(keypoints_1)
  detector->detect(img_2, keypoints_2);//得到图2的关键点(keypoints_2)

  //-- 第二步:根据角点位置计算 BRIEF 描述子
  descriptor->compute(img_1, keypoints_1, descriptors_1);//得到descriptors_1
  descriptor->compute(img_2, keypoints_2, descriptors_2);//得到descriptors_2

  //-- 第三步:对两幅图像中的BRIEF描述子进行匹配,使用 Hamming 距离
  vector<DMatch> match;//暂时存放匹配点,因为后面还要进行筛选
  matcher->match(descriptors_1, descriptors_2, match);

  //-- 第四步:对match进行筛选,得到好的匹配点,把好的匹配点放在matches中
  //先定义两个变量,一个是最大距离,一个是最小距离
  double min_dist = 10000, max_dist = 0;

  //找出所有匹配之间的最小距离和最大距离, 即是最相似的和最不相似的两组点之间的距离
  for (int i = 0; i < descriptors_1.rows; i++) {
   //描述子本质是由 0,1 组成的向量
    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]);
    }
  }
}

Point2d pixel2cam(const Point2d &p, const Mat &k)//p是像素坐标,k是相机内参
{
   
    return Point2d(
        (p.x - k.at<double>(0,2))/ k.at<double>(0,0),//世界坐标系的X
        (p.y - k.at<double>(1,2) )/ k.at<double>(1,1)//世界坐标系的Y,其中Z被归一化
    );
}
//实现位姿估计函数
void pose_estimation_2d2d(std::vector<KeyPoint> keypoints_1, 
                                                            std::vector<KeyPoint> keypoints_2,
                                                            std::vector<DMatch> matches,
                                                            Mat &R, Mat &t)
{
   
    //相机内参
    Mat K =(Mat_<double> (3,3)<<520.9,0,325.1,0,521.0,249.7,0,0,1);

    //把匹配点转换为float点形式,咱们要把关键点的像素点拿出来 ,定义两个容器接受两个图关键点的像素位置
    vector<Point2f> points1;  // 计算本质矩阵和基础矩阵需要浮点类型的数据
    vector<Point2f> points2; 

    for (int i = 0; i < (int)matches.size(); i++)
    {
   
        //queryIdx是图1中匹配的关键点的对应编号
        //trainIdx是图2中匹配的关键点的对应编号
        //pt可以把关键点的像素位置取出来
        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<<"fundenmatal matrix" <<fundamental_matrix<<endl;

    //计算本质矩阵
    Point2d principal_point(325.1, 249.7); //相机光心,//把内参K中的cx ,cy放进一个向量里面 =相机的光心
    double focal_length = 521; // 相机焦距
    //之所以取上面的principal_point、focal_length是因为计算本质矩阵的函数要用
    //得到本质矩阵essential_matrix
    Mat essential_matrix;
    essential_matrix = findEssentialMat(points1,points2,focal_length,principal_point);
    cout<<"essential matrix" <<essential_matrix<<endl;
   
   //计算单应矩阵
   Mat homography_matrix;
   homography_matrix = findHomography(points1,points2,RANSAC,3);
    cout<<"homography matrix" <<homography_matrix<<endl;
    
    //从本质矩阵中恢复旋转和平移信息
    //此函数仅在opencv3中提供
    recoverPose(essential_matrix,points1,points2,R,t,focal_length,principal_point);
    cout<< "R is"<< R << endl;
    cout<< "t is"<< t << endl;
}

2.3 结果分析

-- Max dist : 95.000000 
-- Min dist : 4.000000 
一共找到了79组匹配点
fundamental_matrix is 
[4.844484382466111e-06, 0.0001222601840188731, -0.01786737827487386;
 -0.0001174326832719333, 2.122888800459598e-05, -0.01775877156212593;
 0.01799658210895528, 0.008143605989020664, 1]
essential_matrix is 
[-0.02036185505234771, -0.4007110038118444, -0.033240742498241;
 0.3939270778216368, -0.03506401846698084, 0.5857110303721015;
 -0.006788487241438231, -0.5815434272915687, -0.01438258684486259]
homography_matrix is 
[0.9497129583105288, -0.143556453147626, 31.20121878625771;
 0.04154536627445031, 0.9715568969832015, 5.306887618807696;
 -2.81813676978796e-05, 4.353702039810921e-05, 1]
R is 
[0.9985961798781877, -0.05169917220143662, 0.01152671359827862;
 0.05139607508976053, 0.9983603445075083, 0.02520051547522452;
 -0.01281065954813537, -0.02457271064688494, 0.9996159607036126]
t is 
[-0.8220841067933339;
 -0.0326974270640541;
 0.5684264241053518]
t^R=
[-0.02879601157010514, -0.5666909361828475, -0.0470095088643642;
 0.5570970160413605, -0.04958801046730488, 0.8283204827837457;
 -0.009600370724838811, -0.8224266019846685, -0.02034004937801358]
epipolar constraint = [0.002528128704106514]
epipolar constraint = [-0.001663727901710814]
epipolar constraint = [-0.0008009088410885212]
epipolar constraint = [0.0001705869410470254]
epipolar constraint = [-0.0003338015008984979]
epipolar constraint = [0.0003385525272308065]
epipolar constraint = [0.0001729349818584552]
epipolar constraint = [-9.552408320477601e-06]
epipolar constraint = [-0.0008834408754688165]
epipolar constraint = [-0.000444586092781381]
epipolar constraint = [-0.000156067923593961]
epipolar constraint = [0.001512967129777068]
epipolar constraint = [-0.0002644334828964742]
epipolar constraint = [-3.514414351252562e-06]
epipolar constraint = [-0.0004170632811044614]
epipolar constraint = [-0.0007749589896892117]
epipolar constraint = [0.002091463454860276]
epipolar constraint = [-0.001016195254389909]
epipolar constraint = [0.0005870797511206075]
epipolar constraint = [0.0002701337927295891]
epipolar constraint = [-0.0008153290073634545]
epipolar constraint = [0.005595329855570208]
epipolar constraint = [0.004456949384590653]
epipolar constraint = [-0.00109225844630996]
epipolar constraint = [-0.00122027527435923]
epipolar constraint = [0.0001206258081121597]
epipolar constraint = [-0.0007167735646266809]
epipolar constraint = [0.002034045481378033]
epipolar constraint = [-0.001256283161205782]
epipolar constraint = [-2.841028832301085e-06]
epipolar constraint = [-0.0009452266349239957]
epipolar co
视觉SLAM十四》的第七章主要介绍了ORB特征的手写实现。ORB特征是一种基于FAST角点检测和BRIEF描述子的特征提取方法,它在计算效率和鲁棒性上表现出色,被广泛应用于视觉SLAM中。 第七章还介绍了ORB特征的主要步骤,包括角点检测、特征描述子计算和特征匹配。在角点检测中,通过FAST算法检测图像中的角点位置。然后,利用BRIEF描述子计算对应角点位置的特征描述子。最后,通过特征匹配算法将当前帧的ORB特征与地图中的ORB特征进行匹配,从而实现相机的位姿估计和地图构建。 除了手写ORB特征的实现,第七章还介绍了ORB-SLAM系统的整体框架和关键技术。该系统结合了特征点法和直接法,实现了在无GPS和IMU信息的情况下进行实时的视觉SLAM。通过利用ORB特征进行初始化、追踪和建图,ORB-SLAM系统在室内和室外环境下都取得了良好的效果。 总而言之,视觉SLAM的第七章《视觉SLAM十四》介绍了手写ORB特征的实现方法,并介绍了ORB-SLAM系统的整体框架和关键技术。这些内容对于理解和应用视觉SLAM具有重要意义。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [视觉SLAM十四——ch7](https://blog.csdn.net/weixin_58021155/article/details/123496372)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [《视觉slam十四》学习笔记——ch7实践部分 比较opencv库下的ORB特征的提取和手写ORB的区别](https://blog.csdn.net/weixin_70026476/article/details/127415318)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值