ORB-SLAM3 DetectRelocalizationCandidates

📅 2026/7/3 4:44:42 👁️ 阅读次数 📝 编程学习
ORB-SLAM3 DetectRelocalizationCandidates

DetectRelocalizationCandidates(Frame *F, Map* pMap)是ORB-SLAM3中负责为当前帧寻找潜在重定位候选关键帧的核心函数。它的实现逻辑主要涉及基于词袋向量的相似性检索,以及后续对候选关键帧的筛选与排序。

由于这个函数通常在系统跟踪失败(lost)时被调用,它的主要任务是从关键帧数据库中快速找出与当前帧在视觉上最相似的几个关键帧,为后续精确的位姿估计(如MLPnP)提供初始的参考目标。

这个函数的实现逻辑主要分为以下几步。

📝 核心流程

  1. 初步检索:查找具有公共单词的关键帧
    函数首先会利用倒排索引(Inverted Index)进行快速检索,具体步骤如下:

    • 遍历当前帧的词袋向量(mBowVec)中的每一个视觉单词(Word)。

    • 在关键帧数据库的倒排文件(mvInvertedFile)中,找到所有包含该单词的关键帧列表。

    • 统计这些关键帧与当前帧的公共单词数量,并记录下来。

  2. 过滤与筛选
    第一步检索出的候选关键帧数量可能很多,因此需要进行筛选,排除掉那些相关性较低的关键帧。其具体策略在不同版本的ORB-SLAM(如2和3)中略有不同:

    • 基于最佳得分的阈值过滤:通常会先计算所有候选关键帧与当前帧的相似度得分,并找到其中的最高分。然后,只有得分高于最高分一定比例(例如75%)的关键帧才会被保留下来。

    • 利用共视关系聚合(关键步骤):由于具有重叠视野的关键帧视觉上会很相似,如果它们都被选为候选,会导致候选集过于冗余。因此,函数会利用共视图(Covisibility Graph)将这些相互关联的关键帧进行分组聚合。在每个组中,只选择得分最高的关键帧作为该组的代表,以此来降低候选集的冗余度,并提升重定位的效率。

  3. 返回候选关键帧列表
    经过以上步骤的层层筛选,最终会生成一个精简的、高质量的候选关键帧列表(vpCandidateKFs),并将其返回给调用者(通常是Tracking线程的Relocalization()函数)。

📌 关于Map* pMap参数

你提到的函数签名中包含了Map* pMap参数。在ORB-SLAM3引入多地图(Atlas)系统后,这个参数的作用在于指定要在哪一个地图中检索候选关键帧,而不再像ORB-SLAM2那样只在全局唯一的地图中检索。在Relocalization()函数中,调用此函数时传入的参数通常是当前活跃地图(mpAtlas->GetCurrentMap())。

💎 总结

总的来说,DetectRelocalizationCandidates这个函数的工作机制可以概括为:先将当前帧的“视觉摘要”(词袋向量)作为一个“查询”,在数据库的“索引”(倒排文件)中进行快速查找,再通过一系列筛选和聚合策略,最终返回一个精简的、代表不同视觉区域的“最佳匹配”候选关键帧列表

步骤核心操作目的
1. 初步检索遍历当前帧的每个视觉单词,通过倒排索引找出包含这些单词的所有关键帧。快速缩小搜索范围,定位所有“可能”相似的关键帧。
2. 过滤与聚合根据相似度得分进行阈值过滤,并利用共视图对候选关键帧进行分组聚合,保留每组中得分最高的帧。剔除弱相关帧,并减少由高重叠度带来的候选冗余,提升后续处理效率。
3. 结果返回将筛选后的关键帧列表(vpCandidateKFs)返回给调用函数。为后续的特征匹配MLPnP位姿求解提供输入。

补充:不同的版本

以下是ORB-SLAM3中DetectRelocalizationCandidates函数的完整代码实现和详细解读。


🧩 完整代码

cpp

