0%

ORBSLAM2源码小记(2)--普通帧Frame与关键帧KeyFrame

ORBSLAM2的普通帧Frame与关键帧KeyFrame

主要是记录下相对容易忘的内容,不会写的很细

普通帧Frame

普通帧主要是记录特征点,同时根据相机内参进行去畸变,并计算出校正后的边界。每个普通帧会有一个参考关键帧,为共视程度最好的关键帧,但用于初始化的两个普通帧的参考关键帧为自身,因为初始化时还不存在其他关键帧。

在Tracking过程中,会存储currentFrame和lastFrame两个普通帧,当前普通帧会根据恒速模型、关键帧跟踪或重定位来确定自身的位姿,因此普通帧中也存在存储自身位姿的数据结构。

特征提取与网格划分

特征点数目

会使用ORB特征提取器来提取特征,由于需要保证尺度不变性,所以需要构建图像金字塔,但是图像金字塔层数越高,对应的图像分辨率越低,因此层级越高的图像的特征点数目也会越少。在ORBSLAM2中,每帧图像提取的特征点数目是存在人为规定的上界的,记为N,同时金字塔层数固定为8层,每层分配的特征点数目是按每层面积的开方来等比例分配的,即第i层分配的特征点数目为

这里的s是金字塔放缩因子,默认为

网格分配

这里的网格分配与ORB特征提取的四叉树不是同一个东西。四叉树的目的时非极大抑制,是在特征提取阶段进行的;网格分配的目的是加速匹配,是在特征提取完构造帧的时候进行的。

在构造每一帧时,会把像素坐标系分成若干个网格,然后计算每个特征点的坐标并将其分配到网格里,这样做目的是在匹配特征点时加速,也就是先按照网格来粗略查找,然后按照像素点来精细查找,例如在扩大匹配的过程中,都会使用下面这个函数来搜索一定范围内的特征点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* @brief 找到在 以x,y为中心,半径为r的圆形内且金字塔层级在[minLevel, maxLevel]的特征点
*
* @param[in] x 特征点坐标x
* @param[in] y 特征点坐标y
* @param[in] r 搜索半径
* @param[in] minLevel 最小金字塔层级
* @param[in] maxLevel 最大金字塔层级
* @return vector<size_t> 返回搜索到的候选匹配点id
*/
vector<size_t> Frame::GetFeaturesInArea(const float &x, const float &y, const float &r, const int minLevel, const int maxLevel) const
{
...

// 查找半径为r的圆左侧边界所在网格列坐标
// (mnMaxX-mnMinX)/FRAME_GRID_COLS:表示列方向每个网格可以平均分得几个像素(肯定大于1)
// mfGridElementWidthInv=FRAME_GRID_COLS/(mnMaxX-mnMinX) 是上面倒数,表示每个像素可以均分几个网格列(肯定小于1)
// (x-mnMinX-r),可以看做是从图像的左边界mnMinX到半径r的圆的左边界区域占的像素列数
// 两者相乘,就是求出那个半径为r的圆的左侧边界在哪个网格列中
// 保证nMinCellX 结果大于等于0
const int nMinCellX = max(0, (int)floor((x - mnMinX - r) * mfGridElementWidthInv));

// 如果最终求得的圆的左边界所在的网格列超过了设定了上限,那么就说明计算出错,找不到符合要求的特征点,返回空vector
if (nMinCellX >= FRAME_GRID_COLS)
return vIndices;

// 计算圆所在的右边界网格列索引
const int nMaxCellX = min((int)FRAME_GRID_COLS - 1, (int)ceil((x - mnMinX + r) * mfGridElementWidthInv));
// 如果计算出的圆右边界所在的网格不合法,说明该特征点不好,直接返回空vector
if (nMaxCellX < 0)
return vIndices;

// 计算圆所在的下边界网格列索引
const int nMinCellY = max(0, (int)floor((y - mnMinY - r) * mfGridElementHeightInv));
if (nMinCellY >= FRAME_GRID_ROWS)
return vIndices;

// 计算圆所在的上边界网格列索引
const int nMaxCellY = min((int)FRAME_GRID_ROWS - 1, (int)ceil((y - mnMinY + r) * mfGridElementHeightInv));
if (nMaxCellY < 0)
return vIndices;

// 检查需要搜索的图像金字塔层数范围是否符合要求
const bool bCheckLevels = (minLevel > 0) || (maxLevel >= 0);

// 遍历圆形区域内的所有网格,寻找满足条件的候选特征点,并将其index放到输出里
for (int ix = nMinCellX; ix <= nMaxCellX; ix++)
{
for (int iy = nMinCellY; iy <= nMaxCellY; iy++)
{
// 获取这个网格内的所有特征点在 Frame::mvKeysUn 中的索引
const vector<size_t> vCell = mGrid[ix][iy];
// 如果这个网格中没有特征点,那么跳过这个网格继续下一个
if (vCell.empty())
continue;

// 如果这个网格中有特征点,那么遍历这个图像网格中所有的特征点
for (size_t j = 0, jend = vCell.size(); j < jend; j++)
{
// 根据索引先读取这个特征点
const cv::KeyPoint &kpUn = mvKeysUn[vCell[j]];
// 保证给定的搜索金字塔层级范围合法
if (bCheckLevels)
{
// cv::KeyPoint::octave中表示的是从金字塔的哪一层提取的数据
// 保证特征点是在金字塔层级minLevel和maxLevel之间,不是的话跳过
if (kpUn.octave < minLevel)
continue;
if (maxLevel >= 0) //? 为何特意又强调?感觉多此一举
if (kpUn.octave > maxLevel)
continue;
}

// 通过检查,计算候选特征点到圆中心的距离,查看是否是在这个圆形区域之内
const float distx = kpUn.pt.x - x;
const float disty = kpUn.pt.y - y;

// 如果x方向和y方向的距离都在指定的半径之内,存储其index为候选特征点
if (distx * distx + disty * disty < r * r)
vIndices.push_back(vCell[j]);
}
}
}
return vIndices;
}

