以下是ORB-SLAM3中ORBmatcher::SearchByBoW函数的完整代码及详细解读。
📄 完整代码
cpp
int ORBmatcher::SearchByBoW(KeyFrame* pKF, Frame &F, vector<MapPoint*> &vpMapPointMatches) { // 1. 获取关键帧的地图点 const vector<MapPoint*> vpMapPointsKF = pKF->GetMapPointMatches(); // 2. 初始化当前帧的匹配结果容器(全部置空) vpMapPointMatches = vector<MapPoint*>(F.N, static_cast<MapPoint*>(NULL)); // 3. 获取关键帧的特征向量(FeatureVector) const DBoW2::FeatureVector &vFeatVecKF = pKF->mFeatVec; int nmatches = 0; // 4. 初始化方向直方图(用于旋转一致性检验) vector<int> rotHist[HISTO_LENGTH]; for(int i=0; i<HISTO_LENGTH; i++) rotHist[i].reserve(500); // 5. 角度到直方图bin的转换因子 const float factor = HISTO_LENGTH / 360.0f; // 6. 同步遍历两个特征向量(只比较相同节点ID下的特征点) DBoW2::FeatureVector::const_iterator KFit = vFeatVecKF.begin(); DBoW2::FeatureVector::const_iterator Fit = F.mFeatVec.begin(); DBoW2::FeatureVector::const_iterator KFend = vFeatVecKF.end(); DBoW2::FeatureVector::const_iterator Fend = F.mFeatVec.end(); while(KFit != KFend && Fit != Fend) { // 6.1 只处理属于同一节点的特征点 if(KFit->first == Fit->first) { // 获取该节点下两帧的特征点索引列表 const vector<unsigned int> vIndicesKF = KFit->second; const vector<unsigned int> vIndicesF = Fit->second; // 6.2 遍历关键帧中该节点的所有特征点 for(size_t iKF=0; iKF<vIndicesKF.size(); iKF++) { const unsigned int realIdxKF = vIndicesKF[iKF]; // 获取该特征点对应的地图点 MapPoint* pMP = vpMapPointsKF[realIdxKF]; if(!pMP || pMP->isBad()) continue; // 获取关键帧中该特征点的描述子 const cv::Mat &dKF = pKF->mDescriptors.row(realIdxKF); int bestDist1 = 256; // 最佳距离(最小汉明距离) int bestIdxF = -1; // 最佳匹配点在当前帧中的索引 int bestDist2 = 256; // 次佳距离 // 6.3 遍历当前帧中该节点的所有特征点,寻找最佳匹配 for(size_t iF=0; iF<vIndicesF.size(); iF++) { const unsigned int realIdxF = vIndicesF[iF]; // 如果该点已经被匹配,跳过 // 当前帧中的这个ORB特征点已经有匹配地图点了MapPoint if(vpMapPointMatches[realIdxF]) continue; const cv::Mat &dF = F.mDescriptors.row(realIdxF); const int dist = DescriptorDistance(dKF, dF); // 更新最佳距离和次佳距离 if(dist < bestDist1) { bestDist2 = bestDist1; bestDist1 = dist; bestIdxF = realIdxF; } else if(dist < bestDist2) { bestDist2 = dist; } } // 6.4 双重检验:距离阈值 + 最近邻比率 if(bestDist1 <= TH_LOW) // 阈值检验 { if(static_cast<float>(bestDist1) < mfNNratio * static_cast<float>(bestDist2)) // 比率检验 { // 接受匹配:当前帧的特征点 → 关键帧的地图点 vpMapPointMatches[bestIdxF] = pMP; nmatches++; // 6.5 计算旋转角度差,存入直方图 if(mbCheckOrientation) { const cv::KeyPoint &kp = pKF->mvKeysUn[realIdxKF]; float rot = kp.angle - F.mvKeys[bestIdxF].angle; if(rot < 0.0) rot += 360.0f; int bin = round(rot * factor); if(bin == HISTO_LENGTH) bin = 0; assert(bin >= 0 && bin < HISTO_LENGTH); rotHist[bin].push_back(bestIdxF); } } } } KFit++; Fit++; } else if(KFit->first < Fit->first) { // 对齐迭代器 KFit = vFeatVecKF.lower_bound(Fit->first); } else { Fit = F.mFeatVec.lower_bound(KFit->first); } } // 7. 旋转一致性检验:剔除不符合主流旋转方向的匹配 if(mbCheckOrientation) { int ind1=-1, ind2=-1, ind3=-1; ComputeThreeMaxima(rotHist, HISTO_LENGTH, ind1, ind2, ind3); for(int i=0; i<HISTO_LENGTH; i++) { // 只保留数量最多的三个bin中的匹配 if(i == ind1 || i == ind2 || i == ind3) continue; // 剔除其他bin中的匹配 for(size_t j=0, jend=rotHist[i].size(); j<jend; j++) { vpMapPointMatches[rotHist[i][j]] = static_cast<MapPoint*>(NULL); nmatches--; } } } return nmatches; }📖 详细解读
🎯 函数作用
SearchByBoW利用词袋模型(Bag-of-Words)加速两帧(关键帧与普通帧,或两个关键帧)之间的特征匹配。它只比较属于同一词汇树节点的特征点,避免了全局暴力搜索,大幅提升匹配效率。该函数广泛应用于跟踪参考关键帧、重定位和闭环检测等场景。
📥 输入参数
| 参数 | 类型 | 说明 |
|---|---|---|
pKF | KeyFrame* | 参考关键帧,提供地图点 |
F | Frame& | 当前帧,待匹配的特征点 |
vpMapPointMatches | vector<MapPoint*>& | 输出:当前帧每个特征点匹配到的地图点 |
📤 返回值
返回成功匹配的数量(int)。
⚙️ 核心原理:五步匹配流程
步骤1:数据准备
cpp
const vector<MapPoint*> vpMapPointsKF = pKF->GetMapPointMatches(); vpMapPointMatches = vector<MapPoint*>(F.N, static_cast<MapPoint*>(NULL)); const DBoW2::FeatureVector &vFeatVecKF = pKF->mFeatVec;
获取关键帧中每个特征点对应的地图点
初始化当前帧的匹配结果(全部置空)
获取关键帧的特征向量(
mFeatVec)—— 一个按节点ID索引特征点索引列表的映射表
步骤2:同步遍历 FeatureVector
cpp
DBoW2::FeatureVector::const_iterator KFit = vFeatVecKF.begin(); DBoW2::FeatureVector::const_iterator Fit = F.mFeatVec.begin(); while(KFit != KFend && Fit != Fend) { if(KFit->first == Fit->first) { /* 匹配 */ } else if(KFit->first < Fit->first) { KFit = vFeatVecKF.lower_bound(Fit->first); } else { Fit = F.mFeatVec.lower_bound(KFit->first); } }FeatureVector的本质是std::map<NodeId, vector<unsigned int>>:
Key:词汇树节点ID(NodeId)
Value:属于该节点的特征点索引列表
同步遍历确保只比较两帧中属于同一节点的特征点。这利用了词袋模型的核心思想:属于同一节点的特征点在视觉上相似,最可能产生正确匹配。
步骤3:节点内暴力匹配
对于关键帧中一个有效特征点(有对应地图点),在当前帧同一节点的所有特征点中计算汉明距离,找出最佳匹配(最小距离)和次佳匹配(第二小距离)。
cpp
int bestDist1 = 256; // 最佳距离 int bestIdxF = -1; int bestDist2 = 256; // 次佳距离 for(size_t iF=0; iF<vIndicesF.size(); iF++) { const int dist = DescriptorDistance(dKF, dF); // 更新 bestDist1 和 bestDist2 }步骤4:双重检验接受匹配
一个候选匹配必须同时满足两个条件:
| 检验 | 条件 | 作用 |
|---|---|---|
| 距离阈值 | bestDist1 <= TH_LOW | 确保描述子足够相似 |
| 最近邻比率 | bestDist1 < mfNNratio * bestDist2 | 确保匹配是"独特"的,没有模糊候选 |
mfNNratio默认值为0.6。比率检验能有效剔除因重复纹理导致的误匹配。
步骤5:方向直方图验证
cpp
float rot = kp.angle - F.mvKeys[bestIdxF].angle; if(rot < 0.0) rot += 360.0f; int bin = round(rot * factor); rotHist[bin].push_back(bestIdxF);
将所有匹配点的主方向角度差统计到30个bin的直方图中。匹配完成后,只保留直方图中数量最多的三个bin中的匹配,剔除其余匹配。
这一步基于的物理约束是:两帧图像之间的旋转是全局一致的。如果某个匹配的角度差与主流方向不一致,它很可能是误匹配。
🔧 关键成员变量
| 变量 | 默认值 | 说明 |
|---|---|---|
mfNNratio | 0.6 | 最近邻比率阈值 |
mbCheckOrientation | true | 是否进行旋转一致性检验 |
TH_LOW | 50 | 描述子距离低阈值 |
HISTO_LENGTH | 30 | 方向直方图的bin数量 |
🎯 应用场景
| 场景 | 调用位置 | 用途 |
|---|---|---|
| 跟踪参考关键帧 | Tracking::TrackReferenceKeyFrame() | 恒速模型失效时,匹配当前帧与参考关键帧 |
| 重定位 | Tracking::Relocalization() | 跟踪丢失后,匹配当前帧与候选关键帧 |
| 闭环检测 | LoopClosing::DetectLoop() | 匹配当前关键帧与闭环候选关键帧 |
💎 总结
SearchByBoW通过FeatureVector的同步遍历将匹配范围从"全局所有特征点"缩小到"同一节点内的特征点",再通过距离阈值 + 比率检验 + 方向一致性三重过滤保证匹配质量。它无需位姿信息即可完成匹配,是ORB-SLAM3实现实时跟踪与闭环检测的关键基础函数。