ORB-SLAM2算法13之跟踪线程Tracking

news2025/2/23 16:38:26

文章目录

  • 0 引言
  • 1 跟踪线程Tracking
    • 1.1 概述
    • 1.2 初始化
      • 1.2.1 单目初始化
      • 1.2.2 双目/RGBD初始化
    • 1.3 跟踪方法
      • 1.3.1 恒速模型跟踪
      • 1.3.2 参考关键帧跟踪
      • 1.3.3 重定位跟踪
    • 1.4 局部地图跟踪
      • 1.4.1 流程
      • 1.4.2 更新局部关键帧
      • 1.4.3 更新局部地图点
      • 1.4.4 进一步优化
    • 1.5 关键帧生成
      • 1.5.1 生成条件
      • 1.5.2 创建关键帧

0 引言

ORB-SLAM2算法7详细了解了System主类和多线程、ORB-SLAM2学习笔记8详细了解了图像特征点提取和描述子的生成、ORB-SLAM2算法9详细了解了图像帧、ORB-SLAM2算法10详细了解了图像关键帧、ORB-SLAM2算法11详细了解了地图点及ORB-SLAM2算法12详细了解了单目初始化,本文主要学习ORB-SLAM2中的三大线程中的跟踪线程Tracking

请添加图片描述

1 跟踪线程Tracking

1.1 概述

总的来说2种模式+3个目的+3种方法+5种状态。

Tracking跟踪线程有2种模式:SLAM模式(定位和建图)和Localization模式(仅定位),由变量mbOnlyTracking来控制两种模式:

  1. mbOnlyTracking默认为false,即SLAM模式,此时三大线程都在工作,定位的同时也在建图;
  2. mbOnlyTracking默认为true,即为Localization模式,此时只有Tracking线程工作,只定位,输出追踪的位姿,不会更新地图和关键帧。

Tracking跟踪线程有3种目的:

  1. 更新精确的位姿
  2. 更新地图点
  3. 更新关键帧

Tracking跟踪线程有3种方法:

  1. 恒速模型跟踪 TrackWithMotionModel()
  2. 参考关键帧跟踪 TrackReferenceKeyFrame()
  3. 重定位跟踪 Relocalization()

Tracking跟踪线程有5种状态:

  1. SYSTEM_NOT_READY: 系统没有准备好的状态;
  2. NO_IMAGES_YET :当前无图像;
  3. NOT_INITIALIZED: 有图像但是没有完成初始化;
  4. OK: 正常时候的工作状态;
  5. LOST :系统已经跟丢了的状态。

详细的流程图如下(参考3):

请添加图片描述

1.2 初始化

1.2.1 单目初始化

ORB-SLAM2算法12已经详细了解了单目初始化,这里就再简单列举主要的几个步骤:

  1. 得到用于初始化的第一帧,初始化需要两帧;
  2. 如果当前帧特征点数大于100,则得到用于单目初始化的第二帧;
  3. mInitialFramemCurrentFrame中找匹配的特征点对;
  4. 如果初始化的两帧之间的匹配点太少,重新初始化;
  5. 通过 H H H模型或 F F F模型进行单目初始化,得到两帧之间相对运动,初始化MapPoints
  6. 删除那些无法进行三角化的匹配点;
  7. 将三角化得到的3D点包装成MapPoints
# 单目初始化

void Tracking::MonocularInitialization()
{
    // Step 1 如果单目初始器还没有被创建,则创建。后面如果重新初始化时会清掉这个
    if(!mpInitializer)
    {
        // Set Reference Frame
        // 单目初始帧的特征点数必须大于100
        if(mCurrentFrame.mvKeys.size()>100)
        {
            // 初始化需要两帧,分别是mInitialFrame,mCurrentFrame
            mInitialFrame = Frame(mCurrentFrame);
            // 用当前帧更新上一帧
            mLastFrame = Frame(mCurrentFrame);
            // mvbPrevMatched  记录"上一帧"所有特征点
            mvbPrevMatched.resize(mCurrentFrame.mvKeysUn.size());
            for(size_t i=0; i<mCurrentFrame.mvKeysUn.size(); i++)
                mvbPrevMatched[i]=mCurrentFrame.mvKeysUn[i].pt;

            // 删除前判断一下,来避免出现段错误。不过在这里是多余的判断
            // 不过在这里是多余的判断,因为前面已经判断过了
            if(mpInitializer)
                delete mpInitializer;

            // 由当前帧构造初始器 sigma:1.0 iterations:200
            mpInitializer =  new Initializer(mCurrentFrame,1.0,200);

            // 初始化为-1 表示没有任何匹配。这里面存储的是匹配的点的id
            fill(mvIniMatches.begin(),mvIniMatches.end(),-1);

            return;
        }
    }
    else    //如果单目初始化器已经被创建
    {
        // Try to initialize
        // Step 2 如果当前帧特征点数太少(不超过100),则重新构造初始器
        // NOTICE 只有连续两帧的特征点个数都大于100时,才能继续进行初始化过程
        if((int)mCurrentFrame.mvKeys.size()<=100)
        {
            delete mpInitializer;
            mpInitializer = static_cast<Initializer*>(NULL);
            fill(mvIniMatches.begin(),mvIniMatches.end(),-1);
            return;
        }

        // Find correspondences
        // Step 3 在mInitialFrame与mCurrentFrame中找匹配的特征点对
        ORBmatcher matcher(
            0.9,        //最佳的和次佳特征点评分的比值阈值,这里是比较宽松的,跟踪时一般是0.7
            true);      //检查特征点的方向

        // 对 mInitialFrame,mCurrentFrame 进行特征点匹配
        // mvbPrevMatched为参考帧的特征点坐标,初始化存储的是mInitialFrame中特征点坐标,匹配后存储的是匹配好的当前帧的特征点坐标
        // mvIniMatches 保存参考帧F1中特征点是否匹配上,index保存是F1对应特征点索引,值保存的是匹配好的F2特征点索引
        int nmatches = matcher.SearchForInitialization(
            mInitialFrame,mCurrentFrame,    //初始化时的参考帧和当前帧
            mvbPrevMatched,                 //在初始化参考帧中提取得到的特征点
            mvIniMatches,                   //保存匹配关系
            100);                           //搜索窗口大小

        // Check if there are enough correspondences
        // Step 4 验证匹配结果,如果初始化的两帧之间的匹配点太少,重新初始化
        if(nmatches<100)
        {
            delete mpInitializer;
            mpInitializer = static_cast<Initializer*>(NULL);
            return;
        }

        cv::Mat Rcw; // Current Camera Rotation
        cv::Mat tcw; // Current Camera Translation
        vector<bool> vbTriangulated; // Triangulated Correspondences (mvIniMatches)

        // Step 5 通过H模型或F模型进行单目初始化,得到两帧间相对运动、初始MapPoints
        if(mpInitializer->Initialize(
            mCurrentFrame,      //当前帧
            mvIniMatches,       //当前帧和参考帧的特征点的匹配关系
            Rcw, tcw,           //初始化得到的相机的位姿
            mvIniP3D,           //进行三角化得到的空间点集合
            vbTriangulated))    //以及对应于mvIniMatches来讲,其中哪些点被三角化了
        {
            // Step 6 初始化成功后,删除那些无法进行三角化的匹配点
            for(size_t i=0, iend=mvIniMatches.size(); i<iend;i++)
            {
                if(mvIniMatches[i]>=0 && !vbTriangulated[i])
                {
                    mvIniMatches[i]=-1;
                    nmatches--;
                }
            }

            // Set Frame Poses
            // Step 7 将初始化的第一帧作为世界坐标系,因此第一帧变换矩阵为单位矩阵
            mInitialFrame.SetPose(cv::Mat::eye(4,4,CV_32F));
            // 由Rcw和tcw构造Tcw,并赋值给mTcw,mTcw为世界坐标系到相机坐标系的变换矩阵
            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);

            // Step 8 创建初始化地图点MapPoints
            // Initialize函数会得到mvIniP3D,
            // mvIniP3D是cv::Point3f类型的一个容器,是个存放3D点的临时变量,
            // CreateInitialMapMonocular将3D点包装成MapPoint类型存入KeyFrame和Map中
            CreateInitialMapMonocular();
        }//当初始化成功的时候进行
    }//如果单目初始化器已经被创建
}

