SLAM十四讲---前端0.3---基本的VO---两帧视觉里程计基础上加入g2o以优化PnP

之前用的是RANSAC的PnP求解,opencv一行命令就搞定了。

但是容易受到噪声影响。除了可以光流法搞一下(如同第二版),但是maybe用g2o会更好

因此这里加入g2o优化,以ransac的pnp结果为图优化的初始值

具体为3d-2d的BA,单元边

g2o_type.h

(1)定义了三种误差边
(i)3d-3d的二元边
(ii)3d-3d的一元边
(iii)3d-2d的一元边
(2)然后注释掉计算雅克比的函数:因为计算雅克比矩阵是个费力不讨好的活。。。。。。
这样,g2o直接提供一个数值计算的雅克比函数
(3)在所有虚继承的函数中加上override,比如 virtual void computeError() override;
以作个提醒


#ifndef MYSLAM_G2O_TYPES_H
#define MYSLAM_G2O_TYPES_H

#include "myslam/common_include.h"
#include "camera.h"

#include <g2o/core/base_vertex.h>
#include <g2o/core/base_unary_edge.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/types/sba/types_six_dof_expmap.h>
#include <g2o/solvers/dense/linear_solver_dense.h>
#include <g2o/core/robust_kernel.h>
#include <g2o/core/robust_kernel_impl.h>

namespace myslam
{
class EdgeProjectXYZRGBD : public g2o::BaseBinaryEdge<3, Eigen::Vector3d, g2o::VertexSBAPointXYZ, g2o::VertexSE3Expmap> // 用3d-3d的g2o二元边优化
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
    virtual void computeError() override;
//     virtual void linearizeOplus() override;  // 喜欢提供数值计算的雅克比函数,所以果断注释
    virtual bool read( std::istream& in ) override{}
    virtual bool write( std::ostream& out) const override {}
    
};

// only to optimize the pose, no point
class EdgeProjectXYZRGBDPoseOnly: public g2o::BaseUnaryEdge<3, Eigen::Vector3d, g2o::VertexSE3Expmap >  // 用3d-3d的g2o优化
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    // Error: measure = R*point+t
    virtual void computeError() override;  // 计算误差
//     virtual void linearizeOplus() override;  // 计算雅克比矩阵
    
    virtual bool read( std::istream& in ) override{}
    virtual bool write( std::ostream& out) const override{}
    
    Vector3d point_;
};

class EdgeProjectXYZ2UVPoseOnly: public g2o::BaseUnaryEdge<2, Eigen::Vector2d, g2o::VertexSE3Expmap >  // 3d-2d的g2o优化
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    
    virtual void computeError() override;
//     virtual void linearizeOplus() override;
    
    virtual bool read( std::istream& in ) override{}
    virtual bool write(std::ostream& os) const override{};
    
    Vector3d point_;
    Camera* camera_;  // 涉及内参的,相机内部的变换,因此需要有个成员变量为相机类
};

}


#endif // MYSLAM_G2O_TYPES_H

g2o_type.cpp

(1)这里需要修改一下g2o的初始化方式(因为g2o版本迭代的问题,第一版代码会有一些莫名其妙的bug)
(2)然后我将计算雅克比的函数实现注释掉

#include "myslam/g2o_types.h"

