ORB-SLAM2代码阅读笔记:优化

ORBSLAM是一种基于优化方法的SLAM方法,工程中引入了第三方库g2o,g2o是基于图优化的优化算法库。图优化是将普通的优化问题用图的方式(变量用节点表示,关系用边来表示)来表示。

  • void Optimizer::BundleAdjustment
    3D-2D BA,在GlobalBundleAdjustemnt中调用,计算量比较大
    向优化器添加关键帧位姿顶点

    g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap();
    vSE3->setEstimate(Converter::toSE3Quat(pKF->GetPose()));
    vSE3->setId(pKF->mnId);
    vSE3->setFixed(pKF->mnId==0);
    optimizer.addVertex(vSE3);
    

    向优化器添加MapPoints顶点

    g2o::VertexSBAPointXYZ* vPoint = new g2o::VertexSBAPointXYZ();
    vPoint->setEstimate(Converter::toVector3d(pMP->GetWorldPos()));
    const int id = pMP->mnId+maxKFid+1;//避免和位姿顶点ID重复
    vPoint->setId(id);
    vPoint->setMarginalized(true);
    optimizer.addVertex(vPoint);
    

    在添加每个MapPoints顶点时遍历观察到当前地图点的所有关键帧,向优 化器中添加边。这条边连着3D地图点和位姿,由于观测数据格式不同,所 以这里要分成单目和双目两种情况添加边。
    创建边并填充数据

    g2o::EdgeSE3ProjectXYZ* e = new g2o::EdgeSE3ProjectXYZ();
    e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>  (optimizer.vertex(id)));
    e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>  (optimizer.vertex(pKF->mnId)));
    

    观测数据需分两种情况:

    • 单目:投影点的x、y坐标
     // 构造观测
    Eigen::Matrix<double,2,1> obs;
    obs << kpUn.pt.x, kpUn.pt.y;      
    
    • 双目:投影点的x、y坐标,以及投影点在右目中的x坐标(默认y方向上已经对齐了)
       Eigen::Matrix<double,3,1> obs;
    const float kp_ur = pKF->mvuRight[mit->second];
    obs << kpUn.pt.x, kpUn.pt.y, kp_ur;
    

    顶点和边设置完后进行优化,获取优化后的位姿和地图点进行保存。

  • int Optimizer::OptimizeSim3(KeyFrame *pKF1, KeyFrame *pKF2, vector<MapPoint *> &vpMatches1, g2o::Sim3 &g2oS12, const float th2, const bool bFixScale)
    形成闭环时进行Sim3优化,优化目标是是两关键帧之间的相似变换矩阵

    • 设置优化器算法
    • 将变量(当前待优化帧的初始位姿)作为非固定节点添加进入图优化
    • 将变量(两关键帧的地图点)作为固定节点添加进入图优化
    • 添加边将两关键帧地图点和待优化帧的位姿连接,每一个位姿对应两条边,从关键帧1像素坐标映射到关键帧2像素坐标,从关键帧2像素坐标映射到关键帧1像素坐标。
    // step 2.3 添加两个顶点(3D点)到相机投影的边 -- 投影到当前关键帧 -- 正向投影
        g2o::EdgeSim3ProjectXYZ* e12 = new g2o::EdgeSim3ProjectXYZ();
        e12->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id2)));
        // ? 没看懂为什么这里添加的节点的id为0?
        e12->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(0)));
        e12->setMeasurement(obs1);
        // 信息矩阵也和这个点的可靠程度(在图像金字塔中的图层)有关
        const float &invSigmaSquare1 = pKF1->mvInvLevelSigma2[kpUn1.octave];
        e12->setInformation(Eigen::Matrix2d::Identity()*invSigmaSquare1);
    
        // 使用鲁棒核函数
        g2o::RobustKernelHuber* rk1 = new g2o::RobustKernelHuber;
        e12->setRobustKernel(rk1);
        rk1->setDelta(deltaHuber);
        optimizer.addEdge(e12);
    
        // Set edge x2 = S21*X1
        // 接下来是添加投影到 闭环关键帧 -- 反向投影
        Eigen::Matrix<double,2,1> obs2;
        const cv::KeyPoint &kpUn2 = pKF2->mvKeysUn[i2];
        obs2 << kpUn2.pt.x, kpUn2.pt.y;
    
        g2o::EdgeInverseSim3ProjectXYZ* e21 = new g2o::EdgeInverseSim3ProjectXYZ();
    
        e21->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id1)));
        // ? 这里添加的节点id也为0
        e21->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(0)));
        e21->setMeasurement(obs2);
        float invSigmaSquare2 = pKF2->mvInvLevelSigma2[kpUn2.octave];
        e21->setInformation(Eigen::Matrix2d::Identity()*invSigmaSquare2);
    
        g2o::RobustKernelHuber* rk2 = new g2o::RobustKernelHuber;
        e21->setRobustKernel(rk2);
        rk2->setDelta(deltaHuber);
        optimizer.addEdge(e21);
    
        vpEdges12.push_back(e12);
        vpEdges21.push_back(e21);
        vnIndexEdge.push_back(i);
    
    • 开始迭代优化,迭代下降5次,然后根据当前优化结果位姿筛选内点,用内点重新进行迭代下降优化,优化结果为计算得到的两关键帧之间的相似变换矩阵
  • void Optimizer::LocalBundleAdjustment(KeyFrame pKF, bool pbStopFlag, Map* pMap)
    局部BA优化(tracking线程),得到当前帧连接的KF和这些 KF中的地图点后(lLocalKeyFrames和lLocalMapPoints),和能观测到这些地图点但没有和当前帧相连的KF(lFixedCameras)。

    • 将变量(当前关键帧及其共视关键帧的初始位姿)作为非固定(初始帧固定)节点添加进入图优化,这里如果帧的id是0(第一帧),则固定:
          vSE3->setFixed(pKFi->mnId==0);//第一帧位置固定
    

    固定的原因应该是,如果第一帧不固定,优化后的结果乘上一个变换,同样是最优解,也就是说如果第一帧不固定,会有很多很多可能的解

    • 将变量(观察到当前关键帧及其共视关键帧中地图点的关键帧初始位姿)作为固定节点添加进入图优化
    • 将变量(当前关键帧及其共视关键帧的地图点)作为非固定节点添加进入图优化
    • 在添加每个地图点时遍历关键帧,创建边并填充数据(和BA一样)
    • 开始迭代优化,迭代下降5次,然后根据当前优化结果位姿筛选内点,用内点重新进行迭代下降优化。
    • 恢复优化后的各关键帧位姿和关键帧地图点。
  • int Optimizer::PoseOptimization(Frame *pFrame)
    用于Tracking中匀速运动模型跟踪等,这个优化中只优化Frame的Tcw,不优化MapPoints的坐标,所以在构造图优化的时候,是构造的一元边。观测是2维的Vector2d数据,即像素坐标。

    误差函数为:

    添加当前位姿顶点,遍历地图点添加边和观测,这里也是分成单目和双目两种情况,因为观测数据不同以及定义的边的类型不同:
    • 单目:
    g2o::EdgeSE3ProjectXYZOnlyPose* e = new     g2o::EdgeSE3ProjectXYZOnlyPose();
    
    • 双目:
    g2o::EdgeStereoSE3ProjectXYZOnlyPose* e = new g2o::EdgeStereoSE3ProjectXYZOnlyPose();
    
  • Optimizer::OptimizeEssentialGraph
    闭环检测后的优化
    首先拿到地图中所有KF和地图点,声明vScw和vCorrectedSwc,分别代表未sim3优化前的sim3位姿和优化后的。

    添加顶点时有两种情况:如果该关键帧在闭环时通过Sim3传播调整过,用校正后的位姿;如果该关键帧在闭环时没有通过Sim3传播调整过,用自身的位姿

          if(it!=CorrectedSim3.end())
          {
              vScw[nIDi] = it->second;
              VSim3->setEstimate(it->second);
          }
          else
          {
              Eigen::Matrix<double,3,3> Rcw = Converter::toMatrix3d(pKF->GetRotation());
              Eigen::Matrix<double,3,1> tcw = Converter::toVector3d(pKF->GetTranslation());
              g2o::Sim3 Siw(Rcw,tcw,1.0);
              vScw[nIDi] = Siw;
              VSim3->setEstimate(Siw);
          }
    

    连接两个Sim3节点的二元边,顶点0的位姿是Tiw,1的位姿是Tjw,那么边的测量是Tji,即from i to j。

              const g2o::Sim3 Sjw = vScw[nIDj];
              // 得到两个pose间的Sim3变换
              const g2o::Sim3 Sji = Sjw * Swi;
    
              g2o::EdgeSim3* e = new g2o::EdgeSim3();
              e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(nIDj)));
              e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(nIDi)));
              // 根据两个Pose顶点的位姿算出相对位姿作为边,那还存在误差?优化有用?因为闭环MapPoints调整新形成的边不优化?(wubo???)
              // REVIEW 我和师兄有同样的疑问
              e->setMeasurement(Sji);
    
              // 信息矩阵是单位阵,说明sim3中每个自由度的贡献都是一样的,并且所有的这个边对总误差的贡献也都是一样大的
              e->information() = matLambda;
    
              optimizer.addEdge(e);
    