1.2.2 双目/RGBD初始化

双目/RGBD初始化比单目初始化简单点,其中双目可通过视差获取深度,RGBD相机直接获取深度,然后得到3D点,创建地图点并完成初始化。

  1. 首先要求当前帧的特征点数大于500
  2. 然后设置当前帧的初始位姿为单位矩阵;
  3. 将当前初始化帧构造成初始关键帧,包括关键帧ID,位姿等;
  4. 将初始关键帧添加到地图中;
  5. 为具有真正深度的特征点构造MapPoint(深度为正的点通过反投影得到该特征点在世界坐标系下的3D坐标,构造3D点为地图点,给地图点添加属性,添加地图点到地图中,记录特征点与地图点的索引);
  6. 将该初始关键帧添加到局部地图中;
  7. 更新状态量。
/*
 * @brief 双目和rgbd的地图初始化,比单目简单很多
 *
 * 由于具有深度信息,直接生成MapPoints
 */
void Tracking::StereoInitialization()
{
    // 初始化要求当前帧的特征点超过500
    if(mCurrentFrame.N>500)
    {
        // Set Frame pose to the origin
        // 设定初始位姿为单位旋转,0平移
        mCurrentFrame.SetPose(cv::Mat::eye(4,4,CV_32F));

        // Create KeyFrame
        // 将当前帧构造为初始关键帧
        // mCurrentFrame的数据类型为Frame
        // KeyFrame包含Frame、地图3D点、以及BoW
        // KeyFrame里有一个mpMap,Tracking里有一个mpMap,而KeyFrame里的mpMap都指向Tracking里的这个mpMap
        // KeyFrame里有一个mpKeyFrameDB,Tracking里有一个mpKeyFrameDB,而KeyFrame里的mpMap都指向Tracking里的这个mpKeyFrameDB
        KeyFrame* pKFini = new KeyFrame(mCurrentFrame,mpMap,mpKeyFrameDB);

        // Insert KeyFrame in the map
        // KeyFrame中包含了地图、反过来地图中也包含了KeyFrame,相互包含
        // 在地图中添加该初始关键帧
        mpMap->AddKeyFrame(pKFini);

        // Create MapPoints and asscoiate to KeyFrame
        // 为每个特征点构造MapPoint
        for(int i=0; i<mCurrentFrame.N;i++)
        {
            //只有具有正深度的点才会被构造地图点
            float z = mCurrentFrame.mvDepth[i];
            if(z>0)
            {
                // 通过反投影得到该特征点的世界坐标系下3D坐标
                cv::Mat x3D = mCurrentFrame.UnprojectStereo(i);
                // 将3D点构造为MapPoint
                MapPoint* pNewMP = new MapPoint(x3D,pKFini,mpMap);

                // 为该MapPoint添加属性:
                // a.观测到该MapPoint的关键帧
                // b.该MapPoint的描述子
                // c.该MapPoint的平均观测方向和深度范围

                // a.表示该MapPoint可以被哪个KeyFrame的哪个特征点观测到
                pNewMP->AddObservation(pKFini,i);
                // b.从众多观测到该MapPoint的特征点中挑选区分度最高的描述子             
                pNewMP->ComputeDistinctiveDescriptors();
                // c.更新该MapPoint平均观测方向以及观测距离的范围
                pNewMP->UpdateNormalAndDepth();

                // 在地图中添加该MapPoint
                mpMap->AddMapPoint(pNewMP);
                // 表示该KeyFrame的哪个特征点可以观测到哪个3D点
                pKFini->AddMapPoint(pNewMP,i);

                // 将该MapPoint添加到当前帧的mvpMapPoints中
                // 为当前Frame的特征点与MapPoint之间建立索引
                mCurrentFrame.mvpMapPoints[i]=pNewMP;
            }
        }

        cout << "New map created with " << mpMap->MapPointsInMap() << " points" << endl;

        // 在局部地图中添加该初始关键帧
        mpLocalMapper->InsertKeyFrame(pKFini);

        // 更新当前帧为上一帧
        mLastFrame = Frame(mCurrentFrame);
        mnLastKeyFrameId=mCurrentFrame.mnId;
        mpLastKeyFrame = pKFini;

        mvpLocalKeyFrames.push_back(pKFini);
        // 初始化之后,通过双目图像生成的地图点,都应该被认为是局部地图点
        mvpLocalMapPoints=mpMap->GetAllMapPoints();
        mpReferenceKF = pKFini;
        mCurrentFrame.mpReferenceKF = pKFini;

        // 把当前(最新的)局部MapPoints作为ReferenceMapPoints
        // ReferenceMapPoints是DrawMapPoints函数画图的时候用的
        mpMap->SetReferenceMapPoints(mvpLocalMapPoints);
        mpMap->mvpKeyFrameOrigins.push_back(pKFini);
        mpMapDrawer->SetCurrentCameraPose(mCurrentFrame.mTcw);

        //追踪成功
        mState=OK;
    }
}

1.3 跟踪方法

1.3.1 恒速模型跟踪

该跟踪方法是ORB-SLAM2中的常用方法,如果不是没有运动速度刚完成重定位,就会优先选择恒速模型跟踪。基本思想是假设物体处于匀速运动,用上一帧的位姿和速度来估计当前帧的位姿,而匹配是通过投影来与上一帧看到的地图点进行匹配。详细的实现步骤如下:

  1. 创建ORB特征提取器,并设定最小距离 < 0.9 ∗ * 次小距离才算匹配成功;
  2. 更新上一帧的位姿,上一帧位姿等于上一帧到其参考帧位姿  ∗ *  其参考帧到世界坐标系(系统第一帧)位姿;
  3. 根据之前估计的速度,初始化当前帧的位姿,等于运动速度 ∗ * 上一帧的位姿;
  4. 用上一帧地图点来进行投影匹配,如果匹配点不够,则扩大搜索半径再来一次,如果还是不能获得足够的匹配点,则认为跟踪失败;
  5. 利用3D-2D投影关系,优化当前帧位姿;
  6. 剔除地图点中外点,并累加成功匹配到的地图点数目;
  7. 如果累加成功匹配的地图点数目超过10个点就认为跟踪成功。