namespace myslam
{
void EdgeProjectXYZRGBD::computeError()  // 二元边,所以两个顶点。// 误差为3d向量
{
    const g2o::VertexSBAPointXYZ* point = static_cast<const g2o::VertexSBAPointXYZ*> ( _vertices[0] );  // 点是左系的点
    const g2o::VertexSE3Expmap* pose = static_cast<const g2o::VertexSE3Expmap*> ( _vertices[1] );
    _error = _measurement - pose->estimate().map ( point->estimate() );  // pose目测是左系转到右系Tcr
}

// void EdgeProjectXYZRGBD::linearizeOplus()
// {
//     g2o::VertexSE3Expmap* pose = static_cast<g2o::VertexSE3Expmap *> ( _vertices[1] );
//     g2o::SE3Quat T ( pose->estimate() );  // 这个初始化方法值得探究
//     g2o::VertexSBAPointXYZ* point = static_cast<g2o::VertexSBAPointXYZ*> ( _vertices[0] );
//     Eigen::Vector3d xyz = point->estimate();
//     Eigen::Vector3d xyz_trans = T.map ( xyz );  // 右系的投影点
//     double x = xyz_trans[0];
//     double y = xyz_trans[1];
//     double z = xyz_trans[2];
// 
//     _jacobianOplusXi = - T.rotation().toRotationMatrix();
// 
//     _jacobianOplusXj ( 0,0 ) = 0;
//     _jacobianOplusXj ( 0,1 ) = -z;
//     _jacobianOplusXj ( 0,2 ) = y;
//     _jacobianOplusXj ( 0,3 ) = -1;
//     _jacobianOplusXj ( 0,4 ) = 0;
//     _jacobianOplusXj ( 0,5 ) = 0;
// 
//     _jacobianOplusXj ( 1,0 ) = z;
//     _jacobianOplusXj ( 1,1 ) = 0;
//     _jacobianOplusXj ( 1,2 ) = -x;
//     _jacobianOplusXj ( 1,3 ) = 0;
//     _jacobianOplusXj ( 1,4 ) = -1;
//     _jacobianOplusXj ( 1,5 ) = 0;
// 
//     _jacobianOplusXj ( 2,0 ) = -y;
//     _jacobianOplusXj ( 2,1 ) = x;
//     _jacobianOplusXj ( 2,2 ) = 0;
//     _jacobianOplusXj ( 2,3 ) = 0;
//     _jacobianOplusXj ( 2,4 ) = 0;
//     _jacobianOplusXj ( 2,5 ) = -1;
// }

void EdgeProjectXYZRGBDPoseOnly::computeError()  // 误差为3d向量
{
    const g2o::VertexSE3Expmap* pose = static_cast<const g2o::VertexSE3Expmap*> ( _vertices[0] );
    _error = _measurement - pose->estimate().map ( point_ );
}

// void EdgeProjectXYZRGBDPoseOnly::linearizeOplus()
// {
//     g2o::VertexSE3Expmap* pose = static_cast<g2o::VertexSE3Expmap*> ( _vertices[0] );
//     g2o::SE3Quat T ( pose->estimate() );
//     Vector3d xyz_trans = T.map ( point_ );
//     double x = xyz_trans[0];
//     double y = xyz_trans[1];
//     double z = xyz_trans[2];
// 
//     _jacobianOplusXi ( 0,0 ) = 0;
//     _jacobianOplusXi ( 0,1 ) = -z;
//     _jacobianOplusXi ( 0,2 ) = y;
//     _jacobianOplusXi ( 0,3 ) = -1;
//     _jacobianOplusXi ( 0,4 ) = 0;
//     _jacobianOplusXi ( 0,5 ) = 0;
// 
//     _jacobianOplusXi ( 1,0 ) = z;
//     _jacobianOplusXi ( 1,1 ) = 0;
//     _jacobianOplusXi ( 1,2 ) = -x;
//     _jacobianOplusXi ( 1,3 ) = 0;
//     _jacobianOplusXi ( 1,4 ) = -1;
//     _jacobianOplusXi ( 1,5 ) = 0;
// 
//     _jacobianOplusXi ( 2,0 ) = -y;
//     _jacobianOplusXi ( 2,1 ) = x;
//     _jacobianOplusXi ( 2,2 ) = 0;
//     _jacobianOplusXi ( 2,3 ) = 0;
//     _jacobianOplusXi ( 2,4 ) = 0;
//     _jacobianOplusXi ( 2,5 ) = -1;
// }

void EdgeProjectXYZ2UVPoseOnly::computeError()  //  误差为2d向量
{
    const g2o::VertexSE3Expmap* pose = static_cast<const g2o::VertexSE3Expmap*> ( _vertices[0] );
    _error = _measurement - camera_->camera2pixel ( 
        pose->estimate().map(point_) );
}

// void EdgeProjectXYZ2UVPoseOnly::linearizeOplus()
// {
//     g2o::VertexSE3Expmap* pose = static_cast<g2o::VertexSE3Expmap*> ( _vertices[0] );
//     g2o::SE3Quat T ( pose->estimate() );
//     Vector3d xyz_trans = T.map ( point_ );
//     double x = xyz_trans[0];
//     double y = xyz_trans[1];
//     double z = xyz_trans[2];
//     double z_2 = z*z;
// 
//     _jacobianOplusXi ( 0,0 ) =  x*y/z_2 *camera_->fx_;
//     _jacobianOplusXi ( 0,1 ) = - ( 1+ ( x*x/z_2 ) ) *camera_->fx_;
//     _jacobianOplusXi ( 0,2 ) = y/z * camera_->fx_;
//     _jacobianOplusXi ( 0,3 ) = -1./z * camera_->fx_;
//     _jacobianOplusXi ( 0,4 ) = 0;
//     _jacobianOplusXi ( 0,5 ) = x/z_2 * camera_->fx_;
// 
//     _jacobianOplusXi ( 1,0 ) = ( 1+y*y/z_2 ) *camera_->fy_;
//     _jacobianOplusXi ( 1,1 ) = -x*y/z_2 *camera_->fy_;
//     _jacobianOplusXi ( 1,2 ) = -x/z *camera_->fy_;
//     _jacobianOplusXi ( 1,3 ) = 0;
//     _jacobianOplusXi ( 1,4 ) = -1./z *camera_->fy_;
//     _jacobianOplusXi ( 1,5 ) = y/z_2 *camera_->fy_;
// }

}

然后在visual_odometry.cpp中改写位姿估计函数

