双视图bundle adjustment注释程序

本文介绍了在学习高博的SLAM14讲中,针对g2o库和OpenCV库版本升级导致的代码问题进行的修改。作者详细解释了如何调整ORB特征检测和匹配部分,以及g2o优化器构造时的改动。通过这些修改,原有的BA例程现在可以正常运行。此外,作者还展示了代码中添加的匹配图像显示和匹配点数量的返回。最后,文章探讨了BlockSolver_6_3参数含义的理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在学习高博的SLAM14讲,在g2o的部分有一些困惑,便去特别的学习了一下这部分知识,主要参考了以下几篇文章:

1、高博的博客中有一篇讲述g2o的:https://www.cnblogs.com/gaoxiang12/p/5304272.html

2、从零开始一起学习SLAM有三篇文章:总览顶点编程边编程

根据这几篇文章,我又把高博博客中的BA例程跑了一下,并根据我自己的理解添加了注释。

一、代码修改

由于opencv库和g2o库的版本升级,高博的例程不能直接使用,需要进行一定的修改:

1、opencv部分

opencv部分代码在函数寻找匹配点的部分,也就是findCorrespondingPoints()函数,主要是由于版本更新导致Orb变成了一个纯虚类,不能进行实例化了,源代码如下:

    cv::ORB orb;
    vector<cv::KeyPoint> kp1, kp2;
    cv::Mat desp1, desp2;
    orb( img1, cv::Mat(), kp1, desp1 );
    orb( img2, cv::Mat(), kp2, desp2 );
    cout<<"分别找到了"<<kp1.size()<<"和"<<kp2.size()<<"个特征点"<<endl;

    cv::Ptr<cv::DescriptorMatcher>  matcher = cv::DescriptorMatcher::create( "BruteForce-Hamming");

修改后:

    //由于opencv版本升级,orb现在的是一个纯虚类,无法使用原来的方法进行实例化了
    cv::Ptr<cv::FeatureDetector> orbDetector = cv::ORB::create();
    cv::Ptr<cv::DescriptorExtractor> orbExtractor = cv::ORB::create();
    cv::Ptr<cv::DescriptorMatcher> orbMatcher = cv::DescriptorMatcher::create("BruteForce-Hamming");

    vector<cv::KeyPoint> kp1, kp2;//记录关键点的容器
    cv::Mat desp1, desp2;//描述子,我的理解是把mat当矩阵来用了

    orbDetector->detect(img1, kp1);//从img1中提取关键点,并放入kp1中
    orbDetector->detect(img2, kp2);

    orbExtractor->compute(img1, kp1, desp1);//对kp1计算描述子,并放入desp1中
    orbExtractor->compute(img2, kp2, desp2);

    cout <<"分别找到了" << kp1.size() << "和" << kp2.size() <<"个点" << endl;

其次是源代码使用的是knnMatch,而我在这边根据slam14讲上orb的代码改成了match函数,同时将对于好的匹配的判断也相应的进行了修改,不过区别不大。

源码:

    double knn_match_ratio=0.8;
    vector< vector<cv::DMatch> > matches_knn;
    matcher->knnMatch( desp1, desp2, matches_knn, 2 );
    vector< cv::DMatch > matches;
    for ( size_t i=0; i<matches_knn.size(); i++ )
    {
        if (matches_knn[i][0].distance < knn_match_ratio * matches_knn[i][1].distance )
            matches.push_back( matches_knn[i][0] );
    }

修改后:

    vector<cv::DMatch> matches;//orb匹配数据
    vector<cv::DMatch> good_matches;//通过汉明距离筛选后的好的匹配数据

    orbMatcher->match(desp1, desp2, matches);//对于描述子进行匹配

    double mindistance, maxdistance;//用于求取matches的最大最小汉明距离

    for(auto m :matches)//c++11新特性,可以理解为遍历matches
    {
        if(m.distance > maxdistance)
            maxdistance = m.distance;
        if(m.distance < mindistance)
            mindistance = m.distance;
    }

    for(auto m:matches)
    {   //如果汉明距离小于2倍的最小汉明距离或者是30的话就认为是好的匹配
        if(m.distance <= max(2*mindistance, 30.0))
            good_matches.push_back(m);
    }