// 根据恒速模型用上一帧地图点来对当前帧进行跟踪

bool Tracking::TrackWithMotionModel()
{
    // 最小距离 < 0.9*次小距离 匹配成功,检查旋转
    ORBmatcher matcher(0.9,true);

    // Update last frame pose according to its reference keyframe
    // Create "visual odometry" points
    // Step 1:更新上一帧的位姿;对于双目或RGB-D相机,还会根据深度值生成临时地图点
    UpdateLastFrame();

    // Step 2:根据之前估计的速度,用恒速模型得到当前帧的初始位姿。
    mCurrentFrame.SetPose(mVelocity*mLastFrame.mTcw);
    
    // 清空当前帧的地图点
    fill(mCurrentFrame.mvpMapPoints.begin(),mCurrentFrame.mvpMapPoints.end(),static_cast<MapPoint*>(NULL));

    // Project points seen in previous frame
    // 设置特征匹配过程中的搜索半径
    int th;
    if(mSensor!=System::STEREO)
        th=15;//单目
    else
        th=7;//双目

    // Step 3:用上一帧地图点进行投影匹配,如果匹配点不够,则扩大搜索半径再来一次
    int nmatches = matcher.SearchByProjection(mCurrentFrame,mLastFrame,th,mSensor==System::MONOCULAR);

    // If few matches, uses a wider window search
    // 如果匹配点太少,则扩大搜索半径再来一次
    if(nmatches<20)
    {
        fill(mCurrentFrame.mvpMapPoints.begin(),mCurrentFrame.mvpMapPoints.end(),static_cast<MapPoint*>(NULL));
        nmatches = matcher.SearchByProjection(mCurrentFrame,mLastFrame,2*th,mSensor==System::MONOCULAR); // 2*th
    }

    // 如果还是不能够获得足够的匹配点,那么就认为跟踪失败
    if(nmatches<20)
        return false;

    // Optimize frame pose with all matches
    // Step 4:利用3D-2D投影关系,优化当前帧位姿
    Optimizer::PoseOptimization(&mCurrentFrame);

    // Discard outliers
    // Step 5:剔除地图点中外点
    int nmatchesMap = 0;
    for(int i =0; i<mCurrentFrame.N; i++)
    {
        if(mCurrentFrame.mvpMapPoints[i])
        {
            if(mCurrentFrame.mvbOutlier[i])
            {
                // 如果优化后判断某个地图点是外点,清除它的所有关系
                MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];

                mCurrentFrame.mvpMapPoints[i]=static_cast<MapPoint*>(NULL);
                mCurrentFrame.mvbOutlier[i]=false;
                pMP->mbTrackInView = false;
                pMP->mnLastFrameSeen = mCurrentFrame.mnId;
                nmatches--;
            }
            else if(mCurrentFrame.mvpMapPoints[i]->Observations()>0)
                // 累加成功匹配到的地图点数目
                nmatchesMap++;
        }
    }    

    if(mbOnlyTracking)
    {
        // 纯定位模式下:如果成功追踪的地图点非常少,那么这里的mbVO标志就会置位
        mbVO = nmatchesMap<10;
        return nmatches>20;
    }

    // Step 6:匹配超过10个点就认为跟踪成功
    return nmatchesMap>=10;
}

1.3.2 参考关键帧跟踪

该跟踪方法是相机没有运动刚完成重定位恒速运动模型下匹配较少特征点的情况下使用。基本思想是在以上一种条件下尝试和最近一个关键帧mpReferenceKF去做特征匹配,匹配过程中使用BOW向量的正向索引进行加速。详细的实现步骤如下:

  1. 计算当前帧特征描述子,并转化为BOW向量;
  2. 通过词袋BOW加速当前帧与参考帧之间的特征点匹配,如果匹配数小于15,则跟踪失败;
  3. 将上一帧的位姿作为当前帧位姿的初始值;
  4. 通过优化3D-2D的重投影误差来获得位姿;
  5. 剔除优化后的匹配点中的外点,并累加成功匹配到的地图点数目;
  6. 如果累加成功匹配的地图点数目超过10个点就认为跟踪成功。
// 用参考关键帧的地图点来对当前普通帧进行跟踪

bool Tracking::TrackReferenceKeyFrame()
{
    // Compute Bag of Words vector
    // Step 1:将当前帧的描述子转化为BoW向量
    mCurrentFrame.ComputeBoW();

    // We perform first an ORB matching with the reference keyframe
    // If enough matches are found we setup a PnP solver
    ORBmatcher matcher(0.7,true);
    vector<MapPoint*> vpMapPointMatches;

    // Step 2:通过词袋BoW加速当前帧与参考帧之间的特征点匹配
    int nmatches = matcher.SearchByBoW(
        mpReferenceKF,          //参考关键帧
        mCurrentFrame,          //当前帧
        vpMapPointMatches);     //存储匹配关系

    // 匹配数目小于15,认为跟踪失败
    if(nmatches<15)
        return false;

    // Step 3:将上一帧的位姿态作为当前帧位姿的初始值
    mCurrentFrame.mvpMapPoints = vpMapPointMatches;
    mCurrentFrame.SetPose(mLastFrame.mTcw); // 用上一次的Tcw设置初值,在PoseOptimization可以收敛快一些

    // Step 4:通过优化3D-2D的重投影误差来获得位姿
    Optimizer::PoseOptimization(&mCurrentFrame);

    // Discard outliers
    // Step 5:剔除优化后的匹配点中的外点
    //之所以在优化之后才剔除外点,是因为在优化的过程中就有了对这些外点的标记
    int nmatchesMap = 0;
    for(int i =0; i<mCurrentFrame.N; i++)
    {
        if(mCurrentFrame.mvpMapPoints[i])
        {
            //如果对应到的某个特征点是外点
            if(mCurrentFrame.mvbOutlier[i])
            {
                //清除它在当前帧中存在过的痕迹
                MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];

                mCurrentFrame.mvpMapPoints[i]=static_cast<MapPoint*>(NULL);
                mCurrentFrame.mvbOutlier[i]=false;
                pMP->mbTrackInView = false;
                pMP->mnLastFrameSeen = mCurrentFrame.mnId;
                nmatches--;
            }
            else if(mCurrentFrame.mvpMapPoints[i]->Observations()>0)
                //匹配的内点计数++
                nmatchesMap++;
        }
    }
    // 跟踪成功的数目超过10才认为跟踪成功,否则跟踪失败
    return nmatchesMap>=10;
}

1.3.3 重定位跟踪