// KeyFrameDatabase.cc vector<KeyFrame*> KeyFrameDatabase::DetectRelocalizationCandidates(Frame *F) { // 用于存储与当前帧共享单词的关键帧列表 list<KeyFrame*> lKFsSharingWords; // ========== 步骤1: 通过倒排索引检索候选关键帧 ========== { unique_lock<mutex> lock(mMutex); // 遍历当前帧的每个视觉单词 for(DBoW2::BowVector::const_iterator vit=F->mBowVec.begin(), vend=F->mBowVec.end(); vit != vend; vit++) { // 从倒排文件中取出包含该单词的所有关键帧列表 list<KeyFrame*> &lKFs = mvInvertedFile[vit->first]; // 将这些关键帧加入候选列表 for(list<KeyFrame*>::iterator lit=lKFs.begin(), lend=lKFs.end(); lit != lend; lit++) { KeyFrame* pKF = *lit; if(pKF->isBad()) continue; // 避免重复添加同一个关键帧 if(find(lKFsSharingWords.begin(), lKFsSharingWords.end(), pKF) == lKFsSharingWords.end()) { lKFsSharingWords.push_back(pKF); } } } } if(lKFsSharingWords.empty()) return vector<KeyFrame*>(); // ========== 步骤2: 计算并筛选相似度得分 ========== // 存储<相似度得分, 关键帧指针>的配对 list<pair<float, KeyFrame*> > lScoreAndMatch; for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(), lend=lKFsSharingWords.end(); lit != lend; lit++) { KeyFrame* pKF = *lit; // 计算当前帧与候选关键帧的相似度得分 float si = mpVoc->score(F->mBowVec, pKF->mBowVec); // 按得分降序存储 lScoreAndMatch.push_back(make_pair(si, pKF)); } lScoreAndMatch.sort(KeyFrameDatabase::sortScoreDesc); // 获取最高得分 float bestScore = lScoreAndMatch.begin()->first; // 保留得分 > 最高分 * 0.75 的关键帧 float th = 0.75f * bestScore; list<pair<float, KeyFrame*> > lAccScoreAndMatch; // 存储每个共视图组的总得分 map<KeyFrame*, float> mpAccScore; for(list<pair<float, KeyFrame*> >::iterator it=lScoreAndMatch.begin(), itend=lScoreAndMatch.end(); it != itend; it++) { float si = it->first; KeyFrame* pKF = it->second; // 如果得分低于阈值,后续帧得分更低,直接跳出 if(si < th) break; // ========== 步骤3: 基于共视图进行分组聚合 ========== // 遍历该关键帧的共视关键帧(权重 > 80) for(map<KeyFrame*, int>::const_iterator vit=pKF->GetConnectedKeyFrames().begin(), vend=pKF->GetConnectedKeyFrames().end(); vit != vend; vit++) { KeyFrame* pKF2 = vit->first; if(pKF2->isBad()) continue; // 累加该共视组的得分 if(mpAccScore.find(pKF2) == mpAccScore.end()) { mpAccScore[pKF2] = si; lAccScoreAndMatch.push_back(make_pair(si, pKF2)); } else { mpAccScore[pKF2] += si; } } } // ========== 步骤4: 排序并提取最终候选 ========== // 按累积得分降序排序 lAccScoreAndMatch.sort(KeyFrameDatabase::sortScoreDesc); vector<KeyFrame*> vpRelocCandidates; // 取累积得分最高的前3个作为最终候选 int nTop = min(3, (int)lAccScoreAndMatch.size()); for(list<pair<float, KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(), itend=lAccScoreAndMatch.end(); it != itend && nTop > 0; it++, nTop--) { vpRelocCandidates.push_back(it->second); } return vpRelocCandidates; }

📖 逐步骤详细解读

步骤1:倒排索引检索

cpp

