zoukankan      html  css  js  c++  java
  • 从另一个角度看ORB-SLAM3——第1帧及单目初始化

    上一篇:从另一个角度看ORB-SLAM3——第0帧

    源码:https://github.com/UZ-SLAMLab/ORB_SLAM3

    处理完第0帧后,类似地,第1帧和0到1帧之间所有时刻的IMU测量值都会被系统读取,在TrackMonocular()函数中进行处理,从而完成系统的单目初始化。我们来看看这一过程都经过了哪些函数。

    1. GrabImageMonocular()

    前面的部分不再重复,直接进入函数GrabImageMonocular()。首先将第1帧转化成灰度图,并创建当前帧。这其中包括为每一帧提取ORB特征点和描述子,具体实现可以看代码,入口在Frame的构造函数里:

    ExtractORB(0,imGray,0,1000);
    

    然后调用Track()函数估计第1帧的位姿Tcw

    2. Track()

    记住此时系统状态是NOT_INITIALIZED,因此会执行和读取第0帧时不同的条件语句主体。

    if(mState!=NO_IMAGES_YET)
    {
        if(mLastFrame.mTimeStamp>mCurrentFrame.mTimeStamp)
        {
            cerr << "ERROR: Frame with a timestamp older than previous frame detected!" << endl;
            unique_lock<mutex> lock(mMutexImuQueue);
            mlQueueImuData.clear();
            CreateMapInAtlas();
            return;
        }
        else if(mCurrentFrame.mTimeStamp>mLastFrame.mTimeStamp+1.0)
        {
            cout << "id last: " << mLastFrame.mnId << "    id curr: " << mCurrentFrame.mnId << endl;
            if(mpAtlas->isInertial())
            {
    
                if(mpAtlas->isImuInitialized())
                {
                    cout << "Timestamp jump detected. State set to LOST. Reseting IMU integration..." << endl;
                    if(!pCurrentMap->GetIniertialBA2())
                    {
                        mpSystem->ResetActiveMap();
                    }
                    else
                    {
                        CreateMapInAtlas();
                    }
                }
                else
                {
                    cout << "Timestamp jump detected, before IMU initialization. Reseting..." << endl;
                    mpSystem->ResetActiveMap();
                }
            }
    
            return;
        }
    }
    

    首先检查时间戳是否有问题。这里分为2种情况:(1)当前帧的时间戳小于上一帧mLastFrame,显而易见这是不合理的,表示时间戳出错,调用CreateMapInAtlas()函数新建地图、重置所有状态并返回到上一层;(2)当前帧和上一帧之间时间差超过1秒,表示时间戳出现跳跃。此时若IMU已经初始化过了且完成了第2次Iniertial BA,就重置Active Map。若还没有做BA,或者IMU还没有初始化,就调用CreateMapInAtlas()新建地图。经过这一过程,系统状态将会变成NO_IMAGES_YET。

    如果一切顺利,系统下一步会调用PreintegratedIMU()计算第0帧和第1帧之间的预积分。预积分的推导也是个很复杂的部分,有时间再整理,这里可以参考ORB-SLAM3的论文和Christian Forster的《IMU Preintegration on Manifold for Efficient Visual-Inertial Maximum-a-Posteriori Estimation》补充材料。简要看下这个函数都计算了什么。

    3. PreintegrateIMU()

    while(true)
    {
        bool bSleep = false;
        {
            unique_lock<mutex> lock(mMutexImuQueue);
            if(!mlQueueImuData.empty())
            {
                IMU::Point* m = &mlQueueImuData.front();
                cout.precision(17);
                if(m->t<mCurrentFrame.mpPrevFrame->mTimeStamp-0.001l)
                {
                    mlQueueImuData.pop_front();
                }
                else if(m->t<mCurrentFrame.mTimeStamp-0.001l)
                {
                    mvImuFromLastFrame.push_back(*m);
                    mlQueueImuData.pop_front();
                }
                else
                {
                    mvImuFromLastFrame.push_back(*m);
                    break;
                }
            }
            else
            {
                break;
                bSleep = true;
            }
        }
        if(bSleep)
            usleep(500);
    }
    

    while()循环的主体就干了一件事,逐个检查mlQueueImuData中的IMU测量值的时间戳,将位于第0帧和第1帧之间的数据存储到mvImuFromLastFrame中。

    由于采用中值积分的方式计算预积分,需要取不同时刻间的平均值作为新的加速度、角速度测量值,实现部分如下:

    if((i==0) && (i<(n-1)))
    {
        float tab = mvImuFromLastFrame[i+1].t-mvImuFromLastFrame[i].t;
        float tini = mvImuFromLastFrame[i].t-mCurrentFrame.mpPrevFrame->mTimeStamp;
        acc = (mvImuFromLastFrame[i].a+mvImuFromLastFrame[i+1].a-
                (mvImuFromLastFrame[i+1].a-mvImuFromLastFrame[i].a)*(tini/tab))*0.5f;
        angVel = (mvImuFromLastFrame[i].w+mvImuFromLastFrame[i+1].w-
                (mvImuFromLastFrame[i+1].w-mvImuFromLastFrame[i].w)*(tini/tab))*0.5f;
        tstep = mvImuFromLastFrame[i+1].t-mCurrentFrame.mpPrevFrame->mTimeStamp;
    }
    else if(i<(n-1))
    {
        acc = (mvImuFromLastFrame[i].a+mvImuFromLastFrame[i+1].a)*0.5f;
        angVel = (mvImuFromLastFrame[i].w+mvImuFromLastFrame[i+1].w)*0.5f;
        tstep = mvImuFromLastFrame[i+1].t-mvImuFromLastFrame[i].t;
    }
    else if((i>0) && (i==(n-1)))
    {
        float tab = mvImuFromLastFrame[i+1].t-mvImuFromLastFrame[i].t;
        float tend = mvImuFromLastFrame[i+1].t-mCurrentFrame.mTimeStamp;
        acc = (mvImuFromLastFrame[i].a+mvImuFromLastFrame[i+1].a-
                (mvImuFromLastFrame[i+1].a-mvImuFromLastFrame[i].a)*(tend/tab))*0.5f;
        angVel = (mvImuFromLastFrame[i].w+mvImuFromLastFrame[i+1].w-
                (mvImuFromLastFrame[i+1].w-mvImuFromLastFrame[i].w)*(tend/tab))*0.5f;
        tstep = mCurrentFrame.mTimeStamp-mvImuFromLastFrame[i].t;
    }
    else if((i==0) && (i==(n-1)))
    {
        acc = mvImuFromLastFrame[i].a;
        angVel = mvImuFromLastFrame[i].w;
        tstep = mCurrentFrame.mTimeStamp-mCurrentFrame.mpPrevFrame->mTimeStamp;
    }
    
    

    要累积的预积分有两种:(1)上一帧到到当前帧pImuPreintegratedFromLastFrame和(2)上一关键帧到当前帧mpImuPreintegratedFromLastKF。对第0帧和第1帧来说,这两种预积分的结果相同,因为第0帧既是第1帧的上一帧,也是它的上一关键帧。调用Preintegrated类的IntegrateNewMeasurement方法计算预积分,预积分的递推公式和Forster的论文一致。

    mpImuPreintegratedFromLastKF->IntegrateNewMeasurement(acc,angVel,tstep);
    pImuPreintegratedFromLastFrame->IntegrateNewMeasurement(acc,angVel,tstep);
    

    4. MonocularInitialization()

    计算完预积分,再次进入函数MonocularInitialization(),单目初始化系统并估计当前帧的位姿。mpInitializer此时不再是空指针,else的主体被执行。

    else
    {
        if (((int)mCurrentFrame.mvKeys.size()<100||((mSensor == System::IMU_MONOCULAR)&&(mLastFrame.mTimeStamp-mInitialFrame.mTimeStamp>1.0)))
        {
            delete mpInitializer;
            mpInitializer = static_cast<Initializer*>(NULL);
            fill(mvIniMatches.begin(),mvIniMatches.end(),-1);
    
            return;
        }
    
        // Find correspondences
        ORBmatcher matcher(0.9,true);
        int nmatches = matcher.SearchForInitialization(mInitialFrame,mCurrentFrame,mvbPrevMatched,mvIniMatches,100);
    
        // Check if there are enough correspondences
        if(nmatches<100)
        {
            delete mpInitializer;
            mpInitializer = static_cast<Initializer*>(NULL);
            fill(mvIniMatches.begin(),mvIniMatches.end(),-1);
            return;
        }
    
        cv::Mat Rcw; // Current Camera Rotation
        cv::Mat tcw; // Current Camera Translation
        vector<bool> vbTriangulated; // Triangulated Correspondences (mvIniMatches)
    
        if(mpCamera->ReconstructWithTwoViews(mInitialFrame.mvKeysUn,mCurrentFrame.mvKeysUn,mvIniMatches,Rcw,tcw,mvIniP3D,vbTriangulated))
        {
            for(size_t i=0, iend=mvIniMatches.size(); i<iend;i++)
            {
                if(mvIniMatches[i]>=0 && !vbTriangulated[i])
                {
                    mvIniMatches[i]=-1;
                    nmatches--;
                }
            }
    
            // Set Frame Poses
            mInitialFrame.SetPose(cv::Mat::eye(4,4,CV_32F));
            cv::Mat Tcw = cv::Mat::eye(4,4,CV_32F);
            Rcw.copyTo(Tcw.rowRange(0,3).colRange(0,3));
            tcw.copyTo(Tcw.rowRange(0,3).col(3));
            mCurrentFrame.SetPose(Tcw);
    
            CreateInitialMapMonocular();
    
        }
    }
    

    如果当前帧特征点数少于100或者上一帧和初始帧时间差大于1秒,就重置初始化步骤。这意味着如果第1帧不满足上述条件,我们就要接着读取第2帧,并把第2帧设置为mInitialFrame,从头开始做初始化。当然,这里我们假设第1帧是满足条件的。

    现在,系统需要找到第0帧和第1帧有多少特征点是匹配的,这个工作由SearchForInitialization()完成。类似的处理,如果匹配数小于100,就重置初始化步骤。

    第1帧的位姿包括旋转Rcw和平移twc,这里用到的算法很常规,调用ReconstructWithTwoViews()做对极约束来求解基础矩阵或单应矩阵,再用三角测量估计特征点的深度。我们先看看这个函数是如何实现的。

    5. ReconstructWithTwoViews()

    5.1

    ReconstructWithTwoViews()是个虚函数,会根据所使用的相机模型调用不同的实现,这里就以针孔模型Pinhole为例。函数有4个参数:

    1. 初始帧,即第0帧,经过去畸变的特征点mInitialFrame.mvKeysUn
    2. 当前帧,即第1帧,经过去畸变的特征点mCurrentFrame.mvKeysUn
    3. 第0帧到第1帧的匹配mvIniMatches,每个元素表示第0帧特征点在第1帧中的index,若匹配不成功则为-1;
    4. 第1帧位姿Rcwtcw;
    5. 第0帧特征点的三维坐标mvIniP3D
    6. 存储三角测量匹配结果的vbTriangulated,和mvIniMatches对应。

    内部调用Reconstruct()执行初始化,传递上述的几个参数。

    if(!tvr){
        cv::Mat K = this->toK();
        tvr = new TwoViewReconstruction(K);
    }
    
    return tvr->Reconstruct(vKeys1,vKeys2,vMatches12,R21,t21,vP3D,vbTriangulated);
    

    5.2 Reconstruct()

    mvMatches12存储第0帧到第1帧的匹配结果,若匹配成功,则对应mvbMatched1元素为true,否则为false

    for(size_t i=0, iend=vMatches12.size();i<iend; i++)
    {
        if(vMatches12[i]>=0)
        {
            mvMatches12.push_back(make_pair(i,vMatches12[i]));
            mvbMatched1[i]=true;
        }
        else
            mvbMatched1[i]=false;
    }
    

    应用RANSAC算法估算单应矩阵和基础矩阵,
    mvMatches12中随机挑选8组匹配为一个set,一共mMaxIterations(默认200)个set。

    for(int it=0; it<mMaxIterations; it++)
    {
        vAvailableIndices = vAllIndices;
        // Select a minimum set
        for(size_t j=0; j<8; j++)
        {
            int randi = DUtils::Random::RandomInt(0,vAvailableIndices.size()-1);
            int idx = vAvailableIndices[randi];
    
            mvSets[it][j] = idx;
    
            vAvailableIndices[randi] = vAvailableIndices.back();
            vAvailableIndices.pop_back();
        }
    }
    

    开启两个线程同时计算单应矩阵H和基础矩阵F模型,并为两种模型打分,分数存储在SHSF中。

    thread threadH(&TwoViewReconstruction::FindHomography,this,ref(vbMatchesInliersH), ref(SH), ref(H));
    thread threadF(&TwoViewReconstruction::FindFundamental,this,ref(vbMatchesInliersF), ref(SF), ref(F));
    

    选择分数较好的那组作为求解模型,解出当前帧位姿,并应用三角测量估算特征点深度。

    if(SH+SF == 0.f) return false;
    float RH = SH/(SH+SF);
    float minParallax = 1.0;
    if(RH>0.50)
    {
        cout << "Initialization from Homography" << endl;
        return ReconstructH(vbMatchesInliersH,H, mK,R21,t21,vP3D,vbTriangulated,minParallax,50);
    }
    else
    {
        cout << "Initialization from Fundamental" << endl;
        return ReconstructF(vbMatchesInliersF,F,mK,R21,t21,vP3D,vbTriangulated,minParallax,50);
    }
    

    求解结束后,返回到MonocularInitialization(),设置第0帧位姿为单位矩阵,及初始化成功后的当前帧位姿Tcw

    mInitialFrame.SetPose(cv::Mat::eye(4,4,CV_32F));
    cv::Mat Tcw = cv::Mat::eye(4,4,CV_32F);
    Rcw.copyTo(Tcw.rowRange(0,3).colRange(0,3));
    tcw.copyTo(Tcw.rowRange(0,3).col(3));
    mCurrentFrame.SetPose(Tcw);
    

    最后调用CreateInitialMapMonocular()新建初始化成功后的地图。

    6. CreateInitialMapMonocular()

    用第0帧和第1帧创建关键帧并插入到地图。

    KeyFrame* pKFini = new KeyFrame(mInitialFrame,mpAtlas->GetCurrentMap(),mpKeyFrameDB);
    KeyFrame* pKFcur = new KeyFrame(mCurrentFrame,mpAtlas->GetCurrentMap(),mpKeyFrameDB);
    
    mpAtlas->AddKeyFrame(pKFini);
    mpAtlas->AddKeyFrame(pKFcur);
    

    遍历所有合格的三维点,创建地图点,并添加点与关键帧之间的可见信息。然后为每个地图点从能观察到它的关键帧中选出最佳描述子mDescriptor,并计算平均viewing direction,这两项工作由ComputeDistinctiveDescriptors()UpdateNormalAndDepth()完成,解释起来又是啰里啰嗦,源码写得很有条理,也易读。最后添加地图点到地图中。

    for(size_t i=0; i<mvIniMatches.size();i++)
    {
        if(mvIniMatches[i]<0)
            continue;
    
        //Create MapPoint.
        cv::Mat worldPos(mvIniP3D[i]);
        MapPoint* pMP = new MapPoint(worldPos,pKFcur,mpAtlas->GetCurrentMap());
    
        pKFini->AddMapPoint(pMP,i);
        pKFcur->AddMapPoint(pMP,mvIniMatches[i]);
    
        pMP->AddObservation(pKFini,i);
        pMP->AddObservation(pKFcur,mvIniMatches[i]);
    
        pMP->ComputeDistinctiveDescriptors();
        pMP->UpdateNormalAndDepth();
    
        //Fill Current Frame structure
        mCurrentFrame.mvpMapPoints[mvIniMatches[i]] = pMP;
        mCurrentFrame.mvbOutlier[mvIniMatches[i]] = false;
    
        //Add to Map
        mpAtlas->AddMapPoint(pMP);
    }
    

    更新共视图covisibility graph的边和权重。

    pKFini->UpdateConnections();
    pKFcur->UpdateConnections();
    

    Bundle ajustment图优化当前地图中所有关键帧的位姿和地图点的坐标,使得投影误差最小。

    Optimizer::GlobalBundleAdjustemnt(mpAtlas->GetCurrentMap(),20);
    

    得到优化后位姿和地图点坐标后,需要对整个场景的尺度归一化,常用的方法是令第0帧和第1帧之间的平移距离为单位1。这里采用令第0帧场景深度中位值为单位1的方法,来控制场景的规模。首先调用ComputeSceneMedianDepth()计算第0帧的深度中位值,确定尺度。

    float medianDepth = pKFini->ComputeSceneMedianDepth(2);
    float invMedianDepth;
    if(mSensor == System::IMU_MONOCULAR)
        invMedianDepth = 4.0f/medianDepth; // 4.0f
    else
        invMedianDepth = 1.0f/medianDepth;
    

    用得到的尺度归一化关键帧位姿和地图点坐标。

    // Scale initial baseline
    cv::Mat Tc2w = pKFcur->GetPose();
    Tc2w.col(3).rowRange(0,3) = Tc2w.col(3).rowRange(0,3)*invMedianDepth;
    pKFcur->SetPose(Tc2w);
    
    // Scale points
    vector<MapPoint*> vpAllMapPoints = pKFini->GetMapPointMatches();
    for(size_t iMP=0; iMP<vpAllMapPoints.size(); iMP++)
    {
        if(vpAllMapPoints[iMP])
        {
            MapPoint* pMP = vpAllMapPoints[iMP];
            pMP->SetWorldPos(pMP->GetWorldPos()*invMedianDepth);
            pMP->UpdateNormalAndDepth();
        }
    }
    

    剩下一些常规设置,包括传递预积分指针、计算速度模型的初始速度、插入关键帧到建图线程并改变系统状态mState为OK表示初始化成功等。

  • 相关阅读:
    jQuery基础【1】
    qTip2 精致的jQuery提示信息插件
    jquery tools 系列(三)——scrollable(2)
    JQuery常用函数及功能小结
    【转】JQ命令汇总 jQuery
    jquery tools系列(一)——tabs(选项卡/页签)
    jquery tools系列(四)——overlay
    肚子
    今天买了部数码相机NiKon S510
    《文渊阁四库全书》书目
  • 原文地址:https://www.cnblogs.com/yiqian/p/14915183.html
Copyright © 2011-2022 走看看