假如恒速模型跟踪失败,并且参考关键帧跟踪也失败,则需要进行重定位。基本思想是位置丢失后,使用当前帧的BOW特征向量在关键帧数据库中匹配最相近的关键帧,从而求出当前帧位姿,但由于此刻没有好的初始位姿信息,需要使用传统的3D-2D匹配点的EPnP算法来求解出一个初始位姿,然后再使用最小化重投影误差来优化更新位姿。详细的实现步骤如下:

  1. 计算当前帧特征描述子,并转化为BOW向量;
  2. 初次筛选找到与当前帧相似的候选关键帧;
  3. 通过BOW二次筛选候选帧;
  4. 通过EPnP算法估计姿态;
  5. 通过PoseOptimization对姿态进行优化求解;
  6. 如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解。
// 重定位跟踪

bool Tracking::Relocalization()
{
    // Compute Bag of Words Vector
    // Step 1:计算当前帧特征点的词袋向量
    mCurrentFrame.ComputeBoW();

    // Relocalization is performed when tracking is lost
    // Track Lost: Query KeyFrame Database for keyframe candidates for relocalisation
    // Step 2:用词袋找到与当前帧相似的候选关键帧
    vector<KeyFrame*> vpCandidateKFs = mpKeyFrameDB->DetectRelocalizationCandidates(&mCurrentFrame);
    
    // 如果没有候选关键帧,则退出
    if(vpCandidateKFs.empty())
        return false;

    const int nKFs = vpCandidateKFs.size();

    // We perform first an ORB matching with each candidate
    // If enough matches are found we setup a PnP solver
    ORBmatcher matcher(0.75,true);
    //每个关键帧的解算器
    vector<PnPsolver*> vpPnPsolvers;
    vpPnPsolvers.resize(nKFs);

    //每个关键帧和当前帧中特征点的匹配关系
    vector<vector<MapPoint*> > vvpMapPointMatches;
    vvpMapPointMatches.resize(nKFs);
    
    //放弃某个关键帧的标记
    vector<bool> vbDiscarded;
    vbDiscarded.resize(nKFs);

    //有效的候选关键帧数目
    int nCandidates=0;

    // Step 3:遍历所有的候选关键帧,通过词袋进行快速匹配,用匹配结果初始化PnP Solver
    for(int i=0; i<nKFs; i++)
    {
        KeyFrame* pKF = vpCandidateKFs[i];
        if(pKF->isBad())
            vbDiscarded[i] = true;
        else
        {
            // 当前帧和候选关键帧用BoW进行快速匹配,匹配结果记录在vvpMapPointMatches,nmatches表示匹配的数目
            int nmatches = matcher.SearchByBoW(pKF,mCurrentFrame,vvpMapPointMatches[i]);
            // 如果和当前帧的匹配数小于15,那么只能放弃这个关键帧
            if(nmatches<15)
            {
                vbDiscarded[i] = true;
                continue;
            }
            else
            {
                // 如果匹配数目够用,用匹配结果初始化EPnPsolver
                // 为什么用EPnP? 因为计算复杂度低,精度高
                PnPsolver* pSolver = new PnPsolver(mCurrentFrame,vvpMapPointMatches[i]);
                pSolver->SetRansacParameters(
                    0.99,   //用于计算RANSAC迭代次数理论值的概率
                    10,     //最小内点数, 但是要注意在程序中实际上是min(给定最小内点数,最小集,内点数理论值),不一定使用这个
                    300,    //最大迭代次数
                    4,      //最小集(求解这个问题在一次采样中所需要采样的最少的点的个数,对于Sim3是3,EPnP是4),参与到最小内点数的确定过程中
                    0.5,    //这个是表示(最小内点数/样本总数);实际上的RANSAC正常退出的时候所需要的最小内点数其实是根据这个量来计算得到的
                    5.991); // 自由度为2的卡方检验的阈值,程序中还会根据特征点所在的图层对这个阈值进行缩放
                vpPnPsolvers[i] = pSolver;
                nCandidates++;
            }
        }
    }

    // Alternatively perform some iterations of P4P RANSAC
    // Until we found a camera pose supported by enough inliers
    // 这里的 P4P RANSAC是Epnp,每次迭代需要4个点
    // 是否已经找到相匹配的关键帧的标志
    bool bMatch = false;
    ORBmatcher matcher2(0.9,true);

    // Step 4: 通过一系列操作,直到找到能够匹配上的关键帧
    // 为什么搞这么复杂?答:是担心误闭环
    while(nCandidates>0 && !bMatch)
    {
        //遍历当前所有的候选关键帧
        for(int i=0; i<nKFs; i++)
        {
            // 忽略放弃的
            if(vbDiscarded[i])
                continue;
    
            //内点标记
            vector<bool> vbInliers;     
            
            //内点数
            int nInliers;
            
            // 表示RANSAC已经没有更多的迭代次数可用 -- 也就是说数据不够好,RANSAC也已经尽力了。。。
            bool bNoMore;

            // Step 4.1:通过EPnP算法估计姿态,迭代5次
            PnPsolver* pSolver = vpPnPsolvers[i];
            cv::Mat Tcw = pSolver->iterate(5,bNoMore,vbInliers,nInliers);

            // If Ransac reachs max. iterations discard keyframe
            // bNoMore 为true 表示已经超过了RANSAC最大迭代次数,就放弃当前关键帧
            if(bNoMore)
            {
                vbDiscarded[i]=true;
                nCandidates--;
            }

            // If a Camera Pose is computed, optimize
            if(!Tcw.empty())
            {
                //  Step 4.2:如果EPnP 计算出了位姿,对内点进行BA优化
                Tcw.copyTo(mCurrentFrame.mTcw);
                
                // EPnP 里RANSAC后的内点的集合
                set<MapPoint*> sFound;

                const int np = vbInliers.size();
                //遍历所有内点
                for(int j=0; j<np; j++)
                {
                    if(vbInliers[j])
                    {
                        mCurrentFrame.mvpMapPoints[j]=vvpMapPointMatches[i][j];
                        sFound.insert(vvpMapPointMatches[i][j]);
                    }
                    else
                        mCurrentFrame.mvpMapPoints[j]=NULL;
                }

                // 只优化位姿,不优化地图点的坐标,返回的是内点的数量
                int nGood = Optimizer::PoseOptimization(&mCurrentFrame);

                // 如果优化之后的内点数目不多,跳过了当前候选关键帧,但是却没有放弃当前帧的重定位
                if(nGood<10)
                    continue;

                // 删除外点对应的地图点
                for(int io =0; io<mCurrentFrame.N; io++)
                    if(mCurrentFrame.mvbOutlier[io])
                        mCurrentFrame.mvpMapPoints[io]=static_cast<MapPoint*>(NULL);

                // If few inliers, search by projection in a coarse window and optimize again
                // Step 4.3:如果内点较少,则通过投影的方式对之前未匹配的点进行匹配,再进行优化求解
                // 前面的匹配关系是用词袋匹配过程得到的
                if(nGood<50)
                {
                    // 通过投影的方式将关键帧中未匹配的地图点投影到当前帧中, 生成新的匹配
                    int nadditional = matcher2.SearchByProjection(
                        mCurrentFrame,          //当前帧
                        vpCandidateKFs[i],      //关键帧
                        sFound,                 //已经找到的地图点集合,不会用于PNP
                        10,                     //窗口阈值,会乘以金字塔尺度
                        100);                   //匹配的ORB描述子距离应该小于这个阈值

                    // 如果通过投影过程新增了比较多的匹配特征点对
                    if(nadditional+nGood>=50)
                    {
                        // 根据投影匹配的结果,再次采用3D-2D pnp BA优化位姿
                        nGood = Optimizer::PoseOptimization(&mCurrentFrame);

                        // If many inliers but still not enough, search by projection again in a narrower window
                        // the camera has been already optimized with many points
                        // Step 4.4:如果BA后内点数还是比较少(<50)但是还不至于太少(>30),可以挽救一下, 最后垂死挣扎 
                        // 重新执行上一步 4.3的过程,只不过使用更小的搜索窗口
                        // 这里的位姿已经使用了更多的点进行了优化,应该更准,所以使用更小的窗口搜索
                        if(nGood>30 && nGood<50)
                        {
                            // 用更小窗口、更严格的描述子阈值,重新进行投影搜索匹配
                            sFound.clear();
                            for(int ip =0; ip<mCurrentFrame.N; ip++)
                                if(mCurrentFrame.mvpMapPoints[ip])
                                    sFound.insert(mCurrentFrame.mvpMapPoints[ip]);
                            nadditional =matcher2.SearchByProjection(
                                mCurrentFrame,          //当前帧
                                vpCandidateKFs[i],      //候选的关键帧
                                sFound,                 //已经找到的地图点,不会用于PNP
                                3,                      //新的窗口阈值,会乘以金字塔尺度
                                64);                    //匹配的ORB描述子距离应该小于这个阈值

                            // Final optimization
                            // 如果成功挽救回来,匹配数目达到要求,最后BA优化一下
                            if(nGood+nadditional>=50)
                            {
                                nGood = Optimizer::PoseOptimization(&mCurrentFrame);
                                //更新地图点
                                for(int io =0; io<mCurrentFrame.N; io++)
                                    if(mCurrentFrame.mvbOutlier[io])
                                        mCurrentFrame.mvpMapPoints[io]=NULL;
                            }
                            //如果还是不能够满足就放弃了
                        }
                    }
                }

                // If the pose is supported by enough inliers stop ransacs and continue
                // 如果对于当前的候选关键帧已经有足够的内点(50个)了,那么就认为重定位成功
                if(nGood>=50)
                {
                    bMatch = true;
                    // 只要有一个候选关键帧重定位成功,就退出循环,不考虑其他候选关键帧了
                    break;
                }
            }
        }//一直运行,知道已经没有足够的关键帧,或者是已经有成功匹配上的关键帧
    }

    // 折腾了这么久还是没有匹配上,重定位失败
    if(!bMatch)
    {
        return false;
    }
    else
    {
        // 如果匹配上了,说明当前帧重定位成功了(当前帧已经有了自己的位姿)
        // 记录成功重定位帧的id,防止短时间多次重定位
        mnLastRelocFrameId = mCurrentFrame.mnId;
        return true;
    }
}

