首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

全局优化

按说在上一节中,我们完成了基图的图优化之后,后端的工作就结束了。但作者又在最后开启了一个线程来进行全局的 BA 优化。 不太清楚作者的本意是啥,我猜作者是在想,回环不是想有就能有的,逮着一个机会就可劲儿的提高系统精度。反正在没有回环产生的日子里,后端优化闲着也是闲着,要真是在全局优化过程中再次产生了回环, 我停下来重新弄也来得及。

本文中,我们就来看看这最后一个优化都干了些什么事情。按照老套路,先总体了解一下业务,再详细分析具体完成优化的函数实现。 我个人并不怎么喜欢这段代码,总觉得它潜在的风险挺多的。

1. 全局优化的整体过程

闭环修正的时候,我们就看到 LoopClosing 的成员函数 CorrectLoop 在最后的时候,创建了线程 mpThreadGBA, 以当前关键帧的 id 为输入参数,调用 RunGlobalBundleAdjustment 完成最后的全局优化。

        mpThreadGBA = new thread(&LoopClosing::RunGlobalBundleAdjustment,this,mpCurrentKF->mnId);

下面是这个函数的代码片段,它有一个传参 nLoopKF,是当前关键帧的 id。 这里忍不住想吐槽一下这个命名,要不是专门看了一下传入的实参,我还一直以为它是闭环帧的id。 在函数的一开始输出了一行日志之后,用局部变量 idx 记录下 mnFullBAIdx,并调用函数 Optimizer::GlobalBundleAdjustemnt 完成闭环优化。再之后就是优化结束,更新地图了。 整个过程十分直接。

        void LoopClosing::RunGlobalBundleAdjustment(unsigned long nLoopKF) {
            cout && "Starting Global Bundle Adjustment" && endl;
            int idx =  mnFullBAIdx;
            Optimizer::GlobalBundleAdjustemnt(mpMap,10,&mbStopGBA,nLoopKF,false);

这个 mnFullBAIdx 本身是个布尔变量,在这里非得当个整型使。刚开始的时候,我寻思着, 这个 ORB-SLAM2 的代码稍微有点老,它的依赖现在都有了很多更新,干脆都迁移到新版本上好了。先用 C++17 编译,就因为 mnFullBAIdx 非要当个整型使,搞得我编不过去。 我想,既然你想当整型,那我就让你痛痛快快地,当个整形好了。嘁哧咔嚓,我把头文件给改了, 一顿骚操作,编译通过了。一运行,嘿,没跑起来!退一步,我又给改回来了,用 C++14 编译了一遍把这个问题给回避了。

后来,我研究了一下这个 mnFullBAIdx,合着它在第一次执行 ++ 运算之后就一直是 1 了,之后就再也没有变过。所以直接改成整型,它运行的逻辑就发生了重大的变化。 最直接的影响,就是下面第 6 行的这个判定条件,在布尔变量的逻辑下,只要发生过一次闭环,之后这个条件就不管用了。而在整型变量的逻辑下,mnFullBAIdx++ 和这个判定条件是在两个线程中进行的, 就有一定的概率,导致该条件生效,直接退出闭环优化。

            {
                unique_lock<mutex> lock(mMutexGBA);
                if (idx != mnFullBAIdx) return;

成员变量 mbStopGBA 用于标记是否需要终止全局优化。若该值为 true,那么上面第4行的全局优化可能还没有结束就被打断了。此时意味着检测到新的闭环了,这个算了一半的结果就被抛弃了。 因为接下来要更新地图,所以在第 9 行中通过接口 RequestStop 通知局部地图管理器先暂停一下,第10、11行等局部地图管理器停下来之后再进行更新。

                 if (!mbStopGBA) {
                    mpLocalMapper->RequestStop();
                    while (!mpLocalMapper->isStopped() && !mpLocalMapper->isFinished())
                        usleep(1000);

对地图对象的互斥量 mMutexMapUpdate 加锁。并从地图对象的成员 mvpKeyFrameOrigins 中记录的初始化关键帧开始,通过广度优先搜索遍历整个生成树,更新各关键帧的位姿。 局部容器 lpKFtoCheck 用于记录已扩展待检查的关键帧对象。

                    unique_lock<mutex> lock(mpMap->mMutexMapUpdate);
                    list<KeyFrame*> lpKFtoCheck(mpMap->mvpKeyFrameOrigins.begin(), mpMap->mvpKeyFrameOrigins.end());
                    while (!lpKFtoCheck.empty()) {
                        KeyFrame* pKF = lpKFtoCheck.front();
                        const set<KeyFrame*> sChilds = pKF->GetChilds();
                        cv::Mat Twc = pKF->GetPoseInverse();
                        for (auto sit = sChilds.begin(); sit != sChilds.end(); sit++) {
                            KeyFrame* pChild = *sit;
                            if (pChild->mnBAGlobalForKF != nLoopKF) {
                                cv::Mat Tchildc = pChild->GetPose() * Twc;
                                pChild->mTcwGBA = Tchildc * pKF->mTcwGBA; // *Tcorc*pKF->mTcwGBA;
                                pChild->mnBAGlobalForKF=nLoopKF;

                            }
                            lpKFtoCheck.push_back(pChild);
                        }
                        pKF->mTcwBefGBA = pKF->GetPose();
                        pKF->SetPose(pKF->mTcwGBA);
                        lpKFtoCheck.pop_front();
                    }

接下来,在一个 for 循环中完成地图点的 3D 坐标的更新。

                    const vector<MapPoint*> vpMPs = mpMap->GetAllMapPoints();
                    for(size_t i = 0; i < vpMPs.size(); i++) {
                        MapPoint* pMP = vpMPs[i];
                        if (pMP->isBad()) continue;

                        if (pMP->mnBAGlobalForKF == nLoopKF) {
                            // If optimized by Global BA, just update
                            pMP->SetWorldPos(pMP->mPosGBA);
                        } else {
                            // Update according to the correction of its reference keyframe
                            KeyFrame* pRefKF = pMP->GetReferenceKeyFrame();
                            if (pRefKF->mnBAGlobalForKF != nLoopKF)
                                continue;
                            // Map to non-corrected camera
                            cv::Mat Rcw = pRefKF->mTcwBefGBA.rowRange(0,3).colRange(0,3);
                            cv::Mat tcw = pRefKF->mTcwBefGBA.rowRange(0,3).col(3);
                            cv::Mat Xc = Rcw*pMP->GetWorldPos()+tcw;
                            // Backproject using corrected camera
                            cv::Mat Twc = pRefKF->GetPoseInverse();
                            cv::Mat Rwc = Twc.rowRange(0,3).colRange(0,3);
                            cv::Mat twc = Twc.rowRange(0,3).col(3);
                            pMP->SetWorldPos(Rwc*Xc+twc);
                        }
                    }

最后,通过接口 InformNewBigChange 通知地图对象完成了全局闭环,并告知局部地图管理器,可以接着更新局部地图了。

                    mpMap->InformNewBigChange();
                    mpLocalMapper->Release();
                    cout << "Map updated!" << endl;
                }
                mbFinishedGBA = true;
                mbRunningGBA = false;
        }   }