判断地图点是否在视野内

bool Frame::isInFrustum(MapPoint *pMP, float viewingCosLimit)这个函数在前文中的地图点也提到过,用于判断一个地图点是否在当前帧的视野内。具体而言,当一个地图点满足下面所有条件时,就认为该地图点在当前帧的视野内(也就是理论上可以被检测到)

  • 相机坐标系约束:地图点投影到当前帧后深度信息为正,也就是地图点在相机前方
  • 像素坐标系约束:地图点投影后的像素点位于帧畸变矫正后的有效像素区域内
  • 特征提取约束:地图点与当前帧光心的距离在有效区域内,不然无法通过金字塔提取出来,有效距离的含义在地图点中也提到过了
  • 视角约束:地图点平均方向和地图点与当前帧光心的夹角小于一定阈值,即地图点中所描述的平均观测方向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* @brief 判断地图点是否在视野中
* @param[in] pMP 当前地图点
* @param[in] viewingCosLimit 当前相机指向地图点向量和地图点的平均观测方向夹角余弦阈值
* @return true 地图点合格,且在视野内
* @return false 地图点不合格,抛弃
*/
bool Frame::isInFrustum(MapPoint *pMP, float viewingCosLimit)
{
...

// 根据当前帧(粗糙)位姿转化到当前相机坐标系下的三维点Pc
const cv::Mat Pc = mRcw * P + mtcw;
const float &PcX = Pc.at<float>(0);
const float &PcY = Pc.at<float>(1);
const float &PcZ = Pc.at<float>(2);

// 将这个地图点变换到当前帧的相机坐标系下,如果深度值为正才能继续下一步。
if (PcZ < 0.0f)
return false;

// 将地图点投影到当前帧的像素坐标,如果在图像有效范围内才能继续下一步。
const float invz = 1.0f / PcZ;
const float u = fx * PcX * invz + cx;
const float v = fy * PcY * invz + cy;
if (u < mnMinX || u > mnMaxX)
return false;
if (v < mnMinY || v > mnMaxY)
return false;

// 计算地图点到相机中心的距离,如果在有效距离范围内才能继续下一步。
// 可靠距离范围:[0.8f*mfMinDistance, 1.2f*mfMaxDistance]
const float maxDistance = pMP->GetMaxDistanceInvariance();
const float minDistance = pMP->GetMinDistanceInvariance();
// 得到当前地图点距离当前帧相机光心的距离
const cv::Mat PO = P - mOw;
const float dist = cv::norm(PO);
if (dist < minDistance || dist > maxDistance)
return false;

// 计算当前相机指向地图点向量和地图点的平均观测方向夹角,小于阈值才能进入下一步。
cv::Mat Pn = pMP->GetNormal();
// 计算当前相机指向地图点向量和地图点的平均观测方向夹角的余弦值,注意平均观测方向为单位向量
const float viewCos = PO.dot(Pn) / dist;
if (viewCos < viewingCosLimit)
return false;

...
return true;
}