1.4 局部地图跟踪

1.4.1 流程

1.3章节的三个跟踪模型得到的位姿和地图点都是粗略的,局部地图匹配就是为了进一步优化地图和位姿。

  1. 更新局部关键帧mvpLocalKeyFrames和局部地图点mvpLocalMapPoints
  2. 在局部地图中查找与当前帧匹配的地图点, 其实也就是对局部地图点进行跟踪;
  3. 更新局部所有地图点后对位姿再次优化;
  4. 更新当前帧的地图点被观测程度,并统计跟踪局部地图的效果;
  5. 决定是否跟踪成功。

1.4.2 更新局部关键帧

作者在论文中对局部关键帧有如下定义:局部关键帧包含了两个集合(set),第一,与当前帧有共视关系(share map points)的关键帧,记作集合K1;第二,与集合K1在共视图中(covisibility graph)有良好共视关系的帧(具体见下),记作K2

凡是和当前帧有共同的地图点的关键帧都归为K1K2集合需要满足以下三种条件:

  • K1有良好共视关系的子关键帧(作者选取了最佳共视的10帧)
  • K1中元素的子关键帧
  • K1中元素的父关键帧

满足其中一种条件的关键帧都可以被归为K2集合。

在处理完局部关键帧后,还需要添加参考关键帧与当前帧共视程度最高(有最多share map points)的关键帧作为参考关键帧。

1.4.3 更新局部地图点

上一步得到了所有的局部关键帧,然后更新局部地图点,即所有局部关键帧包含的地图点构成局部地图点。主要有以下三步:

  1. 清空局部地图点;
  2. 将局部关键帧的地图点添加到mvpLocalMapPoints
  3. 同时设置地图点更新标志,来避免重复添加出现在多帧上的地图点。

1.4.4 进一步优化

虽然上一步得到了很多局部地图点,但有很多是不可用的,所以还需按照以下步骤进一步筛选:

  1. 遍历当前帧的特征点,如果已经有相应的 3D 地图点,则进行标记,不需要进行重投影匹配,并且标记已经被遍历过;
  2. 遍历局部地图的所有地图点,如果没有被遍历过,把地图点反投影到当前帧下,保留在当前帧视野内的地图点;
  3. 根据反投影后的2D位置,设置一个半径,并在此范围进行搜索匹配点
  4. 更新局部所有 MapPoints 后对位姿再次优化,采用的是g2o优化器;
  5. 更新地图点状态;
  6. 正常情况下,找到的内点匹配点对数大于 30 算成功,但如果刚进行过重定位则需要内点匹配点对数大于 50 才认为跟踪成功。
// 用局部地图进行跟踪,进一步优化位姿

bool Tracking::TrackLocalMap()
{
    // Update Local KeyFrames and Local Points
    // Step 1:更新局部关键帧 mvpLocalKeyFrames 和局部地图点 mvpLocalMapPoints
    UpdateLocalMap();

    // Step 2:筛选局部地图中新增的在视野范围内的地图点,投影到当前帧搜索匹配,得到更多的匹配关系
    SearchLocalPoints();

    // Optimize Pose
    // 在这个函数之前,在 Relocalization、TrackReferenceKeyFrame、TrackWithMotionModel 中都有位姿优化,
    // Step 3:前面新增了更多的匹配关系,BA优化得到更准确的位姿
    Optimizer::PoseOptimization(&mCurrentFrame);
    mnMatchesInliers = 0;

    // Update MapPoints Statistics
    // Step 4:更新当前帧的地图点被观测程度,并统计跟踪局部地图后匹配数目
    for(int i=0; i<mCurrentFrame.N; i++)
    {
        if(mCurrentFrame.mvpMapPoints[i])
        {
            // 由于当前帧的地图点可以被当前帧观测到,其被观测统计量加1
            if(!mCurrentFrame.mvbOutlier[i])
            {
                // 找到该点的帧数mnFound 加 1
                mCurrentFrame.mvpMapPoints[i]->IncreaseFound();
                //查看当前是否是在纯定位过程
                if(!mbOnlyTracking)
                {
                    // 如果该地图点被相机观测数目nObs大于0,匹配内点计数+1
                    // nObs: 被观测到的相机数目,单目+1,双目或RGB-D则+2
                    if(mCurrentFrame.mvpMapPoints[i]->Observations()>0)
                        mnMatchesInliers++;
                }
                else
                    // 记录当前帧跟踪到的地图点数目,用于统计跟踪效果
                    mnMatchesInliers++;
            }
            // 如果这个地图点是外点,并且当前相机输入还是双目的时候,就删除这个点
            // ?单目就不管吗
            else if(mSensor==System::STEREO)  
                mCurrentFrame.mvpMapPoints[i] = static_cast<MapPoint*>(NULL);

        }
    }

    // Decide if the tracking was succesful
    // More restrictive if there was a relocalization recently
    // Step 5:根据跟踪匹配数目及重定位情况决定是否跟踪成功
    // 如果最近刚刚发生了重定位,那么至少成功匹配50个点才认为是成功跟踪
    if(mCurrentFrame.mnId<mnLastRelocFrameId+mMaxFrames && mnMatchesInliers<50)
        return false;

    //如果是正常的状态话只要跟踪的地图点大于30个就认为成功了
    if(mnMatchesInliers<30)
        return false;
    else
        return true;
}