2. 全局 BA 优化的实现

下面是函数 Optimizer::GlobalBundleAdjustemnt 的实现,这个函数只是一个壳子,它从传入的地图对象中获取了所有的关键帧和地图点之后,就调用了另一个函数 Optimizer::BundleAdjustment。 参数 nIterations 限定了最大的迭代次数,布尔型的指针 pbStopFlag 是一个用来提前结束优化的变量,nLoopKF 是触发闭环的关键帧,bRobust 表示优化的过程中是否需要使用鲁棒核函数。

        void Optimizer::GlobalBundleAdjustemnt(Map* pMap, int nIterations, bool* pbStopFlag,
                                               const unsigned long nLoopKF, const bool bRobust) {
            vector vpKFs = pMap->GetAllKeyFrames();
            vector vpMP = pMap->GetAllMapPoints();
            BundleAdjustment(vpKFs,vpMP,nIterations,pbStopFlag, nLoopKF, bRobust);
        }

下面是函数 Optimizer::BundleAdjustment 的代码片段。该函数一开始定义了一个局部的容器 vbNotIncludedMP 用于记录那些不参与优化的地图点。

接着用我们熟悉的配方[ 1, 2]构建了 g2o 的优化器和求解器对象。

        void Optimizer::BundleAdjustment(const vector<KeyFrame *> & vpKFs, const vector<MapPoint *> & vpMP,
                                 int nIterations, bool* pbStopFlag, const unsigned long nLoopKF, const bool bRobust) {
            vector<bool> vbNotIncludedMP;
            vbNotIncludedMP.resize(vpMP.size());
            g2o::SparseOptimizer optimizer;
            g2o::BlockSolver_6_3::LinearSolverType * linearSolver;
            linearSolver = new g2o::LinearSolverEigen<g2o::BlockSolver_6_3::PoseMatrixType>();
            g2o::BlockSolver_6_3 * solver_ptr = new g2o::BlockSolver_6_3(linearSolver);
            g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg(solver_ptr);
            optimizer.setAlgorithm(solver);

如果输入参数 pbStopFlag 不是空指针,说明调用的时候指定了用来提前结束优化的变量,这里需要通过接口 setForceStopFlag 告知 optimizer。

            if (pbStopFlag)
                optimizer.setForceStopFlag(pbStopFlag);

接下来遍历所有的关键帧,构建节点。局部变量 maxKFid 用于记录关键帧中最大的 id。

            long unsigned int maxKFid = 0;
            for(size_t i = 0; i < vpKFs.size(); i++) {
                KeyFrame* pKF = vpKFs[i];
                if (pKF->isBad())
                    continue;
                g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap();
                vSE3->setEstimate(Converter::toSE3Quat(pKF->GetPose()));
                vSE3->setId(pKF->mnId);
                vSE3->setFixed(pKF->mnId==0);
                optimizer.addVertex(vSE3);
                if(pKF->mnId > maxKFid)
                    maxKFid = pKF->mnId;
            }

下面的代码片段,在一个 for 循环中遍历所有的地图点构建节点,以及描述关键帧与地图点几何约束的连边。局部变量 thHuber2D 和 thHuber3D 分别是自由度为2和3的卡方分布95%置信度阈值, 用于配置鲁棒核函数。有时可能受到地图点融合等多方面的影响吧,导致一个地图点被所有的关键帧抛弃了, 此时就会在第90行,根据局部变量 nEdges 将它从优化器中移除。

            const float thHuber2D = sqrt(5.99);
            const float thHuber3D = sqrt(7.815);
            for (size_t i = 0; i < vpMP.size(); i++) {
                MapPoint* pMP = vpMP[i];
                if (pMP->isBad()) continue;
                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);
                const map<KeyFrame*,size_t> observations = pMP->GetObservations();
                int nEdges = 0;
                for(auto mit = observations.begin(); mit != observations.end(); mit++) {
                    KeyFrame* pKF = mit->first;
                    if (pKF->isBad() || pKF->mnId > maxKFid)
                        continue;
                    nEdges++;
                    const cv::KeyPoint & kpUn = pKF->mvKeysUn[mit->second];
                    if (pKF->mvuRight[mit->second] < 0) {
                        Eigen::Matrix<double,2,1> obs;
                        obs << kpUn.pt.x, kpUn.pt.y;
                        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)));
                        e->setMeasurement(obs);
                        const float & invSigma2 = pKF->mvInvLevelSigma2[kpUn.octave];
                        e->setInformation(Eigen::Matrix2d::Identity() * invSigma2);

                        if(bRobust) {
                            g2o::RobustKernelHuber* rk = new g2o::RobustKernelHuber;
                            e->setRobustKernel(rk);
                            rk->setDelta(thHuber2D);
                        }
                        e->fx = pKF->fx;
                        e->fy = pKF->fy;
                        e->cx = pKF->cx;
                        e->cy = pKF->cy;
                        optimizer.addEdge(e);
                    } else {
                        Eigen::Matrix<double,3,1> obs;
                        const float kp_ur = pKF->mvuRight[mit->second];
                        obs << kpUn.pt.x, kpUn.pt.y, kp_ur;
                        g2o::EdgeStereoSE3ProjectXYZ* e = new g2o::EdgeStereoSE3ProjectXYZ();
                        e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id)));
                        e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(pKF->mnId)));
                        e->setMeasurement(obs);
                        const float & invSigma2 = pKF->mvInvLevelSigma2[kpUn.octave];
                        Eigen::Matrix3d Info = Eigen::Matrix3d::Identity() * invSigma2;
                        e->setInformation(Info);

                        if(bRobust) {
                            g2o::RobustKernelHuber* rk = new g2o::RobustKernelHuber;
                            e->setRobustKernel(rk);
                            rk->setDelta(thHuber3D);
                        }
                        e->fx = pKF->fx;
                        e->fy = pKF->fy;
                        e->cx = pKF->cx;
                        e->cy = pKF->cy;
                        e->bf = pKF->mbf;
                        optimizer.addEdge(e);
                    }
                }
                if (nEdges == 0) {
                    optimizer.removeVertex(vPoint);
                    vbNotIncludedMP[i]=true;
                } else {
                    vbNotIncludedMP[i]=false;
            }   }