同时,我也加入了对于匹配图像的显示:

    cv::Mat img_match;
    cv::drawMatches(img1, kp1, img2, kp2, good_matches, img_match);//绘制匹配图像
    cv::imshow("img_match", img_match);//显示匹配图像
    cv::waitKey(10);//等待10ms(不等待不能显示)

在源程序中,这个函数在内部进行了判断匹配点是否过少,并返回布尔值,我直接改成了返回匹配点的个数,并在主函数中对其进行判断:

return good_matches.size();//返回的是匹配个数 

2、g2o部分

在对g2o进行构造时,由于g2o库的更新,线性求解器和块求解器要求使用unique_ptr

BlockSolver(std::unique_ptr<LinearSolverType> linearSolver);

(from: <block_solver.h> line:123):

而源程序使用的是普通指针:

    // 构造g2o中的图
    // 先构造求解器
    g2o::SparseOptimizer    optimizer;
    // 使用Cholmod中的线性方程求解器
    g2o::BlockSolver_6_3::LinearSolverType* linearSolver = new  g2o::LinearSolverCholmod<g2o::BlockSolver_6_3::PoseMatrixType> ();
    // 6*3 的参数
    g2o::BlockSolver_6_3* block_solver = new g2o::BlockSolver_6_3( linearSolver );
    // L-M 下降
    g2o::OptimizationAlgorithmLevenberg* algorithm = new g2o::OptimizationAlgorithmLevenberg( block_solver );

同时由于将unique_ptr作为参数传递给类的构造函数需要使用move指令,所以修改后的程序如下:

    //这一部分基本上固定,除非是选择不同的求解器算法
    g2o::SparseOptimizer optimizer;
    //由于g2o库的更新,线性求解器和块求解器要求使用:unique_ptr (from: <block_solver.h> line:123)
    unique_ptr<g2o::BlockSolver_6_3::LinearSolverType> linearSolver(new g2o::LinearSolverCholmod<g2o::BlockSolver_6_3::PoseMatrixType>());
    //将unique_ptr作为参数传递给类的构造函数需要使用move指令
    unique_ptr<g2o::BlockSolver_6_3> BlockSolver(new g2o::BlockSolver_6_3(move(linearSolver)));
    g2o::OptimizationAlgorithmLevenberg* algorithm = new g2o::OptimizationAlgorithmLevenberg(move(BlockSolver));

至此,代码修改完毕,可以正常使用,后附完整代码。

二、代码及注释

#include <iostream>

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <boost/concept_check.hpp>

#include <g2o/core/sparse_optimizer.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/robust_kernel.h>
#include <g2o/core/robust_kernel_impl.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/solvers/cholmod/linear_solver_cholmod.h>
#include <g2o/types/slam3d/se3quat.h>
#include <g2o/types/sba/types_six_dof_expmap.h>

using namespace std;
//相机内参K矩阵
double cx = 325.5;
double cy = 253.5;
double fx = 5180;
double fy = 519.0;