1.5 关键帧生成

1.5.1 生成条件

跟踪成功后,还需要判断是否需要生成关键帧,为了确保定位精度,并且不会对大场景造成过多的计算负担,所以是否生成关键帧,需同时满足以下条件:

  1. 系统模式判断,如果仅仅需要跟踪定位,不需要建图,那么不需要生成关键帧
  2. 根据地图中关键帧的数量设置一些参数(系统一开始关键帧少的时候,可以放宽一些条件,多创建一些关键帧);
  3. 距离上一次重定位距离至少1S
  4. 当前帧跟踪至少50个点,保证精度;
  5. 当前帧跟踪到LocalMap中参考帧的地图点数量少于90%,确保关键帧之间有明显的视觉变化;
  6. 局部地图线程空闲或距离上一次加入关键帧过去了20帧(如果需要关键帧插入过了20帧。而LocalMapping线程忙,则发送信号给线程,停止局部地图优化,使得新的关键帧可以被及时处理)。
// 判断当前帧是否需要插入关键帧

bool Tracking::NeedNewKeyFrame()
{
    // Step 1:纯VO模式下不插入关键帧
    if(mbOnlyTracking)
        return false;

    // If Local Mapping is freezed by a Loop Closure do not insert keyframes
    // Step 2:如果局部地图线程被闭环检测使用,则不插入关键帧
    if(mpLocalMapper->isStopped() || mpLocalMapper->stopRequested())
        return false;
    // 获取当前地图中的关键帧数目
    const int nKFs = mpMap->KeyFramesInMap();

    // Do not insert keyframes if not enough frames have passed from last relocalisation
    // mCurrentFrame.mnId是当前帧的ID
    // mnLastRelocFrameId是最近一次重定位帧的ID
    // mMaxFrames等于图像输入的帧率
    //  Step 3:如果距离上一次重定位比较近,并且关键帧数目超出最大限制,不插入关键帧
    if( mCurrentFrame.mnId < mnLastRelocFrameId + mMaxFrames && nKFs>mMaxFrames)                                     
        return false;

    // Tracked MapPoints in the reference keyframe
    // Step 4:得到参考关键帧跟踪到的地图点数量
    // UpdateLocalKeyFrames 函数中会将与当前关键帧共视程度最高的关键帧设定为当前帧的参考关键帧 

    // 地图点的最小观测次数
    int nMinObs = 3;
    if(nKFs<=2)
        nMinObs=2;
    // 参考关键帧地图点中观测的数目>= nMinObs的地图点数目
    int nRefMatches = mpReferenceKF->TrackedMapPoints(nMinObs);

    // Local Mapping accept keyframes?
    // Step 5:查询局部地图线程是否繁忙,当前能否接受新的关键帧
    bool bLocalMappingIdle = mpLocalMapper->AcceptKeyFrames();

    // Check how many "close" points are being tracked and how many could be potentially created.
    // Step 6:对于双目或RGBD摄像头,统计成功跟踪的近点的数量,如果跟踪到的近点太少,没有跟踪到的近点较多,可以插入关键帧
     int nNonTrackedClose = 0;  //双目或RGB-D中没有跟踪到的近点
    int nTrackedClose= 0;       //双目或RGB-D中成功跟踪的近点(三维点)
    if(mSensor!=System::MONOCULAR)
    {
        for(int i =0; i<mCurrentFrame.N; i++)
        {
            // 深度值在有效范围内
            if(mCurrentFrame.mvDepth[i]>0 && mCurrentFrame.mvDepth[i]<mThDepth)
            {
                if(mCurrentFrame.mvpMapPoints[i] && !mCurrentFrame.mvbOutlier[i])
                    nTrackedClose++;
                else
                    nNonTrackedClose++;
            }
        }
    }

    // 双目或RGBD情况下:跟踪到的地图点中近点太少 同时 没有跟踪到的三维点太多,可以插入关键帧了
    // 单目时,为false
    bool bNeedToInsertClose = (nTrackedClose<100) && (nNonTrackedClose>70);

    // Step 7:决策是否需要插入关键帧
    // Thresholds
    // Step 7.1:设定比例阈值,当前帧和参考关键帧跟踪到点的比例,比例越大,越倾向于增加关键帧
    float thRefRatio = 0.75f;

    // 关键帧只有一帧,那么插入关键帧的阈值设置的低一点,插入频率较低
    if(nKFs<2)
        thRefRatio = 0.4f;

    //单目情况下插入关键帧的频率很高    
    if(mSensor==System::MONOCULAR)
        thRefRatio = 0.9f;

    // Condition 1a: More than "MaxFrames" have passed from last keyframe insertion
    // Step 7.2:很长时间没有插入关键帧,可以插入
    const bool c1a = mCurrentFrame.mnId>=mnLastKeyFrameId+mMaxFrames;

    // Condition 1b: More than "MinFrames" have passed and Local Mapping is idle
    // Step 7.3:满足插入关键帧的最小间隔并且localMapper处于空闲状态,可以插入
    const bool c1b = (mCurrentFrame.mnId>=mnLastKeyFrameId+mMinFrames && bLocalMappingIdle);

    // Condition 1c: tracking is weak
    // Step 7.4:在双目,RGB-D的情况下当前帧跟踪到的点比参考关键帧的0.25倍还少,或者满足bNeedToInsertClose
    const bool c1c =  mSensor!=System::MONOCULAR &&             //只考虑在双目,RGB-D的情况
                    (mnMatchesInliers<nRefMatches*0.25 ||       //当前帧和地图点匹配的数目非常少
                      bNeedToInsertClose) ;                     //需要插入

    // Condition 2: Few tracked points compared to reference keyframe. Lots of visual odometry compared to map matches.
    // Step 7.5:和参考帧相比当前跟踪到的点太少 或者满足bNeedToInsertClose;同时跟踪到的内点还不能太少
    const bool c2 = ((mnMatchesInliers<nRefMatches*thRefRatio|| bNeedToInsertClose) && mnMatchesInliers>15);

    if((c1a||c1b||c1c)&&c2)
    {
        // If the mapping accepts keyframes, insert keyframe.
        // Otherwise send a signal to interrupt BA
        // Step 7.6:local mapping空闲时可以直接插入,不空闲的时候要根据情况插入
        if(bLocalMappingIdle)
        {
            //可以插入关键帧
            return true;
        }
        else
        {
            mpLocalMapper->InterruptBA();
            if(mSensor!=System::MONOCULAR)
            {
                // 队列里不能阻塞太多关键帧
                // tracking插入关键帧不是直接插入,而且先插入到mlNewKeyFrames中,
                // 然后localmapper再逐个pop出来插入到mspKeyFrames
                if(mpLocalMapper->KeyframesInQueue()<3)
                    //队列中的关键帧数目不是很多,可以插入
                    return true;
                else
                    //队列中缓冲的关键帧数目太多,暂时不能插入
                    return false;
            }
            else
                //对于单目情况,就直接无法插入关键帧了
                //可能是单目关键帧相对比较密集
                return false;
        }
    }
    else
        //不满足上面的条件,自然不能插入关键帧
        return false;
}