上述的 for 循环运行结束之后,就完成了图优化问题的构建。下面两行代码完成具体优化迭代。

            optimizer.initializeOptimization();
            optimizer.optimize(nIterations);

最后,从优化器中取出关键帧的位姿和地图点的空间坐标,更新地图对象。

            for (size_t i = 0; i < vpKFs.size(); i++) {
                KeyFrame* pKF = vpKFs[i];
                if(pKF->isBad())
                    continue;
                g2o::VertexSE3Expmap* vSE3 = static_cast<g2o::VertexSE3Expmap*>(optimizer.vertex(pKF->mnId));
                g2o::SE3Quat SE3quat = vSE3->estimate();
                if (nLoopKF == 0) {
                    pKF->SetPose(Converter::toCvMat(SE3quat));
                } else {
                    pKF->mTcwGBA.create(4,4,CV_32F);
                    Converter::toCvMat(SE3quat).copyTo(pKF->mTcwGBA);
                    pKF->mnBAGlobalForKF = nLoopKF;
            }   }
            for (size_t i = 0; i < vpMP.size(); i++) {
                if(vbNotIncludedMP[i]) continue;
                MapPoint* pMP = vpMP[i];
                if(pMP->isBad())
                    continue;
                g2o::VertexSBAPointXYZ* vPoint = static_cast<g2o::VertexSBAPointXYZ*>(optimizer.vertex(pMP->mnId+maxKFid+1));
                if (nLoopKF == 0) {
                    pMP->SetWorldPos(Converter::toCvMat(vPoint->estimate()));
                    pMP->UpdateNormalAndDepth();
                } else {
                    pMP->mPosGBA.create(3,1,CV_32F);
                    Converter::toCvMat(vPoint->estimate()).copyTo(pMP->mPosGBA);
                    pMP->mnBAGlobalForKF = nLoopKF;
            }   }
        }

3. 完

本文中我们研究了 ORB-SLAM2 中最后一个优化。并没有什么具体的知识点,其核心的 BA 图优化, 我们已经在前文[1, 2, 3]中多次见到,套路应该很熟了。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1