ORB-SLAM2学习笔记10之图像关键帧KeyFrame
文章目录
- 0 引言
- 1 KeyFrame类
- 1.1 构造函数
- 1.2 成员函数
- 1.3 关键帧之间共视图
- 1.3.1 AddConnection
- 1.3.2 UpdateBestCovisibles
- 1.3.3 UpdateConnections
- 1.3.4 EraseConnection
- 1.3.5 SetBadFlag
- 1.4 地图点
- 1.5 生成树
- 2 KeyFrame用途
0 引言
ORB-SLAM2学习笔记7详细了解了System
主类和多线程、ORB-SLAM2学习笔记8详细了解了图像特征点提取和描述子的生成及ORB-SLAM2学习笔记9详细了解了图像帧,本文在此基础上,继续学习ORB-SLAM2
中的图像关键帧,也就是KeyFrame
类,该类中主要包含关键帧姿态计算、关键帧之间共视图、生成树及地图点操作等函数。
1 KeyFrame类
1.1 构造函数
KeyFrame
的构造函数输入普通图像帧,地图对象和关键帧数据库对象三个参数,而且对成员变量进行初始化,比如帧的ID
,时间戳,相机内参,位姿,特征点,描述子等,还复制了帧对象中的网格数据,以用于加速特征点的匹配,最后还调用了SetPose()
函数,用于设置关键帧的位姿。
KeyFrame::KeyFrame(Frame &F, Map *pMap, KeyFrameDatabase *pKFDB):mnFrameId(F.mnId), // 当前帧的IDmTimeStamp(F.mTimeStamp), // 当前帧的时间戳mnGridCols(FRAME_GRID_COLS), // 网格的列数mnGridRows(FRAME_GRID_ROWS), // 网格的行数mfGridElementWidthInv(F.mfGridElementWidthInv), // 网格元素的宽度的倒数mfGridElementHeightInv(F.mfGridElementHeightInv),// 网格元素的高度的倒数mnTrackReferenceForFrame(0), // 当前关键帧被其他关键帧引用的次数mnFuseTargetForKF(0), // 当前关键帧作为融合目标的次数mnBALocalForKF(0), // 当前关键帧作为局部BA的次数mnBAFixedForKF(0), // 当前关键帧作为固定BA的次数mnLoopQuery(0), // 当前关键帧作为回环查询的次数mnLoopWords(0), // 当前关键帧回环匹配的单词数mnRelocQuery(0), // 当前关键帧作为重定位查询的次数mnRelocWords(0), // 当前关键帧重定位匹配的单词数mnBAGlobalForKF(0), // 当前关键帧作为全局BA的次数fx(F.fx), // 相机的焦距xfy(F.fy), // 相机的焦距ycx(F.cx), // 相机的光心xcy(F.cy), // 相机的光心yinvfx(F.invfx), // 焦距x的倒数invfy(F.invfy), // 焦距y的倒数mbf(F.mbf), // 基线乘以焦距的值mb(F.mb), // 相机的基线长度mThDepth(F.mThDepth), // 深度值的阈值N(F.N), // 特征点的数量mvKeys(F.mvKeys), // 特征点的像素坐标mvKeysUn(F.mvKeysUn), // 特征点的归一化坐标mvuRight(F.mvuRight), // 右目特征点的像素坐标mvDepth(F.mvDepth), // 特征点的深度值mDescriptors(F.mDescriptors.clone()), // 特征点的描述子mBowVec(F.mBowVec), // 特征点的词袋表示mFeatVec(F.mFeatVec), // 特征点的尺度金字塔信息mnScaleLevels(F.mnScaleLevels), // 尺度金字塔的层数mfScaleFactor(F.mfScaleFactor), // 尺度金字塔的尺度因子mfLogScaleFactor(F.mfLogScaleFactor), // 尺度金字塔的尺度因子的对数值mvScaleFactors(F.mvScaleFactors), // 尺度金字塔每层的尺度因子mvLevelSigma2(F.mvLevelSigma2), // 尺度金字塔每层的尺度值的平方mvInvLevelSigma2(F.mvInvLevelSigma2), // 尺度金字塔每层尺度的平方的倒数mnMinX(F.mnMinX), // 特征点的最小x坐标mnMinY(F.mnMinY), // 特征点的最小y坐标mnMaxX(F.mnMaxX), // 特征点的最大x坐标mnMaxY(F.mnMaxY), // 特征点的最大y坐标mK(F.mK), // 相机的内参矩阵mvpMapPoints(F.mvpMapPoints), // 关联的地图点的指针mpKeyFrameDB(pKFDB), // 关键帧数据库指针mpORBvocabulary(F.mpORBvocabulary), // ORB词袋模型的指针mbFirstConnection(true), // 是否是第一个与其他关键帧连接的关键帧mpParent(NULL), // 父关键帧的指针mbNotErase(false), // 是否不被删除mbToBeErased(false), // 是否待删除mbBad(false), // 是否是坏关键帧mHalfBaseline(F.mb/2), // 基线长度的一半mpMap(pMap) // 关联的地图指针
{mnId = nNextId++; // 关键帧的唯一标识符// 复制网格数据,用于加速匹配mGrid.resize(mnGridCols);for(int i=0; i<mnGridCols; i++){mGrid[i].resize(mnGridRows);for(int j=0; j<mnGridRows; j++)mGrid[i][j] = F.mGrid[i][j];}SetPose(F.mTcw); // 设置关键帧的位姿
}
1.2 成员函数
KeyFrame
类中的成员函数一览表:
成员函数 | 类型 | 定义 |
---|---|---|
void KeyFrame::ComputeBoW() | public | 计算词袋表示 |
void KeyFrame::SetPose(const cv::Mat &Tcw_) | public | 设置当前关键帧的位姿 |
cv::Mat KeyFrame::GetPose() | public | 获取位姿 |
cv::Mat KeyFrame::GetPoseInverse() | public | 获取位姿的逆 |
cv::Mat KeyFrame::GetCameraCenter() | public | 获取(左目)相机的中心在世界坐标系下的坐标 |
cv::Mat KeyFrame::GetStereoCenter() | public | 获取双目相机的中心,这个只有在可视化时才会用到 |
cv::Mat KeyFrame::GetRotation() | public | 获取姿态,旋转矩阵 |
cv::Mat KeyFrame::GetTranslation() | public | 获取位置,平移矩阵 |
void KeyFrame::AddConnection(KeyFrame *pKF, const int &weight) | public | 为当前关键帧新建或更新和其他关键帧的连接权重 |
void KeyFrame::UpdateBestCovisibles() | public | 按照权重从大到小对连接(共视)的关键帧进行排序 |
set<KeyFrame*> KeyFrame::GetConnectedKeyFrames() | public | 得到与该关键帧连接(大于15个共视地图点)的关键帧(没有排序的) |
vector<KeyFrame*> KeyFrame::GetVectorCovisibleKeyFrames() | public | 得到与该关键帧连接的关键帧(已按权值排序) |
vector<KeyFrame*> KeyFrame::GetBestCovisibilityKeyFrames(const int &N) | public | 得到与该关键帧连接的前N 个最强共视关键帧(已按权值排序) |
vector<KeyFrame*> KeyFrame::GetCovisiblesByWeight(const int &w) | public | 得到与该关键帧连接的权重超过w 的关键帧 |
int KeyFrame::GetWeight(KeyFrame *pKF) | public | 得到该关键帧与pKF 的权重 |
void KeyFrame::AddMapPoint(MapPoint *pMP, const size_t &idx) | public | 添加地图点到关键帧 |
void KeyFrame::EraseMapPointMatch(const size_t &idx);void KeyFrame::EraseMapPointMatch(MapPoint* pMP) | public | 删除bad==true 的地图点,将该地图点置为NULL |
void KeyFrame::ReplaceMapPointMatch(const size_t &idx, MapPoint* pMP) | public | 地图点的替换 |
set<MapPoint*> KeyFrame::GetMapPoints() | public | 获取当前关键帧中的所有地图点 |
int KeyFrame::TrackedMapPoints(const int &minObs) | public | 关键帧中,大于等于最小观测数目minObs 的MapPoints 的数量,这些特征点被认为追踪到了 |
vector<MapPoint*> KeyFrame::GetMapPointMatches();MapPoint* KeyFrame::GetMapPoint(const size_t &idx) | public | 获取当前关键帧的具体的地图点 |
void KeyFrame::UpdateConnections() | public | 更新关键帧之间的连接图 |
void KeyFrame::AddChild(KeyFrame *pKF) | public | 添加子关键帧(即和子关键帧具有最大共视关系的关键帧就是当前关键帧) |
void KeyFrame::EraseChild(KeyFrame *pKF) | public | 删除某个子关键帧 |
void KeyFrame::ChangeParent(KeyFrame *pKF) | public | 改变当前关键帧的父关键帧 |
set<KeyFrame*> KeyFrame::GetChilds() | public | 获取当前关键帧的子关键帧 |
KeyFrame* KeyFrame::GetParent() | public | 获取当前关键帧的父关键帧 |
bool KeyFrame::hasChild(KeyFrame *pKF) | public | 判断某个关键帧是否是当前关键帧的子关键帧 |
void KeyFrame::AddLoopEdge(KeyFrame *pKF) | public | 给当前关键帧添加回环边,回环边连接了形成了闭环关系的关键帧 |
set<KeyFrame*> KeyFrame::GetLoopEdges() | public | 获取和当前关键帧形成闭环关系的关键帧 |
void KeyFrame::SetNotErase() | public | 设置当前关键帧不要在优化的过程中被删除,由回环检测线程调用 |
void KeyFrame::SetErase() | public | 删除当前的这个关键帧,表示不进行回环检测过程,由回环检测线程调用 |
void KeyFrame::SetBadFlag() | public | 真正地执行删除关键帧的操作 |
bool KeyFrame::isBad() | public | 判断关键帧是否是无效的 |
void KeyFrame::EraseConnection(KeyFrame* pKF) | public | 删除当前关键帧和指定关键帧之间的共视关系 |
vector<size_t> KeyFrame::GetFeaturesInArea(const float &x, const float &y, const float &r) const | public | 获取某个特征点的邻域中的特征点id ,类比Frame 类的函数 |
bool KeyFrame::IsInImage(const float &x, const float &y) const | public | 判断某个点是否在当前关键帧的图像中 |
cv::Mat KeyFrame::UnprojectStereo(int i) | public | 在双目和RBGD 情况下将特征点反投影到空间中得到世界坐标系下三维点 |
float KeyFrame::ComputeSceneMedianDepth(const int q) | public | 评估当前关键帧场景深度,q=2 表示中值,只是在单目情况下才会使用 |
成员函数比较多,有些函数功能虽不可少但比较简单,以下重点围绕关键帧之间共视关系Covisibility Graph
、生成树Spanning tree
和地图点MapPoint
这三部分重点学习。
而实际上主要就是为了图优化,图优化需要构建节点和边,节点即关键帧的位姿,位姿的相关函数这里就不细说了,而边有两种:
- 和其他关键帧之间的边,需要通过
MapPoint
产生联系,两帧都能够共同观测到一定数量的MapPoint
,建立共视关系,所以需要有管理关键帧之间共视关系的函数;(详见1.3
) - 和
MapPoint
之间的边, 所以也需要管理和MapPoint
之间关系的函数;(详见1.4
)
至于生成树,由于和其他关键帧之间的边仍旧有很多,为了简化并提高计算速度,ORB-SLAM2
中的通过生成树来管理各关键帧之间的关系,设定每个帧都有一个父节点和子节点,节点是其他关键帧,在构建优化模型时,只有具有父子关系的关键帧之间才能建立边,这样能大大减少边的数量,因此,还需要管理生成树的函数。(详见1.5
)
1.3 关键帧之间共视图
ORB-SLAM论文中有张图能比较清晰地展示关键帧,地图点,共视图,生成树等关系:
- 图
a
绿色表示当前相机,蓝色表示关键帧,红色和黑色表示地图点; - 图b绿色即是共视图
Covisibility graph
,共视图用来描述不同关键帧可以看到多少相同的地图点:每个关键帧是一个节点,如果两个关键帧之间的共视地图点数量大于15
,则两个节点建立边; - 图
c
绿色即是生成树Spanning tree
,生成树保留了所有关键帧的节点,但给各个关键帧设定了父节点和子节点,每帧只跟各自的父节点和子节点相连; - 图
d
绿色即是本质图essential graph
,是根据生成树建立的图模型,简化版的共视图。
1.3.1 AddConnection
该函数主要目的是新建或更新关键帧之间的连接权重:
- 输入参数:
pKF
是当前关键帧共视的其他关键帧 - 输入参数:
weight
是当前关键帧和其他关键帧的权重(共视地图点数目)
// 为当前关键帧新建或更新和其他关键帧的连接权重void KeyFrame::AddConnection(KeyFrame *pKF, const int &weight)
{{// 互斥锁,防止同时操作共享数据产生冲突unique_lock<mutex> lock(mMutexConnections);// 新建或更新连接权重if(!mConnectedKeyFrameWeights.count(pKF)) // count函数返回0,说明mConnectedKeyFrameWeights中没有pKF,新建连接mConnectedKeyFrameWeights[pKF]=weight;else if(mConnectedKeyFrameWeights[pKF]!=weight) // 之前连接的权重不一样了,需要更新mConnectedKeyFrameWeights[pKF]=weight;elsereturn;}// 连接关系变化就要更新最佳共视,主要是重新进行排序UpdateBestCovisibles();
}
1.3.2 UpdateBestCovisibles
该函数主要目的是按照连接权重weight
对连接的关键帧进行排序,按权重大小从大到小排列,更新后的变量存储在mvpOrderedConnectedKeyFrames
和 mvOrderedWeights
中。
// 按照权重从大到小对连接(共视)的关键帧进行排序void KeyFrame::UpdateBestCovisibles()
{// 互斥锁,防止同时操作共享数据产生冲突unique_lock<mutex> lock(mMutexConnections);// http://stackoverflow.com/questions/3389648/difference-between-stdliststdpair-and-stdmap-in-c-stl (std::map 和 std::list<std::pair>的区别)vector<pair<int,KeyFrame*> > vPairs;vPairs.reserve(mConnectedKeyFrameWeights.size());// 取出所有连接的关键帧,mConnectedKeyFrameWeights的类型为std::map<KeyFrame*,int>,而vPairs变量将共视的地图点数放在前面,利于排序for(map<KeyFrame*,int>::iterator mit=mConnectedKeyFrameWeights.begin(), mend=mConnectedKeyFrameWeights.end(); mit!=mend; mit++)vPairs.push_back(make_pair(mit->second,mit->first));// 按照权重进行排序(默认是从小到大)sort(vPairs.begin(),vPairs.end());// 为什么要用链表保存?因为插入和删除操作方便,只需要修改上一节点位置,不需要移动其他元素list<KeyFrame*> lKFs; // 所有连接关键帧list<int> lWs; // 所有连接关键帧对应的权重(共视地图点数目)for(size_t i=0, iend=vPairs.size(); i<iend;i++){// push_front 后变成从大到小lKFs.push_front(vPairs[i].second);lWs.push_front(vPairs[i].first);}// 权重从大到小排列的连接关键帧mvpOrderedConnectedKeyFrames = vector<KeyFrame*>(lKFs.begin(),lKFs.end());// 从大到小排列的权重,和mvpOrderedConnectedKeyFrames一一对应mvOrderedWeights = vector<int>(lWs.begin(), lWs.end());
}
1.3.3 UpdateConnections
该函数主要目的是更新关键帧之间的连接图,主要分为三步:
-
首先获得该关键帧的所有
MapPoint
点,统计观测到这些3D
点的每个关键帧与其它所有关键帧之间的共视程度,对每一个找到的关键帧,建立一条边,边的权重是该关键帧与当前关键帧公共3D
点的个数; -
并且该权重必须大于一个阈值,如果没有超过该阈值的权重,那么就只保留权重最大的边(与其它关键帧的共视程度比较高);
-
对这些连接按照权重从大到小进行排序,以方便将来的处理,更新完
Covisibility graph
之后,如果没有初始化过,则初始化为连接权重最大的边(与其它关键帧共视程度最高的那个关键帧),类似于最大生成树。
// 更新关键帧之间的连接图void KeyFrame::UpdateConnections()
{// 关键帧-权重,权重为其它关键帧与当前关键帧共视地图点的个数,也称为共视程度map<KeyFrame*,int> KFcounter; vector<MapPoint*> vpMP;{// 获得该关键帧的所有地图点unique_lock<mutex> lockMPs(mMutexFeatures);vpMP = mvpMapPoints;}//For all map points in keyframe check in which other keyframes are they seen//Increase counter for those keyframes// Step 1 通过地图点被关键帧观测来间接统计关键帧之间的共视程度// 统计每一个地图点都有多少关键帧与当前关键帧存在共视关系,统计结果放在KFcounterfor(vector<MapPoint*>::iterator vit=vpMP.begin(), vend=vpMP.end(); vit!=vend; vit++){MapPoint* pMP = *vit;if(!pMP)continue;if(pMP->isBad())continue;// 对于每一个地图点,observations记录了可以观测到该地图点的所有关键帧map<KeyFrame*,size_t> observations = pMP->GetObservations();for(map<KeyFrame*,size_t>::iterator mit=observations.begin(), mend=observations.end(); mit!=mend; mit++){// 除去自身,自己与自己不算共视if(mit->first->mnId==mnId)continue;// 这里的操作非常精彩!// map[key] = value,当要插入的键存在时,会覆盖键对应的原来的值。如果键不存在,则添加一组键值对// mit->first 是地图点看到的关键帧,同一个关键帧看到的地图点会累加到该关键帧计数// 所以最后KFcounter 第一个参数表示某个关键帧,第2个参数表示该关键帧看到了多少当前帧的地图点,也就是共视程度KFcounter[mit->first]++;}}// This should not happen// 没有共视关系,直接退出 if(KFcounter.empty())return;// If the counter is greater than threshold add connection// In case no keyframe counter is over threshold add the one with maximum counterint nmax=0; // 记录最高的共视程度KeyFrame* pKFmax=NULL;// 至少有15个共视地图点才会添加共视关系int th = 15;// vPairs记录与其它关键帧共视帧数大于th的关键帧// pair<int,KeyFrame*>将关键帧的权重写在前面,关键帧写在后面方便后面排序vector<pair<int,KeyFrame*> > vPairs;vPairs.reserve(KFcounter.size());// Step 2 找到对应权重最大的关键帧(共视程度最高的关键帧)for(map<KeyFrame*,int>::iterator mit=KFcounter.begin(), mend=KFcounter.end(); mit!=mend; mit++){if(mit->second>nmax){nmax=mit->second;pKFmax=mit->first;}// 建立共视关系至少需要大于等于th个共视地图点if(mit->second>=th){// 对应权重需要大于阈值,对这些关键帧建立连接vPairs.push_back(make_pair(mit->second,mit->first));// 对方关键帧也要添加这个信息// 更新KFcounter中该关键帧的mConnectedKeyFrameWeights// 更新其它KeyFrame的mConnectedKeyFrameWeights,更新其它关键帧与当前帧的连接权重(mit->first)->AddConnection(this,mit->second);}}// Step 3 如果没有超过阈值的权重,则对权重最大的关键帧建立连接if(vPairs.empty()){// 如果每个关键帧与它共视的关键帧的个数都少于th,// 那就只更新与其它关键帧共视程度最高的关键帧的mConnectedKeyFrameWeights// 这是对之前th这个阈值可能过高的一个补丁vPairs.push_back(make_pair(nmax,pKFmax));pKFmax->AddConnection(this,nmax);}// Step 4 对满足共视程度的关键帧对更新连接关系及权重(从大到小)// vPairs里存的都是相互共视程度比较高的关键帧和共视权重,接下来由大到小进行排序sort(vPairs.begin(),vPairs.end()); // sort函数默认升序排列// 将排序后的结果分别组织成为两种数据类型list<KeyFrame*> lKFs;list<int> lWs;for(size_t i=0; i<vPairs.size();i++){// push_front 后变成了从大到小顺序lKFs.push_front(vPairs[i].second);lWs.push_front(vPairs[i].first);}{unique_lock<mutex> lockCon(mMutexConnections);// mspConnectedKeyFrames = spConnectedKeyFrames;// 更新当前帧与其它关键帧的连接权重// ?bug 这里直接赋值,会把小于阈值的共视关系也放入mConnectedKeyFrameWeights,会增加计算量// ?但后续主要用mvpOrderedConnectedKeyFrames来取共视帧,对结果没影响mConnectedKeyFrameWeights = KFcounter;mvpOrderedConnectedKeyFrames = vector<KeyFrame*>(lKFs.begin(),lKFs.end());mvOrderedWeights = vector<int>(lWs.begin(), lWs.end());// Step 5 更新生成树的连接if(mbFirstConnection && mnId!=0){// 初始化该关键帧的父关键帧为共视程度最高的那个关键帧mpParent = mvpOrderedConnectedKeyFrames.front();// 建立双向连接关系,将当前关键帧作为其子关键帧mpParent->AddChild(this);mbFirstConnection = false;}}
}
1.3.4 EraseConnection
该函数的主要目的是删除当前关键帧和指定关键帧之间的共视关系。
// 删除当前关键帧和指定关键帧之间的共视关系void KeyFrame::EraseConnection(KeyFrame* pKF)
{// 其实这个应该表示是否真的是有共视关系bool bUpdate = false;{unique_lock<mutex> lock(mMutexConnections);if(mConnectedKeyFrameWeights.count(pKF)){mConnectedKeyFrameWeights.erase(pKF);bUpdate=true;}}// 如果是真的有共视关系,那么删除之后就要更新共视关系if(bUpdate)UpdateBestCovisibles();
}
1.3.5 SetBadFlag
该函数的主要目的真正地执行删除关键帧及相关的共视图和生成树,但是直接删除会引入一些问题,比如删除了一个父节点,那和它相关的子节点怎么办?ORB-SLAM2
中是直接给这些子节点换个父节点。详细的步骤如下:
- 首先处理一下删除不了的特殊情况,比如第
0
帧不允许被删除; - 遍历所有和当前关键帧相连的关键帧,删除他们与当前关键帧的联系;
- 遍历每一个当前关键帧的地图点,删除每一个地图点和当前关键帧的联系;
- 更新生成树,主要是处理好父子关键帧,不然会造成整个关键帧维护的图断裂或混乱;
- 遍历每一个子关键帧,让它们更新它们指向的父关键帧;
- 子关键帧遍历每一个与它共视的关键帧;
sParentCandidates
中刚开始存的是这里子关键帧的“爷爷”,也是当前关键帧的候选父关键帧,如果孩子和sParentCandidates
中有共视,选择共视最强的那个作为新的父节点;(可以理解成爷爷变父亲了)
// 真正地执行删除关键帧的操作void KeyFrame::SetBadFlag()
{ // Step 1 首先处理一下删除不了的特殊情况{unique_lock<mutex> lock(mMutexConnections);// 第0关键帧不允许被删除if(mnId==0)return;else if(mbNotErase){// mbNotErase表示不应该删除,于是把mbToBeErased置为true,假装已经删除,其实没有删除mbToBeErased = true;return;}}// Step 2 遍历所有和当前关键帧相连的关键帧,删除他们与当前关键帧的联系for(map<KeyFrame*,int>::iterator mit = mConnectedKeyFrameWeights.begin(), mend=mConnectedKeyFrameWeights.end(); mit!=mend; mit++)mit->first->EraseConnection(this); // 让其它的关键帧删除与自己的联系// Step 3 遍历每一个当前关键帧的地图点,删除每一个地图点和当前关键帧的联系for(size_t i=0; i<mvpMapPoints.size(); i++)if(mvpMapPoints[i])mvpMapPoints[i]->EraseObservation(this); {unique_lock<mutex> lock(mMutexConnections);unique_lock<mutex> lock1(mMutexFeatures);// 清空自己与其它关键帧之间的联系mConnectedKeyFrameWeights.clear();mvpOrderedConnectedKeyFrames.clear();// Update Spanning Tree // Step 4 更新生成树,主要是处理好父子关键帧,不然会造成整个关键帧维护的图断裂,或者混乱// 候选父关键帧set<KeyFrame*> sParentCandidates;// 将当前帧的父关键帧放入候选父关键帧sParentCandidates.insert(mpParent);// Assign at each iteration one children with a parent (the pair with highest covisibility weight)// Include that children as new parent candidate for the rest// 每迭代一次就为其中一个子关键帧寻找父关键帧(最高共视程度),找到父的子关键帧可以作为其他子关键帧的候选父关键帧while(!mspChildrens.empty()){bool bContinue = false;int max = -1;KeyFrame* pC;KeyFrame* pP;// Step 4.1 遍历每一个子关键帧,让它们更新它们指向的父关键帧for(set<KeyFrame*>::iterator sit=mspChildrens.begin(), send=mspChildrens.end(); sit!=send; sit++){KeyFrame* pKF = *sit;// 跳过无效的子关键帧if(pKF->isBad()) continue;// Check if a parent candidate is connected to the keyframe// Step 4.2 子关键帧遍历每一个与它共视的关键帧 vector<KeyFrame*> vpConnected = pKF->GetVectorCovisibleKeyFrames();for(size_t i=0, iend=vpConnected.size(); i<iend; i++){// sParentCandidates 中刚开始存的是这里子关键帧的“爷爷”,也是当前关键帧的候选父关键帧for(set<KeyFrame*>::iterator spcit=sParentCandidates.begin(), spcend=sParentCandidates.end(); spcit!=spcend; spcit++){// Step 4.3 如果孩子和sParentCandidates中有共视,选择共视最强的那个作为新的父if(vpConnected[i]->mnId == (*spcit)->mnId){int w = pKF->GetWeight(vpConnected[i]);// 寻找并更新权值最大的那个共视关系if(w>max){pC = pKF; //子关键帧pP = vpConnected[i]; //目前和子关键帧具有最大权值的关键帧(将来的父关键帧) max = w; //这个最大的权值bContinue = true; //说明子节点找到了可以作为其新父关键帧的帧}}}}}// Step 4.4 如果在上面的过程中找到了新的父节点// 下面代码应该放到遍历子关键帧循环中?// 回答:不需要!这里while循环还没退出,会使用更新的sParentCandidatesif(bContinue){// 因为父节点死了,并且子节点找到了新的父节点,就把它更新为自己的父节点pC->ChangeParent(pP);// 因为子节点找到了新的父节点并更新了父节点,那么该子节点升级,作为其它子节点的备选父节点sParentCandidates.insert(pC);// 该子节点处理完毕,删掉mspChildrens.erase(pC);}elsebreak;}// If a children has no covisibility links with any parent candidate, assign to the original parent of this KF// Step 4.5 如果还有子节点没有找到新的父节点if(!mspChildrens.empty())for(set<KeyFrame*>::iterator sit=mspChildrens.begin(); sit!=mspChildrens.end(); sit++){// 直接把父节点的父节点作为自己的父节点 即对于这些子节点来说,他们的新的父节点其实就是自己的爷爷节点(*sit)->ChangeParent(mpParent);}mpParent->EraseChild(this);// mTcp 表示原父关键帧到当前关键帧的位姿变换,在保存位姿的时候使用mTcp = Tcw*mpParent->GetPoseInverse();// 标记当前关键帧已经挂了mbBad = true;} // 地图和关键帧数据库中删除该关键帧mpMap->EraseKeyFrame(this);mpKeyFrameDB->erase(this);
}
1.4 地图点
和地图点相关的函数,主要是针对存放MapPoint的容器mvpMapPoint进行的,比如新增,删除,替换等操作。
// Add MapPoint to KeyFrame
// 新增 MapPoint
void KeyFrame::AddMapPoint(MapPoint *pMP, const size_t &idx)
{unique_lock<mutex> lock(mMutexFeatures);mvpMapPoints[idx]=pMP;
}
/*** @brief 由于其他的原因,导致当前关键帧观测到的某个地图点被删除(bad==true)了,将该地图点置为NULL* * @param[in] idx 地图点在该关键帧中的id*/
void KeyFrame::EraseMapPointMatch(const size_t &idx)
{unique_lock<mutex> lock(mMutexFeatures);// NOTE 使用这种方式表示其中的某个地图点被删除mvpMapPoints[idx]=static_cast<MapPoint*>(NULL);
}// 同上
void KeyFrame::EraseMapPointMatch(MapPoint* pMP)
{//获取当前地图点在某个关键帧的观测中,对应的特征点的索引,如果没有观测,索引为-1int idx = pMP->GetIndexInKeyFrame(this);if(idx>=0)mvpMapPoints[idx]=static_cast<MapPoint*>(NULL);
}// 地图点的替换
void KeyFrame::ReplaceMapPointMatch(const size_t &idx, MapPoint* pMP)
{mvpMapPoints[idx]=pMP;
}// 获取当前关键帧中的所有地图点
set<MapPoint*> KeyFrame::GetMapPoints()
{unique_lock<mutex> lock(mMutexFeatures);set<MapPoint*> s;for(size_t i=0, iend=mvpMapPoints.size(); i<iend; i++){// 判断是否被删除了if(!mvpMapPoints[i])continue;MapPoint* pMP = mvpMapPoints[i];// 如果是没有来得及删除的坏点也要进行这一步if(!pMP->isBad())s.insert(pMP);}return s;
}// 关键帧中,大于等于最少观测数目minObs的MapPoints的数量.这些特征点被认为追踪到了
int KeyFrame::TrackedMapPoints(const int &minObs)
{unique_lock<mutex> lock(mMutexFeatures);int nPoints=0;// 是否检查数目const bool bCheckObs = minObs>0;// N是当前帧中特征点的个数for(int i=0; i<N; i++){MapPoint* pMP = mvpMapPoints[i];if(pMP) //没有被删除{if(!pMP->isBad()) //并且不是坏点{if(bCheckObs){// 满足输入阈值要求的地图点计数加1if(mvpMapPoints[i]->Observations()>=minObs)nPoints++;}elsenPoints++; //!bug}}}return nPoints;
}// 获取当前关键帧的具体的地图点
vector<MapPoint*> KeyFrame::GetMapPointMatches()
{unique_lock<mutex> lock(mMutexFeatures);return mvpMapPoints;
}// 获取当前关键帧的具体的某个地图点
MapPoint* KeyFrame::GetMapPoint(const size_t &idx)
{unique_lock<mutex> lock(mMutexFeatures);return mvpMapPoints[idx];
}
另外,还需要关注上述函数的调用时机,即关键帧何时与地图点发生关系:
- 关键帧增加对地图点观测的时机:
Tracking
线程和LocalMapping
线程创建新地图点后,会马上调用函数KeyFrame::AddMapPoint()
添加当前关键帧对该地图点的观测.LocalMapping
线程处理完毕缓冲队列内所有关键帧后会调用LocalMapping::SearchInNeighbors()
融合当前关键帧和共视关键帧间的重复地图点,其中调用函数ORBmatcher::Fuse()
实现融合过程中会调用函数KeyFrame::AddMapPoint()
;LoopClosing
线程闭环矫正函数LoopClosing::CorrectLoop()
将闭环关键帧与其匹配关键帧间的地图进行融合,会调用函数KeyFrame::AddMapPoint()
;
- 关键帧替换和删除对地图点观测的时机:
MapPoint
删除函数MapPoint::SetBadFlag()
或替换函数MapPoint::Replace()
会调用KeyFrame::EraseMapPointMatch()
和KeyFrame::ReplaceMapPointMatch()
删除和替换关键针对地图点的观测;LocalMapping
线程调用进行局部BA
优化的函数Optimizer::LocalBundleAdjustment()
内部调用函数KeyFrame::EraseMapPointMatch()
删除对重投影误差较大的地图点的观测。
1.5 生成树
和生成树相关的函数,主要操作时围绕自己的子节点和父节点,其中子节点有多个,即mspChildrens,父节点只能有一个,即mpParent。
// 添加子关键帧(即和子关键帧具有最大共视关系的关键帧就是当前关键帧)
void KeyFrame::AddChild(KeyFrame *pKF)
{unique_lock<mutex> lockCon(mMutexConnections);mspChildrens.insert(pKF);
}// 删除某个子关键帧
void KeyFrame::EraseChild(KeyFrame *pKF)
{unique_lock<mutex> lockCon(mMutexConnections);mspChildrens.erase(pKF);
}// 改变当前关键帧的父关键帧
void KeyFrame::ChangeParent(KeyFrame *pKF)
{unique_lock<mutex> lockCon(mMutexConnections);// 添加双向连接关系mpParent = pKF;pKF->AddChild(this);
}//获取当前关键帧的子关键帧
set<KeyFrame*> KeyFrame::GetChilds()
{unique_lock<mutex> lockCon(mMutexConnections);return mspChildrens;
}//获取当前关键帧的父关键帧
KeyFrame* KeyFrame::GetParent()
{unique_lock<mutex> lockCon(mMutexConnections);return mpParent;
}// 判断某个关键帧是否是当前关键帧的子关键帧
bool KeyFrame::hasChild(KeyFrame *pKF)
{unique_lock<mutex> lockCon(mMutexConnections);return mspChildrens.count(pKF);
}
2 KeyFrame用途
如上所述,在ORB-SLAM2
中,关键帧Keyframes
是系统中的重要组成部分,具有以下作用和用途:
-
地图构建:关键帧用于构建场景的三维地图。它们通过提取关键点、计算特征描述子和建立特征点之间的匹配关系来捕获场景的结构和几何信息。关键帧之间的匹配信息被用于三角化重建场景中的特征点,并估计相机的运动和场景的几何结构。
-
定位:关键帧用于相机的实时定位。当新的图像帧进入系统时,
ORB-SLAM2
会与之前的关键帧进行匹配,通过计算特征点之间的匹配关系和求解相机位姿来估计相机的当前位置。关键帧中保存的地图信息能够提供更好的定位精度和鲁棒性。 -
回环检测:关键帧用于检测环回(
Loop Closure
)事件,即相机在场景中经过一段时间后再次经过之前的位置。ORB-SLAM2
使用关键帧之间的特征匹配来检测回环,并利用回环信息进行地图的优化和校正,提高系统的一致性和鲁棒性。 -
关键帧选择:
ORB-SLAM2
使用一种自适应的关键帧选择策略,根据一些准则选择最具代表性和信息丰富的关键帧进行处理。这可以减少计算复杂性,提高系统的实时性能。
总之,关键帧在ORB-SLAM2
系统中扮演着重要的角色,用于场景重建、定位、回环检测和关键帧选择,使系统能够实时地感知和理解环境,提供稳定和精确的定位和地图重建能力。
Reference:
- https://github.com/raulmur/ORB_SLAM2
- https://github.com/electech6/ORB_SLAM2_detailed_comments/tree/master
- http://webdiis.unizar.es/~raulmur/MurMontielTardosTRO15.pdf
- https://blog.csdn.net/ncepu_Chen/article/details/116784875#t5
- https://zhuanlan.zhihu.com/p/84293190
⭐️👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍🌔
相关文章:
ORB-SLAM2学习笔记10之图像关键帧KeyFrame
文章目录 0 引言1 KeyFrame类1.1 构造函数1.2 成员函数1.3 关键帧之间共视图1.3.1 AddConnection1.3.2 UpdateBestCovisibles1.3.3 UpdateConnections1.3.4 EraseConnection1.3.5 SetBadFlag 1.4 地图点1.5 生成树 2 KeyFrame用途 0 引言 ORB-SLAM2学习笔记7详细了解了System主…...
【ownCloud】添加信任域
在我进行使用mysql:5.6和 owncloud 镜像,构建一个个人网盘后 我的虚拟机更改了ip地址导致出现下列状况 报错:您正在访问来自不信任域名的服务器。 please contact your administrator. if you are an administrator of this instance, configure the &q…...
C++--类型转换
1.什么是类型转换 在传统C语言中,由强制类型转换和隐式类型转换,隐式类型转换,编译器在在编译阶段自动处理,能转换则转换,强制类型转换由用户自己转换。 缺陷: 转换的可视性比较差,所有的转换形…...
在服务器上部署 Nginx 并设置图片服务器
问题:我要指定/home/images专门存放图片!该怎么做,而且我的系统是centos8系统,只有一个root用户,用root用户已经安装了nginx 答案: 既然你使用了 CentOS 8,并且你想使用 /home/images 目录存放…...
使用NXP GUI GUIDER生成的GUI移植到雅特力MCU平台过程详解(ST/GD/国民/极海通用)
好记性不如烂笔头,既然不够聪明,就乖乖的做笔记,温故而知新。 本文档用于本人对知识点的梳理和记录 一、前言 上一篇我们有介绍NXP GUI Guider工具如何制作和调试GUI,GUI神器 NXP GUI GUIDER开发工具入门教程https://blog.csdn.n…...
JVM——配置常用参数,GC调优策略
文章目录 JVM 配置常用参数Java内存区域常见配置参数概览堆参数回收器参数项目中常用配置常用组合 常用 GC 调优策略GC 调优原则GC 调优目的GC 调优策略 JVM 配置常用参数 Java内存区域常见配置参数概览堆参数;回收器参数;项目中常用配置;常…...
使用IDEA把Java程序打包成jar
点击左上角File,选择Project Structure 左侧选中Artifacts,点击右侧的号 选择JAR->From modules with dependencies 选择你要运行的main方法所在的类,选好了点击OK Artifacts添加完成后点击右下角OK 在工具栏中找到Build,选择Build Artifacts 刚才创建好的Artifacts,选择Bui…...
元宇宙和数字孪生的异同探究
元宇宙和数字孪生,作为两个备受瞩目的概念,都在不同领域引起了巨大的关注。虽然它们都涉及数字化世界的构建,但元宇宙和数字孪生在概念、应用和影响方面存在一些异同点。 相似之处: 数字表示: 元宇宙和数字孪生都依赖…...
初识微服务
我们在曾经最常见的就是所谓的单体架构,但是由于网民越来越多,单体架构已经逐渐的被淘汰出去,所以我们在单体架构的基础上提出了微服务,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合࿰…...
数据库锁的分类 各种锁
锁的一个分类 数据库中的锁前言分享链接个人总结全局锁:表级锁行级锁: 数据库中的锁 前言 C支持并发有锁,Linux里面也有锁机制,数据库也有锁,什么互斥锁,表级锁,间隙锁,好多…&…...
MySQL数据库软件
MySQL数据库软件的详细知识介绍: 1. 存储引擎 MySQL支持多种存储引擎,如InnoDB、MyISAM等。不同引擎有各自的特点,InnoDB支持事务、行锁,MyISAM支持全文索引等。 2. 索引结构 MySQL索引主要有B树索引、哈希索引、全文索引等。这些索引通过不同的数据结构加速查找效率。 3. …...
无涯教程-PHP - preg_match_all()函数
preg_match_all() - 语法 int preg_match_all (string pattern, string string, array pattern_array [, int order]); preg_match_all()函数匹配字符串中所有出现的模式。 它将按照您使用可选输入参数order指定的顺序将这些匹配项放置在pattern_array数组中。有两种可能的类…...
Docker 练习2 安装MySQL
一、实验要求 1、使用mysql:5.6和 owncloud 镜像,构建一个个人网盘。 2、安装搭建私有仓库 Harbor 3、编写Dockerfile制作Web应用系统nginx镜像,生成镜像nginx:v1.1,并推送其到私有仓库。具体要求如下: (1)…...
AndroidStudio 编译报错Unable to make field private final
用 AndroidStudio 打开某个工程后,编译报错如下 Execution failed for task :app:processDebugMainManifest. > Unable to make field private final java.lang.String java.io.File.path accessible: module java.base does not "opens java.io" to …...
linux 上安装es
首先 到官网 https://www.elastic.co/cn/downloads/elasticsearch 下载对应的安装包,我这里下载的是 https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.9.1-linux-x86_64.tar.gz 然后讲该压缩包上传到 linux 的/usr/local 目录下执行 tar -z…...
自然语言处理从入门到应用——LangChain:索引(Indexes)-[文本分割器(Text Splitters)]
分类目录:《自然语言处理从入门到应用》总目录 当我们需要处理长文本时,有必要将文本分割成块。虽然这听起来很简单,但这里存在很多潜在的复杂性。理想情况下,我们希望将语义相关的文本块保持在一起,但什么是"语义…...
Chrome如何安装插件(文件夹)
1.下载的插件 说明:插件文件夹 2.打开扩展程序位置 3.点击已加载的扩展程序 说明:找到插件的位置 4.报错 说明:那还要进入文件里面。 5.插件的位置 说明:如果已经安装了插件,那么需要查看插件的位置。chrome输入 …...
MySql 环境搭建
目录 MySql 在 CentOS 7 环境下安装。 说明: 1.卸载不要的环境 2.配置 mysql 官方 yum 源 3.开始安装 4.启动 mysql 5.mysql 登录 6.配置 mysql 7. 设置开机启动 MySql 在 CentOS 7 环境下安装。 说明: 在安装与卸载中,用户切换成 r…...
mysql 表的约束
目录 mysql 表的约束 NULL/NOT NULL DEFAULT comment zerofill PRIMARY KRY 删除主键 添加主键 复合主键 AUTO_INCREMENT UNIQUE KEY FOREIGN KEY mysql 表的约束 约束是 mysql 为了保证数据正确的一种手段,而前面在谈数据类型的时候,数据类…...
认识Redis
1. 前置操作 以下内容基于CentOS 1.1. 安装 yum -y install redis 1.2. 启动 redis-server /etc/redis.conf & 1.3. 打开 redis-cli 1.4. 停止 redis-cli shutdown 1.5. 设置远程连接 修改 /etc/redis/redis.conf 修改 bind 127.0.0.1为 bind 0.0.0.0 1.6. 使用…...
同步、异步无障碍:Python异步装饰器指南
一、引言 Python异步开发已经非常流行了,一些主流的组件像MySQL、Redis、RabbitMQ等都提供了异步的客户端,再处理耗时的时候不会堵塞住主线程,不但可以提高并发能力,也能减少多线程带来的cpu上下文切换以及内存资源消耗。但在业务…...
CodeSite for .NET Crack
CodeSite for .NET Crack CodeSite for.NET与Visual Studio集成,通过实时查看器日志记录系统提供对代码执行的更深入了解,该系统有助于在本地或远程执行代码时快速查找问题。超越传统的断点调试,在应用程序继续运行时记录应用程序的执行&…...
基于IMX6ULLmini的linux裸机开发系列九:时钟控制模块
时钟控制模块 核心 4个层次配置芯片时钟 晶振时钟 PLL与PFD时钟 PLL选择时钟 根时钟/外设时钟 系统时钟来源 RTC时钟源:32.768KHz 系统时钟:24MHz,作为芯片的主晶振使用 PLL和PFD倍频时钟 7路锁相环电路(每个锁相环电路…...
【数据结构与算法】1. 绪论
1. 绪论 1.1 数据结构 1.1.1 数据结构的基本概念 1.1.2 数据结构的三要素 数据结构三要素: 逻辑结构 划分方法一: 线性结构:线性表、栈、队列、串非线性结构:树、图 划分方法二: 集合结构线性结构树形结构网状&…...
2023年京东儿童智能手表行业数据分析(京东销售数据分析)
儿童消费市场向来火爆,儿童智能手表作为能够实现定位导航,信息通讯,SOS求救,远程监听,智能防丢等多功能的智能可穿戴设备,能够通过较为精准的定位功能和安全防护能力保障儿童的安全,因而广受消费…...
数据结构(6)
2-3查找树 2-结点:含有一个键(及其对应的值)和两条链,左链接指向2-3树中的键都小于该结点,右链接指向的2-3树中的键都大于该结点。 3-结点:含有两个键(及其对应的值)和三条链,左链接指向的2-3树中的键都小于该结点&a…...
C++学习|CUDA安装和配置
CUDA安装和配置 Windows下安装CUDAVS项目配置CUDA Windows下安装CUDA 第一步:先看自己NIVIDIA显卡适合什么版本的CUDA。打开电脑的“NIVIDIA控制面板”->系统信息->组件。会看到我的显卡驱动最高支持的CUDA版本是11.4.56。 第二步:去CUDA官网&…...
3.若依前后端分离版开发用户自定义配置表格功能
一、背景 在项目上线测试的时候,关于同一个界面的表格,不同的用户会出现不同的字段排列需求,有些用户希望把A字段排在最前面,有些用户则希望A字段不显示。针对这种情况,开发一个表格自定义配置的功能,每个…...
【操作系统】24王道考研笔记——第三章 内存管理
第三章 内存管理 一、内存管理概念 1.基本概念 2.覆盖与交换 覆盖技术: 交换技术: 总结: 3.连续分配管理方式 单一连续分配 固定分区分配 动态分区分配 动态分区分配算法: 总结: 4.基本分页存储管理 定义…...
Spring缓存深入解析:@Cacheable的使用详解
摘要:在本文中,我们将深入研究Spring框架中的Cacheable注解。我们会通过详细的Java示例,探讨如何使用这个功能强大的注解来提升应用程序性能。 一、什么是缓存? 在计算机科学中,缓存是一种存储技术,用于保…...
安庆做网站企业/太原建站seo
axios的封装 // 使用axios用于对数据的请求 import axios from axios // 创建axios实例 const instance axios.create({baseURL: baseURL version,timeout: 5000 })// 创建请求的拦截器 instance.interceptors.request.use(config > {config.headers[Authorization] loca…...
社交app开发成本预算表/优化网站的方法有哪些
实现了 出/入 分别计数; - 检测类别:行人、自行车、小汽车、摩托车、公交车、卡车。 -支持yolov5s.pt yolov5x.pt yolov5m.pt yolov5l.pt模型...
深圳专业定制建站公司/线上线下一体化营销
--查询指定供应商指定的一段时间内出票的张数 如果每查询一个月,修改一次时间太麻烦,写个循环的! declare date1 date declare date2 date declare startdate date declare enddate date declare countsum int declare count int set start…...
虚拟服务器建网站/网络推广平台哪家公司最好
MySQL WHILE和LOOP和REPEAT循环的用法区别 MySQL三种循环的区别 MySQL循环使用方法一、MySQL循环概述MySQL中有三种循环,分别是 WHILE , REPEAT , LOOP (据说还有 goto),不可单独使用,主要用于 存储过程 PROCEDURE 和 函数 FUNCTION 中。二、…...
内蒙古建设工程交易中心网站/手机百度浏览器
TimeLimitingCollector 包装其他的收集器,当查询超过指定时间时通过抛出TimeExceededException异常来中止搜索。通过一个被包装的收集器,一个时钟定时器和超时时间来构造TimeLimitingCollector对象。setBaseline(long clockTime):在包…...
代理登录网站/uc信息流广告投放
利用matlab将位图转为SVG矢量图0 前言1 算法思路1.1 读取图片1.2 中值滤波1.3 中值滤波1.4 去除孤立的像素1.5 提取单独的颜色1.6 找出二值图像中所有的连接体1.7 将提取出来的每个多边形膨胀1.8 输出为SVG格式2 最终结果展示3 完整的Matlab代码惯例声明:本人没有相…...