1.5.2 创建关键帧

判断是否生成关键帧之后,利用CreateNewKeyFrame()创建关键帧,详细的步骤如下:

  1. 构造关键帧;
  2. 当前关键帧设置为当前帧的参考关键帧;
  3. 根据Tcw计算额外矩阵,关键帧相对于普通帧会额外计算一些矩阵( mRcw 旋转矩阵、mRwc 旋转矩阵的逆、mtcw 平移向量和 mOw 光心在世界坐标系下的坐标);
  4. 获取正深度特征点,用于重新构建地图点;
  5. 按照深度从小到大排序;
  6. 将距离比较近的点包装成地图点;
  7. 将关键帧加入到待处理队列 mlNewKeyFrames 中供局部建图线程处理。
// 创建关键帧

void Tracking::CreateNewKeyFrame()
{
    // 如果局部建图线程关闭了,就无法插入关键帧
    if(!mpLocalMapper->SetNotStop(true))
        return;

    // Step 1:将当前帧构造成关键帧
    KeyFrame* pKF = new KeyFrame(mCurrentFrame,mpMap,mpKeyFrameDB);
    // Step 2:将当前关键帧设置为当前帧的参考关键帧
    // 在UpdateLocalKeyFrames函数中会将与当前关键帧共视程度最高的关键帧设定为当前帧的参考关键帧
    mpReferenceKF = pKF;
    mCurrentFrame.mpReferenceKF = pKF;

    // 这段代码和 Tracking::UpdateLastFrame 中的那一部分代码功能相同
    // Step 3:对于双目或rgbd摄像头,为当前帧生成新的地图点;单目无操作
    if(mSensor!=System::MONOCULAR)
    {
        // 根据Tcw计算mRcw、mtcw和mRwc、mOw
        mCurrentFrame.UpdatePoseMatrices();

        // We sort points by the measured depth by the stereo/RGBD sensor.
        // We create all those MapPoints whose depth < mThDepth.
        // If there are less than 100 close points we create the 100 closest.
        // Step 3.1:得到当前帧有深度值的特征点(不一定是地图点)
        vector<pair<float,int> > vDepthIdx;
        vDepthIdx.reserve(mCurrentFrame.N);
        for(int i=0; i<mCurrentFrame.N; i++)
        {
            float z = mCurrentFrame.mvDepth[i];
            if(z>0)
            {
                // 第一个元素是深度,第二个元素是对应的特征点的id
                vDepthIdx.push_back(make_pair(z,i));
            }
        }

        if(!vDepthIdx.empty())
        {
            // Step 3.2:按照深度从小到大排序
            sort(vDepthIdx.begin(),vDepthIdx.end());

            // Step 3.3:从中找出不是地图点的包装为地图点 
            // 处理的近点的个数
            int nPoints = 0;
            for(size_t j=0; j<vDepthIdx.size();j++)
            {
                int i = vDepthIdx[j].second;

                bool bCreateNew = false;

                // 如果这个点对应在上一帧中的地图点没有,或者创建后就没有被观测到,那么就包装为地图点
                MapPoint* pMP = mCurrentFrame.mvpMapPoints[i];
                if(!pMP)
                    bCreateNew = true;
                else if(pMP->Observations()<1)
                {
                    bCreateNew = true;
                    mCurrentFrame.mvpMapPoints[i] = static_cast<MapPoint*>(NULL);
                }

                // 如果需要就新建地图点,这里的地图点不是临时的,是全局地图中新建地图点,用于跟踪
                if(bCreateNew)
                {
                    cv::Mat x3D = mCurrentFrame.UnprojectStereo(i);
                    MapPoint* pNewMP = new MapPoint(x3D,pKF,mpMap);
                    // 这些添加属性的操作是每次创建MapPoint后都要做的
                    pNewMP->AddObservation(pKF,i);
                    pKF->AddMapPoint(pNewMP,i);
                    pNewMP->ComputeDistinctiveDescriptors();
                    pNewMP->UpdateNormalAndDepth();
                    mpMap->AddMapPoint(pNewMP);

                    mCurrentFrame.mvpMapPoints[i]=pNewMP;
                    nPoints++;
                }
                else
                {
                    // 因为从近到远排序,记录其中不需要创建地图点的个数
                    nPoints++;
                }

                // Step 3.4:停止新建地图点必须同时满足以下条件:
                // 1、当前的点的深度已经超过了设定的深度阈值(35倍基线)
                // 2、nPoints已经超过100个点,说明距离比较远了,可能不准确,停掉退出
                if(vDepthIdx[j].first>mThDepth && nPoints>100)
                    break;
            }
        }
    }

    // Step 4:插入关键帧
    // 关键帧插入到列表 mlNewKeyFrames中,等待local mapping线程临幸
    mpLocalMapper->InsertKeyFrame(pKF);

    // 插入好了,允许局部建图停止
    mpLocalMapper->SetNotStop(false);

    // 当前帧成为新的关键帧,更新
    mnLastKeyFrameId = mCurrentFrame.mnId;
    mpLastKeyFrame = pKF;
}

Reference:

  1. https://github.com/raulmur/ORB_SLAM2
  2. https://github.com/electech6/ORB_SLAM2_detailed_comments/tree/master
  3. https://blog.csdn.net/qq_41694024/article/details/128321440



须知少时凌云志,曾许人间第一流。



⭐️👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍🌔

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/972996.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

windows编程之线程同步万字总结(创建线程,互斥对象,互斥事件,信号量,关键段,多线程群聊服务器)

文章目录 创建线程方法一_beginthreadex函数讲解使用示例&#xff1a; 方法二CreateThread函数讲解:使用示例: 互斥对象:创建互斥对象CreateMutex 互斥事件介绍创建或打开一个未命名的互斥事件对象 信号量介绍信号量的相关函数使用示例 关键段相关函数错误使用示例正确使用示例…

vite介绍

