本篇基于高博的系列文章实践练习.绝大部分搬运自参考博客一。参考博客一写得太好了,以作备忘!博客一博主为东北大学硕士,github主页,学习态度值得学习!参考博客三中对G2O源码进行了一些分析,对我的帮助也很大。在练习过程中自己想将优化结果打印出来查看,捣鼓了很久在博客三中找到了答案:
(5)static_cast关键字
static_cast
(expression)
static_cast叫做编译时类型检查。该运算符把expression转换为type-id类型,这样做的目的是在程序还没有运行时进行类型检查,来保证转换的安全性。
而与之对应的,dynamic_cast关键字(运行时类型检查)主要用于类层次结构中父类和子类之间指针和引用的转换,由于具有运行时类型检查,因此可以保证下行转换的安全性。即转换成功就返回转换后的正确类型指针,如果转换失败,则返回NULL。
所谓上行转换是指由子类指针到父类指针的转换,下行转换是指由父类指针到子类指针的转换。
//打印优化后的相机姿态
for(int i = 0;i<globalOptimizer.vertices().size() -1;i++){
g2o::VertexSE3* vertex = dynamic_cast<g2o::VertexSE3*>(globalOptimizer.vertex(i));
Eigen::Isometry3d pose = vertex->estimate(); //该帧优化后的位姿
cout << "current is "<<i<<" T=\n" << pose.matrix() << endl;
}
G2O本质是求解的非线性问题,可以求解BA问题。有如下最小二乘系统,对应图模型如图.
ξ = argmin ξ 1 2 ∑ i ∥ r i ∥ Σ i 2 \boldsymbol{\xi}=\underset{\boldsymbol{\xi}}{\operatorname{argmin}} \frac{1}{2} \sum_{i}\left\|\mathbf{r}_{i}\right\|_{\boldsymbol{\Sigma}_{i}}^{2} ξ=ξargmin21i∑∥ri∥Σi2
对应的高斯牛顿求解,normal equation:
J ⊤ Σ − 1 J ⏟ H or Λ δ ξ = − J ⊤ Σ − 1 r ⏟ b \underbrace{\mathbf{J}^{\top} \boldsymbol{\Sigma}^{-1} \mathbf{J}}_{\mathbf{H} \text { or } \mathbf{\Lambda}} \delta \boldsymbol{\xi}=\underbrace{-\mathbf{J}^{\top} \boldsymbol{\Sigma}^{-1} \mathbf{r}}_{\mathbf{b}} H or Λ J⊤Σ−1Jδξ=b −J⊤Σ−1r
连加形式:
∑ i = 1 n J i ⊤ Σ i − 1 J i δ ξ = − ∑ i = 1 n J ⊤ Σ i − 1 r \sum_{i=1}^{n} \mathbf{J}_{i}^{\top} \boldsymbol{\Sigma}_{i}^{-1} \mathbf{J}_{i} \delta \boldsymbol{\xi}=-\sum_{i=1}^{n} \mathbf{J}^{\top} \boldsymbol{\Sigma}_{i}^{-1} \mathbf{r} i=1∑nJi⊤Σi−1Jiδξ=−i=1∑nJ⊤Σi−1r
使用G2O求解:
在SLAM中防止乱飘,会固定第一帧,当然也有其他方式防止漂移。在高博RGBD系列博客中,固定第一帧不做优化来防止漂移:
// 向globalOptimizer增加第一个顶点
// The parameterization for the increments constructed is a 6d vector (x,y,z,qx,qy,qz)
g2o::VertexSE3* v = new g2o::VertexSE3();//顶点:相机位姿+ID
v->setId( 0 );
v->setEstimate( Eigen::Isometry3d::Identity() ); //估计为单位矩阵
v->setFixed( true ); //第一个顶点固定,不用优化
globalOptimizer.addVertex( v );
首先构造 g2o 模型,包括选择线性方程求解器、矩阵求解器和下降算法;
// 分别对应图中的前四个步骤
// step.4.1.设置图模型创建优化器.
g2o::SparseOptimizer optimizer;
// step.1.使用Cholmod中的线性方程求解器得到 linearSolver
g2o::BlockSolver_6_3::LinearSolverType * linearSolver;
linearSolver = new g2o::LinearSolverDense<g2o::BlockSolver_6_3::PoseMatrixType>();
// step.2.再用稀疏矩阵块求解器 solver_ptr
g2o::BlockSolver_6_3 * solver_ptr = new g2o::BlockSolver_6_3(linearSolver);
// step.3.选择梯度下降方法:L-M 算法 求解上面的 solver_ptr 得到 solver.
g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg(solver_ptr);
// step.4.2.设置求解器.
optimizer.setAlgorithm(solver);
// 打开调试输出
optimizer.setVerbose( true );
先给出前面初始化的代码再看类图吧,直接看图一脸懵,参考高博博客 .
上图中左侧的 SparseOptimizer
是第一步初始化的图模型,也是最终要维护的;
先看下半部分,对应前面的初始化代码,用于指定求解器和迭代算法,一个 SparseOptimizer
拥有一个 Optimization Algorithm
,其优化算法继承自 Gauss-Newton, Levernberg-Marquardt, Powell's dogleg
三者之一,不同的方法表现为最终的 H 矩阵构造不同;同时这个 Optimization Algorithm
拥有一个 Solver
,其包含两个部分:
SparseBlockMatrix
,用于计算稀疏的雅克比和海森矩阵; PCG, CSparse, Choldmod
三者选则,用于计算迭代过程中最关键的一步然后看上半部分 SparseOptimizer
是一个 Optimizable Graph
,从而也是一个 Hyper Graph
(超图), 一个SparseOptimizer
含有很多个顶点和很多个边;
Base Vertex
,也就是 OptimizableGraph::Vertex
;OptimizableGraph::Edge
,又分为 BaseUnaryEdge
(单边), BaseBinaryEdge
(双边)或 BaseMultiEdge
(多边);Base Vertex
和 Base Edge
都是抽象的基类,而实际用的顶点和边,都是它们的派生类;SparseOptimizer.addVertex()
和 SparseOptimizer.addEdge()
向一个图中添加顶点和边,最后调用 SparseOptimizer.optimize()
完成优化。综上所述,在 g2o 中选择优化方法一共有三个步骤
// variable size solver
using BlockSolverX = BlockSolverPL<Eigen::Dynamic, Eigen::Dynamic>;
// solver for BA/3D SLAM
using BlockSolver_6_3 = BlockSolverPL<6, 3>;
// solver fo BA with scale
using BlockSolver_7_3 = BlockSolverPL<7, 3>;
// 2Dof landmarks 3Dof poses
using BlockSolver_3_2 = BlockSolverPL<3, 2>;
结合前面 1.1 节,总结一下 g2o 的代码流程
// 每个误差项优化变量维度为3,误差值维度为1
typedef g2o::BlockSolver< g2o::BlockSolverTraits<3,1> > Block;
// 创建一个线性求解器 LinearSolver,采用 dense cholesky 分解法
Block::LinearSolverType* linearSolver
= new g2o::LinearSolverDense<Block::PoseMatrixType>();
要求解的增量方程为 H Δ x = − b H \Delta x=-b HΔx=−b,通常想到的方法就是直接求逆 Δ x = − H − 1 ∗ b \Delta x=-H^{-1} * b Δx=−H−1∗b,对于较小维度的 H 矩阵可以直接求逆,但当 H 维度较大时采用下面的方法进行求逆:
- LinearSolverCholmod:使用 sparse cholesky 分解法,继承自LinearSolverCCS;
- LinearSolverCSparse:使用 CSparse 法,继承自LinearSolverCCS;
- LinearSolverPCG :使用preconditioned conjugate gradient 法,继承自 LinearSolver
- LinearSolverDense :使用 dense cholesky 分解法,继承自 LinearSolver
- LinearSolverEigen: 依赖项只有 eigen,使用 eigen 中 sparse Cholesky 求解,因此编译好后可以方便的在其他地方使用,性能和 CSparse 差不多,继承自 LinearSolver;
Block* solver_ptr = new Block( linearSolver );
BlockSolver 内部包含 LinearSolver,用上面定义的线性求解器 LinearSolver 来初始化,前面已经给定了优化变量的维度;
g2o::OptimizationAlgorithmLevenberg* solver =
new g2o::OptimizationAlgorithmLevenberg( solver_ptr );
优化算法
g2o::OptimizationAlgorithmGaussNewton
g2o::OptimizationAlgorithmLevenberg
g2o::OptimizationAlgorithmDogleg
g2o::SparseOptimizer optimizer; // 图模型
optimizer.setAlgorithm( solver ); // 用前面定义好的求解器作为求解方法:
optimizer.setVerbose( true ); // 打开调试输出
// 创建一个顶点
CurveFittingVertex* v = new CurveFittingVertex();
// 初始化顶点的值
v->setEstimate( Eigen::Vector3d(0,0,0) );
// 设置顶点的编号
v->setId(0);
// 向图中添加顶点
optimizer.addVertex( v );
for ( int i=0; i<N; i++ ) // 往图中增加边
{
// 创建一条边
CurveFittingEdge* edge = new CurveFittingEdge( x_data[i] );
// 设置边的 id
edge->setId(i);
// 设置边连接的顶点
edge->setVertex( 0, v );
// 设置观测数值
edge->setMeasurement( y_data[i] );
// 设置信息矩阵:协方差矩阵之逆
edge->setInformation( Eigen::Matrix<double,1,1>::Identity()*1/(w_sigma*w_sigma) );
// 将边添加到图中
optimizer.addEdge( edge );
}
optimizer.initializeOptimization();
optimizer.optimize(100); // 迭代次数
回到前面的类图上面的顶点部分的箭头,g2o 提供了一个比较通用的适合大多数情况的模板类 BaseVertex
D 是 int 类型,表示顶点 Vertex 的最小维度,比如 3D 空间中旋转是 3 维的,那么这里 D=3;
在源码注释中说 D 并非是顶点(更确切的说是状态变量)的维度,而是其在流形空间(manifold)的最小表示(SO3->so3,SE3->se3).
static const int Dimension = D; ///< dimension of the estimate (minimal) in the manifold space
T 是待估计 Vertex 的数据类型,比如用四元数表达三维旋转的话,T 就是 Quaternion 类型
typedef T EstimateType;
EstimateType _estimate;
g2o 提供的常用的顶点类型,若没有的类型可参考自己定义
VertexSE2 : public BaseVertex<3, SE2> //2D pose Vertex, (x,y,theta)
VertexSE3 : public BaseVertex<6, SE3Quat>
VertexSE3 : public BaseVertex<6, Isometry3>
VertexPointXY : public BaseVertex<2, Eigen::Vector2d>
VertexPointXYZ : public BaseVertex<3, Vector3>
VertexSBAPointXYZ : public BaseVertex<3, Vector3d>//常用
VertexLine2D : public BaseVertex<2, Line2D>
VertexLine3D : public BaseVertex<4, Line3D>
VertexSegment2D : public BaseVertex<4, Vector4>
VertexCircle : public g2o::BaseVertex<3, Eigen::Vector3d>
VertexPlane : public BaseVertex<3, Plane3D>
VertexCam : public BaseVertex<6, SBACam>
VertexPosition3D : public g2o::BaseVertex<3, Eigen::Vector3d>
VertexPositionVelocity3D : public g2o::BaseVertex<6, Vector6d>
VertexOdomDifferentialParams: public BaseVertex <3, Vector3>
VertexCameraBAL : public BaseVertex<9, Eigen::VectorXd>
VertexSim3Expmap : public BaseVertex<7, Sim3> //常用
VertexParams : public g2o::BaseVertex<3, Eigen::Vector3d>
VertexSE3Expmap : public BaseVertex<6, SE3Quat> //常用
VertexBaseline : public BaseVertex<1, double>
高博RGBD系列文章中:
//edge====继承自===>BaseBinaryEdge<6, Isometry3, VertexSE3, VertexSE3>
//D:测量值的维度;E:测量值的数据类型;VertexSE3:顶点类型
g2o::EdgeSE3* edge = new g2o::EdgeSE3();
// 连接此边的两个顶点id
edge->vertices() [0] = globalOptimizer.vertex( i-1 );
edge->vertices() [1] = globalOptimizer.vertex( i );
顶点主要的成员函数(位于 g2o/core/base_vertex.h 中)
// 返回优化之后顶点的值.
const EstimateType& estimate() const { return _estimate;}
自定义一个顶点需要重写以下函数
virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
virtual void setToOriginImpl();
virtual void oplusImpl(const number_t* update);
自定义顶点类的格式
class myVertex: public g2::BaseVertex<Dim, Type>
{
public:
// 类成员变量如果是固定大小对象需要加上以下的宏定义
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
// 构造函数
myVertex(){}
// 读写函数
virtual void read(std::istream& is) {}
virtual void write(std::ostream& os) const {}
// 重置函数
virtual void setOriginImpl()
{
_estimate = Type();
}
// 更新函数
virtual void oplusImpl(const double* update) override
{
_estimate += /*update*/;
}
}
举例一: 以 SLAM 十四讲中曲线拟合的曲线模型顶点为例
class CurveFittingVertex: public g2o::BaseVertex<3, Eigen::Vector3d>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
// 重置
virtual void setToOriginImpl()
{
_estimate << 0,0,0;
}
// 更新
virtual void oplusImpl( const double* update )
{
_estimate += Eigen::Vector3d(update);
}
// 存盘和读盘:留空
virtual bool read( istream& in ) {}
virtual bool write( ostream& out ) const {}
};
举例二: 以李代数表示的位姿作为顶点,位于 g2o/types/sba/types_six_dof_expmap.h 中
/**
* \brief SE3 Vertex parameterized internally with a transformation matrix
and externally with its exponential map
*/
class VertexSE3Expmap : public BaseVertex<6, SE3Quat>{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
// 构造函数.
VertexSE3Expmap();
// 1. 读盘.
bool read(std::istream& is);
// 2. 写盘.
bool write(std::ostream& os) const;
// 3. 顶点重置函数,设定被优化变量的原始值
virtual void setToOriginImpl() {
_estimate = SE3Quat();
}
// 4. 顶点更新函数,增量更新
virtual void oplusImpl(const double* update_) {
Eigen::Map<const Vector6d> update(update_);
setEstimate(SE3Quat::exp(update)*estimate());
}
};
举例三: 以三维向量表示的三维点作为顶点,位于 g2o/types/types_sba.h 中
class VertexSBAPointXYZ : public BaseVertex<3, Vector3d>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
VertexSBAPointXYZ();
virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
virtual void setToOriginImpl() {
_estimate.fill(0.);
}
virtual void oplusImpl(const double* update)
{
Eigen::Map<const Vector3d> v(update);
_estimate += v;
}
};
步骤:
① 创建顶点
② 设置初始值
③ 设置节点编号
④ 添加到优化器中
举例一:曲线拟合
CurveFittingVertex* v = new CurveFittingVertex();
v->setEstimate( Eigen::Vector3d(0,0,0) );
v->setId(0);
optimizer.addVertex( v );
举例二:三维坐标点
int index = 1;
for ( const Point3f p:points_3d ) // landmarks
{
g2o::VertexSBAPointXYZ* point = new g2o::VertexSBAPointXYZ();
point->setId ( index++ );
point->setEstimate ( Eigen::Vector3d ( p.x, p.y, p.z ) );
point->setMarginalized ( true );
optimizer.addVertex ( point );
}
还是回到前面的类图,关注右上角的边部分,我们一般使用的类是 BaseUnaryEdge,BaseBinaryEdge,BaseMultiEdge
分别表示一元边,两元边,多元边(位于 g2o/g2o/core/base_edge.h 中),类似于顶点,他们又继承自OptimizableGraph::Edge (位于 g2o/g2o/core/optimizable_graph.h 中),hyper_graph::Edge(位于g2o/g2o/core/hyper_graph.h 中)。
一元边表示只连接一个顶点,二元边表示连接两个顶点,多元边表示连接 3 个或以上顶点。
主要参数有:D, E, VertexXi, VertexXj
举例:用边表示三维点投影到图像平面的重投影误差
BaseBinaryEdge<2, Vector2D, VertexSBAPointXYZ, VertexSE3Expmap>
自定义边需要重写以下成员函数
virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
virtual void computeError();
virtual void linearizeOplus();
除以上几个成员函数之外还有几个重要的成员变量和函数
_measurement:存储观测值
_error:存储computeError() 函数计算的误差
_vertices[]:存储顶点信息,比如二元边的话,_vertices[] 的大小为2,存储顺序和调用setVertex(int, vertex) 是设定的int 有关(0 或1)
setId(int):来定义边的编号(决定了在H矩阵中的位置)
setMeasurement(type) 函数来定义观测值
setVertex(int, vertex) 来定义顶点
setInformation() 来定义协方差矩阵的逆
自定义边的格式:类似于定义顶点,但重点是 computeError(),linearizeOplus() 两个函数,也更复杂
class myEdge: public g2o::BaseBinaryEdge<errorDim, errorType, Vertex1Type, Vertex2Type>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
myEdge(){}
virtual bool read(istream& in) {}
virtual bool write(ostream& out) const {}
virtual void computeError() override
{
// ...
_error = _measurement - Something;
}
virtual void linearizeOplus() override
{
_jacobianOplusXi(pos, pos) = something;
// ...
/*
_jocobianOplusXj(pos, pos) = something;
...
*/
}
private:
// data
}
举例一:一元边。来源十四讲中的曲线拟合
定义误差边为 CurveFittingEdge,维度为 1,类型为 double,连接的顶点为 CurveFittingVertex;
class CurveFittingEdge: public g2o::BaseUnaryEdge<1,double,CurveFittingVertex>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
CurveFittingEdge( double x ): BaseUnaryEdge(), _x(x) {}
// 计算曲线模型误差
void computeError()
{
const CurveFittingVertex* v = static_cast<const CurveFittingVertex*> (_vertices[0]);
const Eigen::Vector3d abc = v->estimate();
_error(0,0) = _measurement - std::exp( abc(0,0)*_x*_x + abc(1,0)*_x + abc(2,0) ) ;
}
virtual bool read( istream& in ) {}
virtual bool write( ostream& out ) const {}
public:
double _x; // x 值, y 值为 _measurement
};
举例二: 二元边。
3D-2D点的PnP 问题,也就是最小化重投影误差问题,来源于 g2o/types/sba/types_six_dof_expmap.h
构造边为 EdgeProjectXYZ2UV,维度为 2,类型为 Vector2D,连接的两个顶点分别为三维点 VertexSBAPointXYZ 和李群表示的相机位姿 VertexSE3Expmap;
//继承了BaseBinaryEdge类,观测值是2维,类型Vector2D,顶点分别是三维点、李群位姿
class G2O_TYPES_SBA_API EdgeProjectXYZ2UV : public BaseBinaryEdge<2, Vector2D, VertexSBAPointXYZ, VertexSE3Expmap>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
//1. 构造函数初始化
EdgeProjectXYZ2UV();
//2. 计算误差
void computeError()
{
// 李群相机位姿 顶点 v1
const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
// 三维点 顶点 v2
const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
//相机参数
const CameraParameters * cam
= static_cast<const CameraParameters *>(parameter(0));
//误差计算,测量值减去估计值,也就是重投影误差obs-cam
//估计值计算方法是T*p,得到相机坐标系下坐标,然后在利用camera2pixel()函数得到像素坐标。
Vector2D obs(_measurement);
_error = obs-cam->cam_map(v1->estimate().map(v2->estimate()));
}
//3. 线性增量函数,也就是雅克比矩阵J的计算方法
virtual void linearizeOplus();
//4. 相机参数
CameraParameters * _cam;
bool read(std::istream& is);
bool write(std::ostream& os) const;
};
关于误差计算函数:误差 = 观测 - 投影
_error = obs - cam->cam_map(v1->estimate().map(v2->estimate()));
关于雅克比矩阵的计算函数 linearizeOplus():误差(观测相机方程)关于相机位姿和三维点的偏导数.
∂ e ∂ δ ξ = − [ f x Z ′ 0 − f x X ′ Z ′ 2 − f x X ′ Y ′ Z ′ 2 f x + f x X 2 Z ′ 2 − f x Y ′ Z ′ 0 f y Z ′ − f y Y ′ Z ′ 2 − f y − f y Y ′ 2 Z ′ 2 f y X ′ Y ′ Z ′ 2 f y X ′ Z ′ ] \frac{\partial \boldsymbol{e}}{\partial \delta \boldsymbol{\xi}}=-\left[\begin{array}{ccccc} \frac{f_{x}}{Z^{\prime}} & 0 & -\frac{f_{x} X^{\prime}}{Z^{\prime 2}} & -\frac{f_{x} X^{\prime} Y^{\prime}}{Z^{\prime 2}} & f_{x}+\frac{f_{x} X^{2}}{Z^{\prime 2}} & -\frac{f_{x} Y^{\prime}}{Z^{\prime}} \\ 0 & \frac{f_{y}}{Z^{\prime}} & -\frac{f_{y} Y^{\prime}}{Z^{\prime 2}} & -f_{y}-\frac{f_{y} Y^{\prime 2}}{Z^{\prime 2}} & \frac{f_{y} X^{\prime} Y^{\prime}}{Z^{\prime 2}} & \frac{f_{y} X^{\prime}}{Z^{\prime}} \end{array}\right] ∂δξ∂e=−[Z′fx00Z′fy−Z′2fxX′−Z′2fyY′−Z′2fxX′Y′−fy−Z′2fyY′2fx+Z′2fxX2Z′2fyX′Y′−Z′fxY′Z′fyX′] ∂ e ∂ P = ∂ e ∂ P ′ ∂ P ′ ∂ P \frac{\partial e}{\partial \boldsymbol{P}}=\frac{\partial \boldsymbol{e}}{\partial \boldsymbol{P}^{\prime}} \frac{\partial \boldsymbol{P}^{\prime}}{\partial \boldsymbol{P}} ∂P∂e=∂P′∂e∂P∂P′
上式分别对应下面代码中的 _jacobianOplusXj
和 _jacobianOplusXi
(代码位于2o/g2o/types/sba/types_six_dof_expmap.cpp 中)
void EdgeProjectXYZ2UV::linearizeOplus() {
VertexSE3Expmap * vj = static_cast<VertexSE3Expmap *>(_vertices[1]);
SE3Quat T(vj->estimate());
VertexSBAPointXYZ* vi = static_cast<VertexSBAPointXYZ*>(_vertices[0]);
Vector3D xyz = vi->estimate();
Vector3D xyz_trans = T.map(xyz);
double x = xyz_trans[0];
double y = xyz_trans[1];
double z = xyz_trans[2];
double z_2 = z*z;
const CameraParameters * cam = static_cast<const CameraParameters *>(parameter(0));
Matrix<double,2,3,Eigen::ColMajor> tmp;
tmp(0,0) = cam->focal_length;
tmp(0,1) = 0;
tmp(0,2) = -x/z*cam->focal_length;
tmp(1,0) = 0;
tmp(1,1) = cam->focal_length;
tmp(1,2) = -y/z*cam->focal_length;
_jacobianOplusXi = -1./z * tmp * T.rotation().toRotationMatrix();
_jacobianOplusXj(0,0) = x*y/z_2 *cam->focal_length;
_jacobianOplusXj(0,1) = -(1+(x*x/z_2)) *cam->focal_length;
_jacobianOplusXj(0,2) = y/z *cam->focal_length;
_jacobianOplusXj(0,3) = -1./z *cam->focal_length;
_jacobianOplusXj(0,4) = 0;
_jacobianOplusXj(0,5) = x/z_2 *cam->focal_length;
_jacobianOplusXj(1,0) = (1+y*y/z_2) *cam->focal_length;
_jacobianOplusXj(1,1) = -x*y/z_2 *cam->focal_length;
_jacobianOplusXj(1,2) = -x/z *cam->focal_length;
_jacobianOplusXj(1,3) = 0;
_jacobianOplusXj(1,4) = -1./z *cam->focal_length;
_jacobianOplusXj(1,5) = y/z_2 *cam->focal_length;
}
举例一:一元边.
以曲线拟合为例(slambook/ch6/g2o_curve_fitting/main.cpp)
for ( int i=0; i<N; i++ )
{
// 创建边
CurveFittingEdge* edge = new CurveFittingEdge( x_data[i] );
edge->setId(i); // 设置边的 id
edge->setVertex( 0, v ); // 设置连接的顶点
edge->setMeasurement( y_data[i] ); // 观测数值
// 信息矩阵:协方差矩阵之逆
edge->setInformation( Eigen::Matrix<double,1,1>::Identity()*1/(w_sigma*w_sigma) );
// 将边添加到图中
optimizer.addEdge( edge );
}
举例二:二元边。
位于 slambook/ch7/pose_estimation_3d2d.cpp 中
index = 1;
for ( const Point2f p:points_2d )
{
// 创建边.
g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();
// 设置边的 id
edge->setId ( index );
// 指定连接的顶点
edge->setVertex ( 0, dynamic_cast<g2o::VertexSBAPointXYZ*> ( optimizer.vertex ( index ) ) );
edge->setVertex ( 1, pose );
// 设置观测值
edge->setMeasurement ( Eigen::Vector2d ( p.x, p.y ) );
edge->setParameterId ( 0,0 );
edge->setInformation ( Eigen::Matrix2d::Identity() );
optimizer.addEdge ( edge );
index++;
}
注意在 setVertex() 中设置顶点的 id 和类型时要与边的类中定义的相对应,比如定义边时 v1 表示相机位姿,id 为 1,v2 表示三维点,id 为0
class G2O_TYPES_SBA_API EdgeProjectXYZ2UV
.....
//李群相机位姿v1
const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
// 顶点v2
const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
ORB-SLAM2 中顶点有三个,SE(3) 相机位姿、地图点坐标、闭环检测时的 sim(3) 相机位姿:
与前面自定义顶点的举例二中一样,即 g2o/types/types_six_dof_expmap.h
中 6 维李代数表示的 VertexSE3Expmap。
与前面自定义顶点的举例三中一样,即 g2o/types/types_sba.h
中三维向量表示的 VertexSBAPointXYZ。
与 6 维李代数表示的相机为了多了一个尺度因子
// BRIEF 7 维 Sim3 表示的相机位姿.(用于闭环),增加了一个尺度 s.
class VertexSim3Expmap : public BaseVertex<7, Sim3>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
VertexSim3Expmap();
virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
// 重置函数,尺度因子 s 设置为 1.
virtual void setToOriginImpl() {
_estimate = Sim3();
}
// 顶点更新函数.
virtual void oplusImpl(const double* update_)
{
Eigen::Map<Vector7d> update(const_cast<double*>(update_));
if (_fix_scale)
update[6] = 0;
Sim3 s(update);
setEstimate(s*estimate());
}
Vector2d _principle_point1, _principle_point2;
Vector2d _focal_length1, _focal_length2;
// 地图点投影到闭环帧的坐标?
Vector2d cam_map1(const Vector2d & v) const
{
Vector2d res;
res[0] = v[0]*_focal_length1[0] + _principle_point1[0];
res[1] = v[1]*_focal_length1[1] + _principle_point1[1];
return res;
}
// 地图点投影到当前帧的坐标?
Vector2d cam_map2(const Vector2d & v) const
{
Vector2d res;
res[0] = v[0]*_focal_length2[0] + _principle_point2[0];
res[1] = v[1]*_focal_length2[1] + _principle_point2[1];
return res;
}
bool _fix_scale;
protected:
};
在PoseOptimization()
仅优化相机位姿 时,重投影误差是一个一元边,顶点(优化变量)为相机的位姿。对应 g2o/types/types_six_dof_expmap.h 中定义的边 EdgeSE3ProjectXYZOnlyPose
。
// BRIEF 一元边:重投影误差(仅用于优化相机位姿时)
class EdgeSE3ProjectXYZOnlyPose: public BaseUnaryEdge<2, Vector2d, VertexSE3Expmap>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
EdgeSE3ProjectXYZOnlyPose(){}
bool read(std::istream& is);
bool write(std::ostream& os) const;
// 误差计算函数.
void computeError()
{
// 李代数表示的相机位姿 v1(顶点).
const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[0]);
// 观测值:特征点当前帧中的坐标.
Vector2d obs(_measurement);
// 重投影误差,.map() 是将地图点的世界坐标转换到相机坐标系,cam_project() 操作将其投影到当前帧.
_error = obs-cam_project(v1->estimate().map(Xw));
}
// 检查地图点在相机坐标系中的深度是否为正.
bool isDepthPositive()
{
const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[0]);
return (v1->estimate().map(Xw))(2)>0.0;
}
// 雅克比矩阵:重投影误差相对于相机位姿的一阶导.
virtual void linearizeOplus();
// 投影到当前帧.
Vector2d cam_project(const Vector3d & trans_xyz) const;
Vector3d Xw; // 地图点的世界坐标.
double fx, fy, cx, cy; // 相机参数.
}; // 边:重投影误差(仅用于优化相机位姿时).
其中 linearizeOplus() 函数构造雅克比矩阵,对应 g2o/types/types_six_dof_expmap.cpp
中的 void EdgeSE3ProjectXYZOnlyPose::linearizeOplus()
// BRIEF 优化相机位姿是重投影误差的雅克比矩阵(对相机位姿的偏导)
void EdgeSE3ProjectXYZOnlyPose::linearizeOplus()
{
// 顶点:相机位姿.
VertexSE3Expmap * vi = static_cast<VertexSE3Expmap *>(_vertices[0]);
// 地图点的相机坐标.
Vector3d xyz_trans = vi->estimate().map(Xw);
double x = xyz_trans[0];
double y = xyz_trans[1];
double invz = 1.0/xyz_trans[2];
double invz_2 = invz*invz;
_jacobianOplusXi(0,0) = x*y*invz_2 *fx;
_jacobianOplusXi(0,1) = -(1+(x*x*invz_2)) *fx;
_jacobianOplusXi(0,2) = y*invz *fx;
_jacobianOplusXi(0,3) = -invz *fx;
_jacobianOplusXi(0,4) = 0;
_jacobianOplusXi(0,5) = x*invz_2 *fx;
_jacobianOplusXi(1,0) = (1+y*y*invz_2) *fy;
_jacobianOplusXi(1,1) = -x*y*invz_2 *fy;
_jacobianOplusXi(1,2) = -x*invz *fy;
_jacobianOplusXi(1,3) = 0;
_jacobianOplusXi(1,4) = -invz *fy;
_jacobianOplusXi(1,5) = y*invz_2 *fy;
} // 优化相机位姿是重投影误差的雅克比矩阵.
在 BA 优化时优化的量包括相机位姿和地图点坐标,重投影误差是一个二元边,连接的顶点为李代数表示的相机位姿和三维向量表示的地图点坐标。对应 g2o/types/types_six_dof_expmap.h
中定义的边 EdgeSE3ProjectXYZ,其定义格式与前面自定义边中举例二的二元边一样。
// BRIEF 边:重投影误差(BA优化时)
class EdgeSE3ProjectXYZ: public BaseBinaryEdge<2, Vector2d, VertexSBAPointXYZ, VertexSE3Expmap>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
EdgeSE3ProjectXYZ();
bool read(std::istream& is);
bool write(std::ostream& os) const;
// 误差计算函数
void computeError()
{
// 顶点 v1:相机位姿,顶点的 id 为 1.
const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
// 顶点 v2:地图点,顶点的 id 为 0.
const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
// 观测值:特征点在当前帧中的像素坐标.
Vector2d obs(_measurement);
// 计算误差:观测-重投影.
_error = obs-cam_project(v1->estimate().map(v2->estimate()));
}
// 检查地图点在相机坐标系中深度是否为正.
bool isDepthPositive()
{
const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
return (v1->estimate().map(v2->estimate()))(2)>0.0;
}
// 构造雅克比矩阵:重投影误差相对于相机坐标和地图点的一阶导.
virtual void linearizeOplus();
// 投影到当前帧.
Vector2d cam_project(const Vector3d & trans_xyz) const;
double fx, fy, cx, cy;
}; // 边:重投影误差(BA优化时).
其中 linearizeOplus()
函数用于构造雅克比矩阵,需要重投影误差对相机位姿和地图点求一阶偏导,对应 g2o/types/types_six_dof_expmap.cpp 中的 void EdgeSE3ProjectXYZ::linearizeOplus() 。
// BRIEF 重投影误差的雅克比矩阵(BA 优化时相对于相机位姿和地图点的一阶导)
void EdgeSE3ProjectXYZ::linearizeOplus()
{
VertexSE3Expmap * vj = static_cast<VertexSE3Expmap *>(_vertices[1]);
SE3Quat T(vj->estimate());
VertexSBAPointXYZ* vi = static_cast<VertexSBAPointXYZ*>(_vertices[0]);
Vector3d xyz = vi->estimate();
Vector3d xyz_trans = T.map(xyz);
double x = xyz_trans[0];
double y = xyz_trans[1];
double z = xyz_trans[2];
double z_2 = z*z;
Matrix<double,2,3> tmp;
tmp(0,0) = fx;
tmp(0,1) = 0;
tmp(0,2) = -x/z*fx;
tmp(1,0) = 0;
tmp(1,1) = fy;
tmp(1,2) = -y/z*fy;
_jacobianOplusXi = -1./z * tmp * T.rotation().toRotationMatrix();
_jacobianOplusXj(0,0) = x*y/z_2 *fx;
_jacobianOplusXj(0,1) = -(1+(x*x/z_2)) *fx;
_jacobianOplusXj(0,2) = y/z *fx;
_jacobianOplusXj(0,3) = -1./z *fx;
_jacobianOplusXj(0,4) = 0;
_jacobianOplusXj(0,5) = x/z_2 *fx;
_jacobianOplusXj(1,0) = (1+y*y/z_2) *fy;
_jacobianOplusXj(1,1) = -x*y/z_2 *fy;
_jacobianOplusXj(1,2) = -x/z *fy;
_jacobianOplusXj(1,3) = 0;
_jacobianOplusXj(1,4) = -1./z *fy;
_jacobianOplusXj(1,5) = y/z_2 *fy;
} // 重投影误差的雅克比矩阵(BA 优化时相对于相机位姿和地图点的一阶导)
闭环检测时执行 sim3 优化,边是重投影误差,顶点是 sim3 表示的相机位姿和地图点坐标,对应g2o/types/types_seven_dof_expmap.h
中定义的边投影到当前帧 EdgeSim3ProjectXYZ 和 投影到闭环帧 EdgeInverseSim3ProjectXYZ。
// BRIEF 边:重投影误差(闭环检测时)顶点:地图点坐标,sim3 表示的相机位姿.
class EdgeSim3ProjectXYZ : public BaseBinaryEdge<2, Vector2d, VertexSBAPointXYZ, VertexSim3Expmap>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
EdgeSim3ProjectXYZ();
virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
// 重投影误差计算.
void computeError()
{
const VertexSim3Expmap* v1 = static_cast<const VertexSim3Expmap*>(_vertices[1]);
const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
Vector2d obs(_measurement);
_error = obs-v1->cam_map1(project(v1->estimate().map(v2->estimate())));
}
// TODO 这里继承的 BaseBinaryEdge 雅克比矩阵构造函数?.
// virtual void linearizeOplus();
};
OptimizeEssentialGraph 会在成功进行闭环检测后、全局 BA 优化前进行,边为 Sim3 之间的相对误差,顶点为 Sim3 表示的相机位姿。对应 g2o/types/types_seven_dof_expmap.h 中定义的边 EdgeSim3。
// BRIEF 边:Sim3 之间的相对误差,顶点:Sim3 表示的 pose。(OptimizeEssentialGraph 优化中)
class EdgeSim3 : public BaseBinaryEdge<7, Sim3, VertexSim3Expmap, VertexSim3Expmap>
{
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
EdgeSim3();
virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
// 误差计算.
void computeError()
{
const VertexSim3Expmap* v1 = static_cast<const VertexSim3Expmap*>(_vertices[0]);
const VertexSim3Expmap* v2 = static_cast<const VertexSim3Expmap*>(_vertices[1]);
Sim3 C(_measurement);
Sim3 error_=C*v1->estimate()*v2->estimate().inverse();
_error = error_.log();
}
virtual double initialEstimatePossible(const OptimizableGraph::VertexSet& , OptimizableGraph::Vertex* ) { return 1.;}
virtual void initialEstimate(const OptimizableGraph::VertexSet& from, OptimizableGraph::Vertex* /*to*/)
{
VertexSim3Expmap* v1 = static_cast<VertexSim3Expmap*>(_vertices[0]);
VertexSim3Expmap* v2 = static_cast<VertexSim3Expmap*>(_vertices[1]);
if (from.count(v1) > 0)
v2->setEstimate(measurement()*v1->estimate());
else
v1->setEstimate(measurement().inverse()*v2->estimate());
}
}; // 边:Sim3 之间的相对误差,顶点:Sim3 表示的 pose。(OptimizeEssentialGraph 优化中)
函数描述:
顶点和边:
1. Vertex: g2o::VertexSE3Expmap(),即当前帧的 Tcw
2. Edge: 重投影误差(一元边)
- 单目情况:g2o::EdgeSE3ProjectXYZOnlyPose(),BaseUnaryEdge
+ Vertex:待优化当前帧的Tcw
+ measurement:MapPoint在当前帧中的二维位置(u,v)
+ InfoMatrix: invSigma2(与特征点所在的尺度有关)
- 双目情况:g2o::EdgeStereoSE3ProjectXYZOnlyPose(),BaseUnaryEdge
+ Vertex:待优化当前帧的Tcw
+ measurement:MapPoint在当前帧中的二维位置(ul,v,ur)
+ InfoMatrix: invSigma2(与特征点所在的尺度有关)
函数执行流程:
//步骤 1: 实例化一个 EdgeSE3ProjectXYZOnlyPose 一元边
g2o::EdgeSE3ProjectXYZOnlyPose* e = new g2o::EdgeSE3ProjectXYZOnlyPose();
//步骤 2: 设置边连接的顶点
e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(0)));
//步骤 3: 添加边的观测值(2D)
e->setMeasurement(obs); // 观测:地图点在当前帧中的像素坐标.
//步骤 4: 设置边的信息矩阵
const float invSigma2 = pFrame->mvInvLevelSigma2[kpUn.octave];
// note 设置权重(信息矩阵),与特征点金字塔有关,参考:https://www.zhihu.com/question/58762862
e->setInformation(Eigen::Matrix2d::Identity()*invSigma2); // 信息矩阵(权重)
//步骤 5: 设置核函数
//步骤 6: 获取地图点的世界坐标(3D)
cv::Mat Xw = pMP->GetWorldPos();
e->Xw[0] = Xw.at<float>(0);
e->Xw[1] = Xw.at<float>(1);
e->Xw[2] = Xw.at<float>(2);
//步骤 7: 将边添加到优化器中。
函数描述:
执行条件:在已经处理完队列中的最后一个关键帧之后,并且闭环检测线程没有请求停止局部建图线程,则开始对当前帧进行局部 BA 优化,或者当新的关键帧加入到 convisibility graph 时,在关键帧附近进行一次局部优化,如下图所示;
顶点与边
1. Vertex:
- g2o::VertexSE3Expmap(),LocalKeyFrames,即当前关键帧一级与当前关键帧相连的关键帧的位姿
- g2o::VertexSE3Expmap(),FixedCameras,即能观测到LocalMapPoints的关键帧(并且不属于LocalKeyFrames)的位姿,在优化中这些关键帧的位姿不变
- g2o::VertexSBAPointXYZ(),LocalMapPoints,即LocalKeyFrames能观测到的所有MapPoints的位置
2. Edge:
- 单目 g2o::EdgeSE3ProjectXYZ(),BaseBinaryEdge
+ Vertex:关键帧的Tcw,MapPoint的Pw
+ measurement:MapPoint在关键帧中的二维位置(u,v)
+ InfoMatrix: invSigma2(与特征点所在的尺度有关)
- 双目 g2o::EdgeStereoSE3ProjectXYZ(),BaseBinaryEdge
+ Vertex:关键帧的Tcw,MapPoint的Pw
+ measurement:MapPoint在关键帧中的二维位置(ul,v,ur)
+ InfoMatrix: invSigma2(与特征点所在的尺度有关)
函数执行流程
//步骤 1:将当前帧加入到列表中
list<KeyFrame*> lLocalKeyFrames;
lLocalKeyFrames.push_back(pKF);
//步骤 2:将与当前关键帧一级相连的关键帧加入到列表中
const vector<KeyFrame*> vNeighKFs = pKF->GetVectorCovisibleKeyFrames();
// 实例化一个 VertexSE3Expmap 顶点的对象.
g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap();
// 设置顶点的关键帧位姿属性(SE3 形式)、ID 和固定的顶点.
vSE3->setEstimate(Converter::toSE3Quat(pKFi->GetPose()));
vSE3->setId(pKFi->mnId);
vSE3->setFixed(pKFi->mnId==0); //第一帧位置固定
// 将顶点添加到的优化器中.
optimizer.addVertex(vSE3);
g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap();
vSE3->setEstimate(Converter::toSE3Quat(pKFi->GetPose()));
vSE3->setId(pKFi->mnId);
vSE3->setFixed(true);
optimizer.addVertex(vSE3);
// 实例化一个 VertexSBAPointXYZ 顶点.
g2o::VertexSBAPointXYZ* vPoint = new g2o::VertexSBAPointXYZ();
// 设置顶点的位置、ID,边缘化.
vPoint->setEstimate(Converter::toVector3d(pMP->GetWorldPos()));
int id = pMP->mnId+maxKFid+1;
vPoint->setId(id);
vPoint->setMarginalized(true);
// 将顶点添加到优化器中.
optimizer.addVertex(vPoint);
//步骤 1:实例化一条二元边 EdgeSE3ProjectXYZ
g2o::EdgeSE3ProjectXYZ* e = new g2o::EdgeSE3ProjectXYZ();
//步骤 2:为边添加关联的顶点
e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id)));// 地图点 ID.
e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(pKFi->mnId)));// 关键帧 ID.
//步骤 3:添加测量值
e->setMeasurement(obs); // 测量:地图点在当前帧中的二维位置.
//步骤 4:构造信息矩阵
const float &invSigma2 = pKFi->mvInvLevelSigma2[kpUn.octave]; // 与特征点所在的尺度有关.
e->setInformation(Eigen::Matrix2d::Identity()*invSigma2); // 设置信息矩阵.
//步骤 5:设置核函数
//步骤 6:将边添加到优化器中
optimizer.initializeOptimization();
optimizer.optimize(5); // 迭代 5 次.
optimizer.initializeOptimization(0);
optimizer.optimize(10);
函数描述
全局 BA 优化在单目初始化和闭环矫正时执行。在全局优化中,所有的关键帧(除了第一帧)和三维点都参与优化,如下图所示:
顶点与边
3D-2D 最小化重投影误差 e = (u,v) - project(Tcw*Pw)
1. Vertex: g2o::VertexSE3Expmap(),即当前帧的Tcw
g2o::VertexSBAPointXYZ(),MapPoint 的 mWorldPos
2. Edge:
- g2o::EdgeSE3ProjectXYZ(),BaseBinaryEdge
+ Vertex:待优化当前帧的Tcw
+ Vertex:待优化MapPoint的mWorldPos
+ measurement:MapPoint在当前帧中的二维位置(u,v)
+ InfoMatrix: invSigma2(与特征点所在的尺度有关)
函数执行流程
g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap();
vSE3->setEstimate(Converter::toSE3Quat(pKF->GetPose()));
vSE3->setId(pKF->mnId);
vSE3->setFixed(pKF->mnId==0); // 固定第一帧的位姿,其它帧可修正位姿
optimizer.addVertex(vSE3);
g2o::VertexSBAPointXYZ* vPoint = new g2o::VertexSBAPointXYZ();
vPoint->setEstimate(Converter::toVector3d(pMP->GetWorldPos()));
const int id = pMP->mnId+maxKFid+1;
vPoint->setId(id);
vPoint->setMarginalized(true);
optimizer.addVertex(vPoint);
//步骤 1:实例化一条边
g2o::EdgeSE3ProjectXYZ* e = new g2o::EdgeSE3ProjectXYZ();
//步骤 2:添加顶点
e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id)));
e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(pKF->mnId)));
//步骤 3:添加测量值
Eigen::Matrix<double,2,1> obs;
obs << kpUn.pt.x, kpUn.pt.y;
e->setMeasurement(obs);
//步骤 4:设置信息矩阵
const float &invSigma2 = pKF->mvInvLevelSigma2[kpUn.octave];
e->setInformation(Eigen::Matrix2d::Identity()*invSigma2);
//步骤 5:设置核函数
//步骤 6:添加到优化器中
函数描述
在用 RANSAC 求解过 Sim3,以及通过 Sim3 匹配更多的地图点后,对当前关键帧,闭环关键帧,以及匹配的地图点进行优化,获得更准确的 Sim3 变换,再进行下一步的闭环调整;
当检测到闭环时,闭环连接的两个关键帧的位姿需要通过 Sim3 优化(以使得其尺度一致),在函数 Optimizer::OptimizeSim3() 中实现,优化求解两帧之间的相似变换矩阵,使得二维对应点的投影误差最小;
如下图所示,Pos6 和 Pos2 为一个可能的闭环,则通过 ( u 4 , 2 , v 4 , 2 ) \left(u_{4,2}, v_{4,2}\right) (u4,2,v4,2)和 ( u 4 , 6 , v 4 , 6 ) \left(u_{4,6}, v_{4,6}\right) (u4,6,v4,6) 之间的投影误差来优化 S 6 , 2 S_{6,2} S6,2。
顶点与边:
1. Vertex:
- g2o::VertexSim3Expmap(),两个关键帧的位姿
- g2o::VertexSBAPointXYZ(),两个关键帧共有的 MapPoints
2. Edge:
- g2o::EdgeSim3ProjectXYZ(),BaseBinaryEdge,投影到当前帧
+ Vertex:关键帧的Sim3,MapPoint的Pw
+ measurement:MapPoint在关键帧中的二维位置(u,v)
+ InfoMatrix: invSigma2(与特征点所在的尺度有关)
- g2o::EdgeInverseSim3ProjectXYZ(),BaseBinaryEdge,投影到闭环帧
+ Vertex:关键帧的Sim3,MapPoint的Pw
+ measurement:MapPoint在关键帧中的二维位置(u,v)
+ InfoMatrix: invSigma2(与特征点所在的尺度有关)
函数执行流程
g2o::VertexSim3Expmap * vSim3 = new g2o::VertexSim3Expmap();
vSim3->_fix_scale=bFixScale;
vSim3->setEstimate(g2oS12); // 两个关键帧间的Sim3变换
vSim3->setId(0);
vSim3->setFixed(false);// 优化Sim3顶点
vSim3->_principle_point1[0] = K1.at<float>(0,2); // 光心横坐标cx
vSim3->_principle_point1[1] = K1.at<float>(1,2); // 光心纵坐标cy
vSim3->_focal_length1[0] = K1.at<float>(0,0); // 焦距 fx
vSim3->_focal_length1[1] = K1.at<float>(1,1); // 焦距 fy
vSim3->_principle_point2[0] = K2.at<float>(0,2);
vSim3->_principle_point2[1] = K2.at<float>(1,2);
vSim3->_focal_length2[0] = K2.at<float>(0,0);
vSim3->_focal_length2[1] = K2.at<float>(1,1);
optimizer.addVertex(vSim3);
函数描述
顶点与边:
1. Vertex:
- g2o::VertexSim3Expmap,Essential graph中关键帧的位姿
2. Edge:
- g2o::EdgeSim3(),BaseBinaryEdge
+ Vertex:关键帧的Tcw,MapPoint的Pw
+ measurement:经过CorrectLoop函数步骤2,Sim3传播校正后的位姿
+ InfoMatrix: 单位矩阵
函数执行流程