对于双目与RGBD

对于双目与RGBD而言,会额外存储以下信息(单目下面两个数据结构都是-1):

  • mvuRight:左目特征点对应右目图像的横坐标(对极矫正后纵坐标是一样的)
  • mvDepth:左目特征点对应的深度

如果是双目相机,由于在对极约束后可以直接得到mvuRight,因此需要根据视差来计算mvDepth;相反,对于RGBD相机而言,mvDepth是直接获取的,所以需要根据基线来反向计算出mvuRight

关键帧KeyFrame

在tracking线程中决定是否添加关键帧,关键帧具备普通帧的数据结构比如位姿、特征点、网格划分等等,但同样维护了一些用于优化的数据。

共视图与生成树

当两个关键帧能同时观测到至少15个共同的地图点时,就认为两者具有共视关系,共视权重为共同地图点数量。每个关键帧会按照权重降序存储共视帧,并单独记录共视权重最高的共视关键帧作为父关键帧。共视图会用于LocalMapping中扩大地图点以及LoopClosing中闭环检测和重定位,除此之外Tracking中也会利用共视关系来优化普通帧的位姿。

记录父关键帧的目的是维护生成树,每个关键帧只会存在一个父节点,但是可能会有多个子节点。生成树本身不会参与任何优化,其作用是在LoopClosing中创建本质图进行优化。(本质图仅用于闭环检测,本质图不止包含生成树,还包含闭环关系和一些较好的共视关系)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* 更新关键帧之间的连接图
*
* 1. 首先获得该关键帧的所有MapPoint点,统计观测到这些3d点的每个关键帧与其它所有关键帧之间的共视程度
* 对每一个找到的关键帧,建立一条边,边的权重是该关键帧与当前关键帧公共3d点的个数。
* 2. 并且该权重必须大于一个阈值,如果没有超过该阈值的权重,那么就只保留权重最大的边(与其它关键帧的共视程度比较高)
* 3. 对这些连接按照权重从大到小进行排序,以方便将来的处理
* 更新完covisibility图之后,如果没有初始化过,则初始化为连接权重最大的边(与其它关键帧共视程度最高的那个关键帧),类似于最大生成树
*/
void KeyFrame::UpdateConnections()
{
...

// 通过地图点被关键帧观测来间接统计关键帧之间的共视程度
// 统计每一个地图点都有多少关键帧与当前关键帧存在共视关系,统计结果放在KFcounter
for(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]++;
}
}

...
}

关键帧数据库KeyFrameDatabase

每个关键帧中还会存储一个关键帧数据库KeyFrameDatabase,记为KFDB,其中KFDB全局只会存在一个实例,它使用词袋作为特征,为每个关键帧构建特征集。其作用是通过词袋匹配在Tracking中重定位时获取重定位候选帧以及在LoopClosing中获取闭环候选帧。

KFDB里有两个比较关键的数据结构:

  • const ORBVocabulary *mpVoc:预先训练好的词典(本身也是个词袋),用于计算两个关键帧之间的相似程度
  • std::vector<list<KeyFrame *>> mvInvertedFile:倒排索引,mvInvertedFile[i]表示包含了第i个word id的所有关键帧,用于粗略匹配候选关键帧

计算重定位候选关键帧和闭环候选关键帧时的步骤是极为类似的:

重定位候选关键帧(输入普通帧Frame)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/*
* @brief 在重定位中找到与该帧相似的候选关键帧组
* Step 1. 找出和当前帧具有公共单词的所有关键帧
* Step 2. 只和具有共同单词较多的关键帧进行相似度计算
* Step 3. 将与关键帧相连(权值最高)的前十个关键帧归为一组,计算累计得分
* Step 4. 只返回累计得分较高的组中分数最高的关键帧
* @param F 需要重定位的帧
* @return 相似的候选关键帧数组
*/
vector<KeyFrame *> KeyFrameDatabase::DetectRelocalizationCandidates(Frame *F)
{
list<KeyFrame *> lKFsSharingWords;

// 找出和当前帧具有公共单词(word)的所有关键帧
{
unique_lock<mutex> lock(mMutex);

// mBowVec 内部实际存储的是std::map<WordId, WordValue>
// WordId 和 WordValue 表示Word在叶子中的id 和权重
for (DBoW2::BowVector::const_iterator vit = F->mBowVec.begin(), vend = F->mBowVec.end(); vit != vend; vit++)
{
// 根据倒排索引,提取所有包含该wordid的所有KeyFrame
list<KeyFrame *> &lKFs = mvInvertedFile[vit->first];

for (list<KeyFrame *>::iterator lit = lKFs.begin(), lend = lKFs.end(); lit != lend; lit++)
{
KeyFrame *pKFi = *lit;
// pKFi->mnRelocQuery起标记作用,是为了防止重复选取
if (pKFi->mnRelocQuery != F->mnId)
{
// pKFi还没有标记为F的重定位候选帧
pKFi->mnRelocWords = 0;
pKFi->mnRelocQuery = F->mnId;
lKFsSharingWords.push_back(pKFi);
}
pKFi->mnRelocWords++;
}
}
}

// 如果和当前帧具有公共单词的关键帧数目为0,无法进行重定位,返回空
if (lKFsSharingWords.empty())
return vector<KeyFrame *>();

// 统计上述关键帧中与当前帧F具有共同单词最多的单词数maxCommonWords,用来设定阈值1
int maxCommonWords = 0;
for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
{
if ((*lit)->mnRelocWords > maxCommonWords)
maxCommonWords = (*lit)->mnRelocWords;
}

// 阈值1:最小公共单词数为最大公共单词数目的0.8倍
int minCommonWords = maxCommonWords * 0.8f;

list<pair<float, KeyFrame *>> lScoreAndMatch;

int nscores = 0;

// 遍历上述关键帧,挑选出共有单词数大于阈值1的及其和当前帧单词匹配得分存入lScoreAndMatch
for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
{
KeyFrame *pKFi = *lit;

// 当前帧F只和具有共同单词较多(大于minCommonWords)的关键帧进行比较
if (pKFi->mnRelocWords > minCommonWords)
{
nscores++; // 这个变量后面没有用到
// 用mBowVec来计算两者的相似度得分
float si = mpVoc->score(F->mBowVec, pKFi->mBowVec);
pKFi->mRelocScore = si;
lScoreAndMatch.push_back(make_pair(si, pKFi));
}
}

if (lScoreAndMatch.empty())
return vector<KeyFrame *>();

list<pair<float, KeyFrame *>> lAccScoreAndMatch;
float bestAccScore = 0;

// 计算lScoreAndMatch中每个关键帧的共视关键帧组的总得分,得到最高组得分bestAccScore,并以此决定阈值2
// 单单计算当前帧和某一关键帧的相似性是不够的,这里将与关键帧共视程度最高的前十个关键帧归为一组,计算累计得分
for (list<pair<float, KeyFrame *>>::iterator it = lScoreAndMatch.begin(), itend = lScoreAndMatch.end(); it != itend; it++)
{
KeyFrame *pKFi = it->second;
// 取出与关键帧pKFi共视程度最高的前10个关键帧
vector<KeyFrame *> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);

// 该组最高分数
float bestScore = it->first;
// 该组累计得分
float accScore = bestScore;
// 该组最高分数对应的关键帧
KeyFrame *pBestKF = pKFi;
// 遍历共视关键帧,累计得分
for (vector<KeyFrame *>::iterator vit = vpNeighs.begin(), vend = vpNeighs.end(); vit != vend; vit++)
{
KeyFrame *pKF2 = *vit;
if (pKF2->mnRelocQuery != F->mnId)
continue;
// 只有pKF2也在重定位候选帧中,才能贡献分数
accScore += pKF2->mRelocScore;

// 统计得到组里分数最高的KeyFrame
if (pKF2->mRelocScore > bestScore)
{
pBestKF = pKF2;
bestScore = pKF2->mRelocScore;
}
}

lAccScoreAndMatch.push_back(make_pair(accScore, pBestKF));

// 记录所有组中最高的得分
if (accScore > bestAccScore)
bestAccScore = accScore;
}