int findCorrespondingPoints(const cv::Mat& img1,const cv::Mat& img2,vector<cv::Point2f>& pts1,vector<cv::Point2f>& pts2)
{
    //由于opencv版本升级,orb现在的是一个纯虚类,无法使用原来的方法进行实例化了
    cv::Ptr<cv::FeatureDetector> orbDetector = cv::ORB::create();
    cv::Ptr<cv::DescriptorExtractor> orbExtractor = cv::ORB::create();
    cv::Ptr<cv::DescriptorMatcher> orbMatcher = cv::DescriptorMatcher::create("BruteForce-Hamming");

    vector<cv::KeyPoint> kp1, kp2;//记录关键点的容器
    cv::Mat desp1, desp2;//描述子,我的理解是把mat当矩阵来用了

    orbDetector->detect(img1, kp1);//从img1中提取关键点,并放入kp1中
    orbDetector->detect(img2, kp2);

    orbExtractor->compute(img1, kp1, desp1);//对kp1计算描述子,并放入desp1中
    orbExtractor->compute(img2, kp2, desp2);

    cout <<"分别找到了" << kp1.size() << "和" << kp2.size() <<"个点" << endl;

    vector<cv::DMatch> matches;//orb匹配数据
    vector<cv::DMatch> good_matches;//通过汉明距离筛选后的好的匹配数据

    orbMatcher->match(desp1, desp2, matches);//对于描述子进行匹配

    double mindistance, maxdistance;//用于求取matches的最大最小汉明距离

    for(auto m :matches)//c++11新特性,可以理解为遍历matches
    {
        if(m.distance > maxdistance)
            maxdistance = m.distance;
        if(m.distance < mindistance)
            mindistance = m.distance;
    }

    for(auto m:matches)
    {   //如果汉明距离小于2倍的最小汉明距离或者是30的话就认为是好的匹配
        if(m.distance <= max(2*mindistance, 30.0))
            good_matches.push_back(m);
    }

    for(auto m:good_matches)
    {//存储好的匹配的keypoints
        pts1.push_back(kp1[m.queryIdx].pt);
        pts2.push_back(kp2[m.trainIdx].pt);
    }

    cv::Mat img_match;
    cv::drawMatches(img1, kp1, img2, kp2, good_matches, img_match);//绘制匹配图像
    cv::imshow("img_match", img_match);//显示匹配图像
    cv::waitKey(10);//等待10ms(不等待不能显示)

    return good_matches.size();//返回的是匹配个数 
}