void VisualOdometry::poseEstimationPnP()
{
    // construct the 3d 2d observations
    vector<cv::Point3f> pts3d;
    vector<cv::Point2f> pts2d;
    
    for ( cv::DMatch m:feature_matches_ )
    {
        pts3d.push_back( pts_3d_ref_[m.queryIdx] );
        pts2d.push_back( keypoints_curr_[m.trainIdx].pt );
    }
    
    Mat K = ( cv::Mat_<double>(3,3)<<
        ref_->camera_->fx_, 0, ref_->camera_->cx_,
        0, ref_->camera_->fy_, ref_->camera_->cy_,
        0,0,1
    );
    Mat rvec, tvec, inliers;
    cv::solvePnPRansac( pts3d, pts2d, K, Mat(), rvec, tvec, false, 100, 4.0, 0.99, inliers );  // 用BA也可以,可以用默认顶点和边(单元边)  // rvec, tvec,为旋转和平移
    num_inliers_ = inliers.rows;  // 多少个局内点(相当于用上了多少3d-2d作为局内点)
    cout<<"pnp inliers: "<<num_inliers_<<endl;

    T_c_r_estimated_ = SE3(
        SO3::exp(Vector3d(rvec.at<double>(0,0), rvec.at<double>(1,0), rvec.at<double>(2,0))),  // 原先的SO3改掉
        Vector3d( tvec.at<double>(0,0), tvec.at<double>(1,0), tvec.at<double>(2,0))
    );
    
    // 加入g2o优化 *******************************************************************
    // using bundle adjustment to optimize the pose 
//     typedef g2o::BlockSolver<g2o::BlockSolverTraits<6,2>> Block;
//     Block::LinearSolverType* linearSolver = new g2o::LinearSolverDense<Block::PoseMatrixType>();
//     Block* solver_ptr = new Block( linearSolver );
//     g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg ( solver_ptr );
//     g2o::SparseOptimizer optimizer;
//     optimizer.setAlgorithm ( solver );
    
    // 构建图优化,先设定g2o
    typedef g2o::BlockSolver<g2o::BlockSolverTraits<6, 3>> BlockSolverType;  // pose is 6, landmark is 3  位姿和路标点的维度(相当于顶点们的维度,当然,这里路标点没算做顶点)
    typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType; // 线性求解器类型
    // 梯度下降方法,可以从GN, LM, DogLeg 中选
    auto solver = new g2o::OptimizationAlgorithmLevenberg(
      g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
    g2o::SparseOptimizer optimizer;     // 图模型
    optimizer.setAlgorithm(solver);   // 设置求解器
    optimizer.setVerbose(true);       // 打开调试输出
    
    
    
    g2o::VertexSE3Expmap* pose = new g2o::VertexSE3Expmap();
    pose->setId ( 0 );
    pose->setEstimate ( g2o::SE3Quat (      // 设置初始值为ransac的结果
        T_c_r_estimated_.rotationMatrix(), 
        T_c_r_estimated_.translation()
    ) );
    optimizer.addVertex ( pose );

    // edges
    for ( int i=0; i<inliers.rows; i++ )
    {
        int index = inliers.at<int>(i,0);
        // 3D -> 2D projection
        EdgeProjectXYZ2UVPoseOnly* edge = new EdgeProjectXYZ2UVPoseOnly();  // 3d-2d的单元边,仅仅优化位姿
        edge->setId(i);
        edge->setVertex(0, pose);
        edge->camera_ = curr_->camera_.get();
        edge->point_ = Vector3d( pts3d[index].x, pts3d[index].y, pts3d[index].z );
        edge->setMeasurement( Vector2d(pts2d[index].x, pts2d[index].y) );
        edge->setInformation( Eigen::Matrix2d::Identity() );
        optimizer.addEdge( edge );
    }
    
    optimizer.initializeOptimization();
    optimizer.optimize(10);
    
    T_c_r_estimated_ = SE3 (
        pose->estimate().rotation(),  // 四元数
        pose->estimate().translation()  // 平移
    );
    
    
}


最后记得CMakeLists中加上g2o,要注意,因为使用了默认的g2o顶点,因此需要这样(g2o_types_sba决定了你能够使用g2o默认顶点):

# G2O
LIST( APPEND CMAKE_MODULE_PATH /home/mjy/slambook2/3rdparty/g2o/g2o/cmake_modules ) 
SET( G2O_ROOT /usr/local/include/g2o ) 
find_package( G2O REQUIRED )
include_directories( ${G2O_INCLUDE_DIRS} )

set( THIRD_PARTY_LIBS 
    ${OpenCV_LIBS}
    ${Sophus_LIBRARIES}
    g2o_core g2o_stuff g2o_types_sba)

此外,直接运行可能会报错:double free or corruption (out),需要更改CMakelists.txt

将set( CMAKE_CXX_FLAGS “-std=c++11 -march=native -O3” )改成set(
CMAKE_CXX_FLAGS “-std=c++11 -O3” )

结果发现迭代error变化很小,可能是因为迭代一次用了很多点,所以第一次迭代就搞定了。而且初始值也不错。

但是仍然有缺陷,比如:

中间某帧丢失的话可能会出现比如第一帧和第五帧进行PnP。这样匹配的特征点可能比较少。因此ransac的PnP效果不是太好。

因此考虑加入局部地图:local mapping

就是VO维护的map_地图

这是下一篇要讲述的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章