vite vite是一种新的前端构建工具&#xff0c;vite借助了浏览器对ESM的支持&#xff0c;采用和传统webpack打包完全不一致的unbundle打包机制&#xff1b; vite的快主要体现在两个方面&#xff0c;快速的冷启动和快速的热更新 快速的冷启动&#xff1a;vite只需启动一台静态页…

如何将Express项目部署到Vercel

什么是Vercel&#xff1f; 想必好多前端同学都知道Vercel吧&#xff01;如果还不了解的同学也没关系&#xff0c;好好看这篇文章&#xff0c;认识认识Vercel&#xff0c;我想对你部署项目有一定帮助。 Vercel 是一个云平台&#xff0c;用于托管和部署静态网站、前端应用程序以…

2023年无形资产评估研究报告

第一章 无形资产概况 1.1 定义 无形资产是一种缺乏物质实体的资产。例如&#xff0c;专利、版权、特许权、商誉、商标和商号&#xff0c;以及软件等。这与物质资产&#xff08;如机器、建筑等&#xff09;和金融资产&#xff08;如政府证券等&#xff09;形成了对比。无形资产…

Mybatis 动态SQL – 使用choose标签动态生成条件语句

之前我们介绍了if,where标签的使用&#xff1b;本篇我们需要在if,where标签的基础上介绍如何使用Mybatis提供的choose标签动态生成条件语句。 如果您对if,where标签动态生成条件语句不太了解&#xff0c;建议您先进行了解后再阅读本篇&#xff0c;可以参考&#xff1a; Mybat…

将序数与比特币智能合约集成:第 1 部分

将序数与比特币智能合约集成&#xff1a;第 1 部分 最近&#xff0c;比特币序数在区块链领域引起了广泛关注。 据称&#xff0c;与以太坊 ERC-721 等其他代币标准相比&#xff0c;Ordinals 的一个主要缺点是缺乏对智能合约的支持。 我们展示了如何向 Ordinals 添加智能合约功…

插入排序,选择排序,交换排序,归并排序和非比较排序(C语言版)

前言 所谓排序&#xff0c;就是将一组数据按照递增或者递减的方式进行排列&#xff0c;让这组数据变得有序起来。排序在生活中运用的是十分广泛的&#xff0c;各行各业都用到了排序&#xff0c;比如我们在网购的时候就是按照某种排序的方式来选择东西的。所以去了解排序的实现也…

vue 使用qrcode生成二维码并可下载保存

安装qrcode npm install qrcode --save代码 <template><div style"display: flex; flex-direction: column; align-items: center; justify-content center;"><div>查看溯源码&#xff0c;<a id"saveLink" style"text-decorati…

Ae 效果:CC Glue Gun

生成/CC Glue Gun Generate/CC Glue Gun CC Glue Gun&#xff08;CC 胶水枪&#xff09;可以用于生成仿佛由胶水枪绘制的线条或图案&#xff0c;它模拟了胶水枪绘制在不同表面上的纹理和反光效果。 CC Glue Gun 效果实质上是通过设置画笔笔触的位置来构成画笔描边路径&#xff…

ModaHub魔搭社区专访百度智能云李莅:向量数据库市场会不会更卷?

ModaHub魔搭社区:在当今的信息化时代,数据库技术已经渗透到了我们生活的各个角落。传统的关系型数据库在市场上的竞争已经非常激烈,据统计,市面上有数百种不同类型的数据库产品在竞争。那么,在未来,随着人工智能和大数据技术的发展,向量数据库市场会否也会陷入同样的激烈…

js中如何判断一个变量的数据类型?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐typeof 运算符⭐instanceof 运算符⭐Object.prototype.toString 方法⭐Array.isArray 方法⭐自定义类型检查⭐null 和 undefined 检查⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订…

ClickHouse进阶(七):Clickhouse数据查询-1

进入正文前&#xff0c;感谢宝子们订阅专题、点赞、评论、收藏&#xff01;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; &#x1f3e1;个人主页&#xff1a;含各种IT体系技术,IT贫道_Apache Doris,大数据OLAP体系技术栈,Kerberos安全认证-CSDN博客 &#x1f4cc;订阅…

RK3568-i2c-适配8010rtc时钟芯片

硬件连接 从硬件原理图中可以看出&#xff0c;rtc时钟芯片挂载在i2c3总线上&#xff0c;设备地址需要查看芯片数据手册。编写设备树 &i2c3 {status "okay";rx8010: rx801032 {compatible "epson,rx8010";reg <0x32>;}; };使能驱动 /kernel/…

NPM 常用命令(三)

目录 1、npm compltion 1.1 描述 2、npm config 2.1 常用命令 2.2 描述 set get list delete edit fix 2.3 配置 json global editor location long 3、npm dedupe 3.1 描述 3.2 配置 4、npm deprecate 4.1 命令使用 4.2 描述 4.3 配置 registry ot…

【huggingface】数据集及模型下载并保存至本地

目录 数据集ChnSentiCorppeoples_daily_ner 模型bert-base-chinesehfl/rbt3t5-baseopus-mt-zh-enChinese_Chat_T5_Base 环境&#xff1a;没有代理&#xff0c;无法访问部分国外网络 数据集 正常情况下通过load_dataset加载数据集&#xff1b;save_to_disk保存至本地&#xff1b…

佳作导读 | 《C++ Core Guidelines》

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 佳作导读 | 《C Core Guidelines》 《C Core Guidelines》由Bjarne Stroustrup和Herb Sutter等共同编写关于使用C编程语言的指南&#xff1b;旨在提供关于如何使用C进…

Linux常用命令——csplit命令

在线Linux命令查询工具 csplit 将一个大文件分割成小的碎片文件 补充说明 csplit命令用于将一个大文件分割成小的碎片&#xff0c;并且将分割后的每个碎片保存成一个文件。碎片文件的命名类似“xx00”&#xff0c;“xx01”。csplit命令是split的一个变体&#xff0c;split只…

如何在Win10系统上安装WSL(适用于 Linux 的 Windows 子系统)

诸神缄默不语-个人CSDN博文目录 本文介绍的方法不是唯一的安装方案&#xff0c;但在我的系统上可用。 文章目录 1. 视频版2. 文字版和代码3. 本文撰写过程中使用到的其他网络参考资料 1. 视频版 B站版&#xff1a;在Windows上安装Linux (WSL, 适用于 Linux 的 Windows 子系统…

【Rust 日报】2023-09-03 sudo-rs:sudo 和 su 的内存安全实现

sudo-rs&#xff1a;sudo 和 su 的内存安全实现 sudo-rs 项目通过以下方式改进了原始 sudo 的安全性&#xff1a; 使用内存安全语言&#xff08;Rust&#xff09;&#xff0c;因为估计原始 sudo 中三分之一的安全漏洞都与内存管理问题有关。省略不常用的功能&#xff0c;以减少…

记2个library cache lock故障case

第一个case 客户说晚上10点的时候做业务很卡&#xff0c;遂取对应时段awr 非常明显的library cache lock事件。这个事件是元数据锁&#xff0c;一旦泛滥对数据库的影响范围很大。 因此&#xff0c;他的泛滥第一时间应该想到会有大量持有元数据的动作。对sql进行检查 这个自动…