int main(int argc, char *argv[])
{
    if(argc != 3)//判断调用参数,使用方法:./ba img1 img2(因为在CMakelist里面将程序设置成了叫ba)
    {
        cout << "Usage: ba img1 img2" << endl;
        return -1;
    }
    //读取img1和img2
    cv::Mat img1 = cv::imread(argv[1]);
    cv::Mat img2 = cv::imread(argv[2]);
    //显示img1和img2
    cv::imshow("img1", img1);
    cv::imshow("img2", img2);

    vector<cv::Point2f>pts1, pts2;//点集

    if( findCorrespondingPoints(img1, img2, pts1, pts2) <= 20)//寻找匹配点
    {
        cout << "匹配点不足!"<< endl;
        return -2;
    }
    cout << "找到了" << pts1.size() << "组特征点" << endl;

    //--------------这一部分基本上固定,除非是选择不同的求解器算法------
    g2o::SparseOptimizer optimizer;
    //由于g2o库的更新,线性求解器和块求解器要求使用:unique_ptr (from: <block_solver.h> line:123)
    unique_ptr<g2o::BlockSolver_6_3::LinearSolverType> linearSolver(new g2o::LinearSolverCholmod<g2o::BlockSolver_6_3::PoseMatrixType>());
    //将unique_ptr作为参数传递给类的构造函数需要使用move指令
    unique_ptr<g2o::BlockSolver_6_3> BlockSolver(new g2o::BlockSolver_6_3(move(linearSolver)));
    g2o::OptimizationAlgorithmLevenberg* algorithm = new g2o::OptimizationAlgorithmLevenberg(move(BlockSolver));

    optimizer.setAlgorithm(algorithm);
    optimizer.setVerbose(false);
    //--------------以上基本上固定-----------------------------------
    //添加位姿顶点
    for(int i=0; i < 2; i++)
    {
        g2o::VertexSE3Expmap* v = new g2o::VertexSE3Expmap();//使用的是g2o内部的定点类型
        v->setId(i);
        if(i == 0)
            v->setFixed(true);//第一幅图视作世界坐标系,设定位姿固定为I,0,不对其进行优化
        v->setEstimate(g2o::SE3Quat());//预设值,R为I,t为0
        optimizer.addVertex(v);//图中加入顶点
    }
    //添加特征点顶点
    for(size_t i=0; i < pts1.size(); i++)
    {
        g2o::VertexPointXYZ* v = new g2o::VertexPointXYZ();
        v->setId(i + 2);
        double z = 1;//深度未知,设为1
        double x = (pts1[i].x - cx) * z / fx;//通过相机内参将像素坐标转化到相机坐标系
        double y = (pts1[i].y - cy) * z / fy;//通过相机内参将像素坐标转化到相机坐标系

        v->setMarginalized(true);//在schur分解时是否进行边缘化,g2o内部通过该函数对应参数判断该点是特征点还是位姿
        v->setEstimate(Eigen::Vector3d(x,y,z));//填入观测值
        optimizer.addVertex(v);//图中加入顶点
    }
    //添加相机参数
    g2o::CameraParameters* camera = new g2o::CameraParameters(fx, Eigen::Vector2d(cx, cy), 0);
    camera->setId(0);
    optimizer.addParameter(camera);
    //添加边
    vector<g2o::EdgeProjectXYZ2UV*> edges;
    for(size_t i = 0; i < pts1.size(); i++)
    {
        g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();
        edge->setVertex(0, dynamic_cast<g2o::VertexPointXYZ*> (optimizer.vertex(i+2)));//设定顶点0
        edge->setVertex(1, dynamic_cast<g2o::VertexSE3Expmap*> (optimizer.vertex(0)));//设定顶点1
        edge->setMeasurement(Eigen::Vector2d(pts1[i].x, pts1[i].y));//设置测量值为第一张图的特征点
        edge->setInformation(Eigen::Matrix2d::Identity());//信息矩阵设为0
        edge->setParameterId(0, 0);
        edge->setRobustKernel(new g2o::RobustKernelHuber());//设置鲁棒核函数
        optimizer.addEdge(edge);//添加第一张图边
        edges.push_back(edge);//记录边
    }
    for(size_t i = 0; i < pts2.size(); i++)
    {
        g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();
        edge->setVertex(0, dynamic_cast<g2o::VertexPointXYZ*> (optimizer.vertex(i+2)));
        edge->setVertex(1, dynamic_cast<g2o::VertexSE3Expmap*> (optimizer.vertex(1)));
        edge->setMeasurement(Eigen::Vector2d(pts2[i].x, pts2[i].y));
        edge->setInformation(Eigen::Matrix2d::Identity());
        edge->setParameterId(0, 0);
        edge->setRobustKernel(new g2o::RobustKernelHuber());
        optimizer.addEdge(edge);//添加第二张图的边
        edges.push_back(edge);
    }
    cout << "开始优化" << endl;
    optimizer.setVerbose(true);//输出优化日志 
    optimizer.initializeOptimization();//优化初始化
    optimizer.optimize(10);//优化10次
    cout << "优化结束" << endl;

    g2o::VertexSE3Expmap* v = dynamic_cast<g2o::VertexSE3Expmap*>(optimizer.vertex(1));//读取优化结果
    Eigen::Isometry3d pose = v->estimate();//转化成Isometry3d类型
    cout << "Pose = " << endl << pose.matrix() << endl;

    for(size_t i = 0; i < pts1.size(); i++)
    {//读取优化的特征点结果,并输出
        g2o::VertexPointXYZ* v = dynamic_cast<g2o::VertexPointXYZ*>(optimizer.vertex(2+i));
        cout << "vertex id" << i+2 << ", pos = ";
        Eigen::Vector3d pose = v->estimate();
        cout << pose(0) << "," << pose(1) << "," << pose(2) << endl;
    }

    int inliners = 0;
    for(auto e : edges)
    {//遍历所有的边,如果误差过大则认为是异常点
        e->computeError();
        if(e->chi2() > 1)
            cout << "error = " << e->chi2() << endl;
        else
            inliners++;
    }
    cout << "inliners in total points: " << inliners << "/" << pts1.size() + pts2.size() << endl;
    optimizer.save("ba.g2o");

    cv::waitKey(0);

    return 0;
}

三、学习时遇到的问题

关于BlockSolver_6_3的参数为什么是6和3

我的理解是:在进行g2o的时候放进去的两个顶点分别是pose位姿和landmark坐标点。位姿是平移和旋转,维度是6;坐标点是xyz,维度是3。

以上均为个人理解,欢迎指导、交流、讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值