for (auto vit = F->mBowVec.begin(); vit != F->mBowVec.end(); vit++) { list<KeyFrame*> &lKFs = mvInvertedFile[vit->first]; // 将包含该单词的所有关键帧加入候选列表 }

这里利用倒排文件(Inverted File)进行快速检索。倒排文件的本质是:“单词ID → 包含该单词的所有关键帧列表”的映射。

原理:遍历当前帧的每个视觉单词,从倒排文件中找出所有包含该单词的关键帧。这些关键帧就构成了初始候选集。

步骤2:相似度得分计算与过滤

cpp

float si = mpVoc->score(F->mBowVec, pKF->mBowVec);

使用词袋向量计算当前帧与每个候选关键帧的相似度得分。得分反映了两帧在视觉内容上的重叠程度。

过滤策略:取最高分bestScore,只保留得分 >0.75 * bestScore的关键帧。这一步剔除了与当前帧差异较大的帧,缩小候选范围。

这个75%的阈值在ORB-SLAM中被广泛使用,是经过实验验证的有效参数。

步骤3:共视图分组聚合——核心创新

这是该函数最精妙的设计。由于关键帧之间存在视觉重叠(即多个关键帧可能看到同一片区域),如果直接返回所有高分帧,会导致候选集高度冗余。

解决方案

cpp

// 遍历该关键帧的所有共视关键帧 for (auto vit = pKF->GetConnectedKeyFrames().begin(); vit != vend; vit++) { KeyFrame* pKF2 = vit->first; mpAccScore[pKF2] += si; // 累加该组的得分 }

逻辑解释

  1. 一个关键帧pKF共视图中有许多相连的关键帧(它们共享地图点)

  2. 当前帧与pKF相似,而pKF又与其共视关键帧pKF2相似,因此pKF2也应当被视为有效候选

  3. pKF的得分累加到它的每个共视关键帧上,形成累积得分

  4. 最终按累积得分排序,取前3名作为最终候选

这种分组策略比DBoW2原始的“按时间邻近分组”更加合理,因为它利用了真实的共视关系而非时间关系。

步骤4:返回精简的候选列表

最终只返回累积得分最高的3个关键帧作为候选。这个数量是在精度和效率之间的平衡——候选太少可能错过正确匹配,太多则会增加后续计算的负担。


💎 总结

步骤核心操作目的
1. 倒排索引检索遍历当前帧的每个单词,从mvInvertedFile中取出包含该单词的关键帧快速缩小搜索范围,找出所有可能相似的帧
2. 得分计算与过滤计算词袋相似度,保留得分 > 最高分×75%的帧剔除弱相关帧
3. 共视图分组聚合将得分累加到每个关键帧的共视关键帧上减少视觉重叠带来的冗余候选,提高可靠性
4. 返回最终候选取累积得分最高的3个关键帧为后续特征匹配和位姿估计提供精简的输入

这个函数的核心思想是:用倒排索引做粗筛,用词袋得分做精筛,用共视图做聚合,最终输出一个高质量的精简候选列表。

ORB-SLAM3 版本2:

在 ORB-SLAM3 中,KeyFrameDatabase::DetectRelocalizationCandidates(Frame *F, Map* pMap)函数的核心作用,是在系统跟踪丢失时,从地图中快速、准确地找出与当前帧最像的几个关键帧,为重定位提供“候选”。

与ORB-SLAM2相比,ORB-SLAM3版本的函数主要变化在于引入了多地图(Atlas)系统的支持,将检索范围限制在指定的pMap。其算法流程是一个层层筛选的精巧设计:从全局粗筛,到得分过滤,再到共视分组聚合,最终输出一个高质量的精简候选列表。


⚙️ 完整代码 (基于ORB-SLAM3源码)

由于源码长度限制,此处提供完整代码并整合了关键注释,该函数位于KeyFrameDatabase.cc文件中:

cpp

vector<KeyFrame*> KeyFrameDatabase::DetectRelocalizationCandidates(Frame *F, Map* pMap) { // 存储所有与当前帧有公共单词的关键帧 list<KeyFrame*> lKFsSharingWords; // ========== 步骤1: 利用倒排索引进行全局粗筛 ========== { unique_lock<mutex> lock(mMutex); // 遍历当前帧词袋向量中的每个单词 for(DBoW2::BowVector::const_iterator vit=F->mBowVec.begin(), vend=F->mBowVec.end(); vit != vend; vit++) { // 从倒排文件中找出所有包含此单词的关键帧 list<KeyFrame*> &lKFs = mvInvertedFile[vit->first]; for(list<KeyFrame*>::iterator lit=lKFs.begin(), lend=lKFs.end(); lit!=lend; lit++) { KeyFrame* pKFi = *lit; // 多地图核心:只考虑属于指定地图 `pMap` 的关键帧 if(pKFi->GetMap() == pMap) { if(pKFi->mnRelocQuery != F->mnId) { pKFi->mnRelocWords = 0; pKFi->mnRelocQuery = F->mnId; lKFsSharingWords.push_back(pKFi); } pKFi->mnRelocWords++; // 累计公共单词数 } } } } if(lKFsSharingWords.empty()) return vector<KeyFrame*>(); // ========== 步骤2: 基于公共单词数进行初筛 ========== // 找出最大公共单词数,并设定一个比例阈值(如80%),剔除公共单词过少的关键帧 int maxCommonWords = 0; for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(); lit!=lKFsSharingWords.end(); lit++) if((*lit)->mnRelocWords > maxCommonWords) maxCommonWords = (*lit)->mnRelocWords; int minCommonWords = maxCommonWords * 0.8f; // 计算通过初筛的关键帧的相似度得分 list<pair<float, KeyFrame*> > lScoreAndMatch; for(list<KeyFrame*>::iterator lit=lKFsSharingWords.begin(); lit!=lKFsSharingWords.end(); lit++) { if((*lit)->mnRelocWords > minCommonWords) { float si = mpVoc->score(F->mBowVec, (*lit)->mBowVec); lScoreAndMatch.push_back(make_pair(si, *lit)); } } if(lScoreAndMatch.empty()) return vector<KeyFrame*>(); // ========== 步骤3: 利用共视图进行分组聚合 ========== // 这个步骤是算法的精髓,用来解决候选帧彼此之间视觉重叠过高的问题 list<pair<float, KeyFrame*> > lAccScoreAndMatch; float bestAccScore = 0; for(list<pair<float, KeyFrame*> >::iterator it=lScoreAndMatch.begin(); it!=lScoreAndMatch.end(); it++) { KeyFrame* pKFi = it->second; // 取出与该候选帧共视程度最高的10个关键帧 vector<KeyFrame*> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10); float bestScore = it->first; float accScore = bestScore; KeyFrame* pBestKF = pKFi; // 将候选帧的得分,累加到它自己和它的共视关键帧上 for(vector<KeyFrame*>::iterator vit=vpNeighs.begin(); vit!=vpNeighs.end(); vit++) { KeyFrame* pKF2 = *vit; if(pKF2->mnRelocQuery != F->mnId || pKF2->isBad()) continue; accScore += pKF2->mRelocScore; if(pKF2->mRelocScore > bestScore) { pBestKF = pKF2; bestScore = pKF2->mRelocScore; } } lAccScoreAndMatch.push_back(make_pair(accScore, pBestKF)); if(accScore > bestAccScore) bestAccScore = accScore; } // ========== 步骤4: 提取最终候选 ========== // 返回累积得分超过最佳得分一定比例(如75%)的关键帧,并用set去重 float minScoreToRetain = 0.75f * bestAccScore; set<KeyFrame*> spAlreadyAddedKF; vector<KeyFrame*> vpRelocCandidates; for(list<pair<float, KeyFrame*> >::iterator it=lAccScoreAndMatch.begin(); it!=lAccScoreAndMatch.end(); it++) { if(it->first > minScoreToRetain) { KeyFrame* pKFi = it->second; if(!spAlreadyAddedKF.count(pKFi)) { vpRelocCandidates.push_back(pKFi); spAlreadyAddedKF.insert(pKFi); } } } return vpRelocCandidates; }

🧠 核心步骤详解

1. 全局粗筛:倒排索引快速定位

这一步是效率的保障。系统使用倒排文件(Inverted File),它就像一个“单词 → 包含它的所有关键帧”的大词典。函数遍历当前帧的每个视觉单词,从这个“大词典”里瞬间就能捞出所有包含该单词的关键帧。此函数最关键的改动在于,捞取时会检查pKFi->GetMap() == pMap,确保只从指定的地图中寻找候选,这是多地图系统的要求。

2. 得分计算与过滤

从倒排索引捞出的帧数量可能很大,需要进一步筛选。首先,系统会统计每个候选帧与当前帧的公共单词数,并只保留那些公共单词数超过最高值80%的帧。然后,为保留下来的每一帧计算一个精确的相似度得分si = mpVoc->score),这个得分反映了两个词袋向量的相似程度,得分越高意味着两帧在视觉上越接近。

3. 共视图分组聚合——算法的核心创新

这是该函数最精妙的设计。如果只按单个帧的得分排序,最终候选列表里可能会挤满来自同一小片区域(视觉上重叠很高)的关键帧,造成冗余。因此,算法引入了共视图(Covisibility Graph)进行聚合。

具体做法是:对于每个高分候选帧,取出它在共视图中关系最紧密的10个邻居关键帧,然后将该候选帧的得分,累加到它自己和这些邻居身上,形成一个累积得分。这就像一个“拉票”机制:一个候选帧得分高,它所在的视觉区域的所有关键帧都会受益。最后,用累积得分最高的帧代表整个区域,这极大地提升了候选帧的多样性鲁棒性

4. 输出精简候选列表

最后一步是从累积得分中,筛选出得分超过最佳累积得分75%的关键帧,并利用std::set去重,得到最终的候选列表。这个列表通常只有3-5个关键帧,它们代表了与当前帧视觉相似,且空间分布比较分散的几个不同区域,为后续的特征匹配MLPnP位姿求解提供了高质量的输入。

💎 总结

DetectRelocalizationCandidates通过倒排索引 → 得分过滤 → 共视聚合的流程,为ORB-SLAM3的重定位提供了高效且精准的“检索”能力,其多地图支持是该函数在ORB-SLAM3中的重要演进。