// 得到所有组中总得分大于阈值2的,组内得分最高的关键帧,作为候选关键帧组
// 阈值2:最高得分的0.75倍
float minScoreToRetain = 0.75f * bestAccScore;
set<KeyFrame *> spAlreadyAddedKF;
vector<KeyFrame *> vpRelocCandidates;
vpRelocCandidates.reserve(lAccScoreAndMatch.size());
for (list<pair<float, KeyFrame *>>::iterator it = lAccScoreAndMatch.begin(), itend = lAccScoreAndMatch.end(); it != itend; it++)
{
const float &si = it->first;
// 只返回累计得分大于阈值2的组中分数最高的关键帧
if (si > minScoreToRetain)
{
KeyFrame *pKFi = it->second;
// 判断该pKFi是否已经添加在队列中了
if (!spAlreadyAddedKF.count(pKFi))
{
vpRelocCandidates.push_back(pKFi);
spAlreadyAddedKF.insert(pKFi);
}
}
}

return vpRelocCandidates;
}

闭环候选关键帧(输入关键帧KeyFrame)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* @brief 在闭环检测中找到与该关键帧可能闭环的关键帧(注意不和当前帧连接)
* Step 1:找出和当前帧具有公共单词的所有关键帧,不包括与当前帧连接的关键帧
* Step 2:只和具有共同单词较多的(最大数目的80%以上)关键帧进行相似度计算
* Step 3:计算上述候选帧对应的共视关键帧组的总得分,只取最高组得分75%以上的组
* Step 4:得到上述组中分数最高的关键帧作为闭环候选关键帧
* @param[in] pKF 需要闭环检测的关键帧
* @param[in] minScore 候选闭环关键帧帧和当前关键帧的BoW相似度至少要大于minScore
* @return vector<KeyFrame*> 闭环候选关键帧
*/
vector<KeyFrame *> KeyFrameDatabase::DetectLoopCandidates(KeyFrame *pKF, float minScore)
{
// 取出与当前关键帧相连(>15个共视地图点)的所有关键帧,这些相连关键帧都是局部相连,在闭环检测的时候将被剔除
// 相连关键帧定义见 KeyFrame::UpdateConnections()
set<KeyFrame *> spConnectedKeyFrames = pKF->GetConnectedKeyFrames();

// 用于保存可能与当前关键帧形成闭环的候选帧(只要有相同的word,且不属于局部相连(共视)帧)
list<KeyFrame *> lKFsSharingWords;

// 找出和当前帧具有公共单词的所有关键帧,不包括与当前帧连接的关键帧
{
unique_lock<mutex> lock(mMutex);

// words是检测图像是否匹配的枢纽,遍历该pKF的每一个word
// mBowVec 内部实际存储的是std::map<WordId, WordValue>
// WordId 和 WordValue 表示Word在叶子中的id和权重
for (DBoW2::BowVector::const_iterator vit = pKF->mBowVec.begin(), vend = pKF->mBowVec.end(); vit != vend; vit++)
{
// 提取所有包含该word的KeyFrame
list<KeyFrame *> &lKFs = mvInvertedFile[vit->first];
// 然后对这些关键帧展开遍历
for (list<KeyFrame *>::iterator lit = lKFs.begin(), lend = lKFs.end(); lit != lend; lit++)
{
KeyFrame *pKFi = *lit;

if (pKFi->mnLoopQuery != pKF->mnId)
{
// 还没有标记为pKF的闭环候选帧
pKFi->mnLoopWords = 0;
// 和当前关键帧共视的话不作为闭环候选帧
if (!spConnectedKeyFrames.count(pKFi))
{
// 没有共视就标记作为闭环候选关键帧,放到lKFsSharingWords里
pKFi->mnLoopQuery = pKF->mnId;
lKFsSharingWords.push_back(pKFi);
}
}
pKFi->mnLoopWords++; // 记录pKFi与pKF具有相同word的个数
}
}
}

// 如果没有关键帧和这个关键帧具有相同的单词,那么就返回空
if (lKFsSharingWords.empty())
return vector<KeyFrame *>();

list<pair<float, KeyFrame *>> lScoreAndMatch;

// 统计上述所有闭环候选帧中与当前帧具有共同单词最多的单词数,用来决定相对阈值
int maxCommonWords = 0;
for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
{
if ((*lit)->mnLoopWords > maxCommonWords)
maxCommonWords = (*lit)->mnLoopWords;
}

// 确定最小公共单词数为最大公共单词数目的0.8倍
int minCommonWords = maxCommonWords * 0.8f;

int nscores = 0;

// 遍历上述所有闭环候选帧,挑选出共有单词数大于minCommonWords且单词匹配度大于minScore存入lScoreAndMatch
for (list<KeyFrame *>::iterator lit = lKFsSharingWords.begin(), lend = lKFsSharingWords.end(); lit != lend; lit++)
{
KeyFrame *pKFi = *lit;

// pKF只和具有共同单词较多(大于minCommonWords)的关键帧进行比较
if (pKFi->mnLoopWords > minCommonWords)
{
nscores++; // 这个变量后面没有用到

// 用mBowVec来计算两者的相似度得分
float si = mpVoc->score(pKF->mBowVec, pKFi->mBowVec);

pKFi->mLoopScore = si;
if (si >= minScore)
lScoreAndMatch.push_back(make_pair(si, pKFi));
}
}

// 如果没有超过指定相似度阈值的,那么也就直接跳过去
if (lScoreAndMatch.empty())
return vector<KeyFrame *>();

list<pair<float, KeyFrame *>> lAccScoreAndMatch;
float bestAccScore = minScore;

// 计算上述候选帧对应的共视关键帧组的总得分,得到最高组得分bestAccScore,并以此决定阈值minScoreToRetain
for (list<pair<float, KeyFrame *>>::iterator it = lScoreAndMatch.begin(), itend = lScoreAndMatch.end(); it != itend; it++)
{
KeyFrame *pKFi = it->second;
vector<KeyFrame *> vpNeighs = pKFi->GetBestCovisibilityKeyFrames(10);

float bestScore = it->first; // 该组最高分数
float accScore = it->first; // 该组累计得分
KeyFrame *pBestKF = pKFi; // 该组最高分数对应的关键帧
// 遍历共视关键帧,累计得分
for (vector<KeyFrame *>::iterator vit = vpNeighs.begin(), vend = vpNeighs.end(); vit != vend; vit++)
{
KeyFrame *pKF2 = *vit;
// 只有pKF2也在闭环候选帧中,且公共单词数超过最小要求,才能贡献分数
if (pKF2->mnLoopQuery == pKF->mnId && pKF2->mnLoopWords > minCommonWords)
{
accScore += pKF2->mLoopScore;
// 统计得到组里分数最高的关键帧
if (pKF2->mLoopScore > bestScore)
{
pBestKF = pKF2;
bestScore = pKF2->mLoopScore;
}
}
}

lAccScoreAndMatch.push_back(make_pair(accScore, pBestKF));
// 记录所有组中组得分最高的组,用于确定相对阈值
if (accScore > bestAccScore)
bestAccScore = accScore;
}

// 所有组中最高得分的0.75倍,作为最低阈值
float minScoreToRetain = 0.75f * bestAccScore;

set<KeyFrame *> spAlreadyAddedKF;
vector<KeyFrame *> vpLoopCandidates;
vpLoopCandidates.reserve(lAccScoreAndMatch.size());

// Step 5:只取组得分大于阈值的组,得到组中分数最高的关键帧们作为闭环候选关键帧
for (list<pair<float, KeyFrame *>>::iterator it = lAccScoreAndMatch.begin(), itend = lAccScoreAndMatch.end(); it != itend; it++)
{
if (it->first > minScoreToRetain)
{
KeyFrame *pKFi = it->second;
// spAlreadyAddedKF 是为了防止重复添加
if (!spAlreadyAddedKF.count(pKFi))
{
vpLoopCandidates.push_back(pKFi);
spAlreadyAddedKF.insert(pKFi);
}
}
}

return vpLoopCandidates;
}