图优化中的鲁棒核函数

SLAM中可能给出错误的边。SLAM中的数据关联让科学家头疼了很长时间。出于变化、噪声等原因,机器人并不能确定它看到的某个路标,如果出现错误,就会出现一条误差很大的边,然后试图调整这条边所连接的节点的估计值,使它们顺应这条边的无理要求。由于这个边的误差真的很大,往往会抹平了其他正确边的影响,使优化算法专注于调整一个错误的值。
核函数作用就是保证每条边的误差不会大的没边,掩盖掉其他的边。具体的方式是,把原先误差的二范数度量,替换成一个增长没有那么快的函数,同时保证自己的光滑性质(不然没法求导啊!)。因为它们使得整个优化结果更为鲁棒,所以又叫它们为robust kernel(鲁棒核函数)。很多鲁棒核函数都是分段函数,在输入较大时给出线性的增长速率,例如cauchy核,huber核等等。当然具体的我们也不展开细说了。

这段摘自 深入理解图优化与g2o:图优化篇

高斯牛顿法

  • 牛顿法

    牛顿法是从泰勒公式展开得到的
    迭代式为:

上式为一维的情况,扩展到多维时牛顿法的迭代式为:

其中H为海塞矩阵,
为梯度
  • 高斯牛顿法

    高斯牛顿法通过下面方法替代海塞矩阵:
  • LM法
    高斯-牛顿法中为了避免发散,有两种解决方法
    1.调整下降步伐:β^{s+1}=βs+α Δ.0<α<1
    2.调整下降方向:4(JTJ+λD)Δ=JTrλ→+∞时:Δ/λ→J^Tr$,即方向和梯度方向一样,变成了梯度下降法。

相反,如果λ为0,就变成了高斯牛顿法。
Levenberg-Marquardt方法的好处在于可以调节:
如果下降太快,使用较小的λ,使之更接近高斯牛顿法
如果下降太慢,使用较大的λ,使之更接近梯度下降法

此外,高斯牛顿法中涉及求逆矩阵的操作,(J^TJ) 加入λ 也可以保证该矩阵为一个正定矩阵。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容