一、理解BoW
假设有三句话
我喜欢吃苹果
我喜欢吃香蕉
我喜欢看电影
我们建立一个词典
Vocabulary:
Word 0:我
Word 1:喜欢
Word 2:吃
Word 3:苹果
Word 4:香蕉
Word 5:看
Word 6:电影
那么刚才的三句话就可以表示为:
我 → Word 0
喜欢 → Word 1
吃 → Word 2
苹果 → Word 3
即 BoW1=[1,1,1,1,0,0,0]
同理,剩下的两句可以表示为:
BoW2=[1,1,1,0,1,0,0]
BoW3=[1,1,0,0,0,1,1]
于是,经过上面的处理,一句话就可以用一个向量表示。视觉BoW也是同样的思想,但是图像中并没有文字,但是有特征点和描述子。
假设一张图片提取了1000个 ORB特征点,
Feature 1 → ORB descriptor
Feature 2 → ORB descriptor
Feature 3 → ORB descriptor
...
Feature 1000 → ORB descriptor
每个特征点的描述子都是256bit,这些描述子直接比较起来虽然可以,但是数量很多。所以 ORB-SLAM2 做了一件非常聪明的事情:把大量相似的 ORB 描述子归成一个"视觉单词"。
什么是"视觉单词"?

假设一张图有1000个ORB特征,经过Vocabulary:
ORB Feature
↓
Visual Word
Feature 1 → Word 25
Feature 2 → Word 102
Feature 3 → Word 25
Feature 4 → Word 8
Feature 5 → Word 102
...
统计以后:
Word 8 → 50个特征
Word 25 → 120个特征
Word 102 → 83个特征
Word 300 → 17个特征
...
于是整张图片就变成:
BoW=[0,0,0,...,50,...,120,...,83,...]
这就是视觉 BoW。
所以ORB_SLAM2的BoW本质可以理解为:一张图片 --> ORB特征 --> 视觉单词 -->词频统计,也就是不再关心每一个具体的ORB特征是什么,而是关心这张图片出现了哪些视觉词,以及各自有多少。
二、ORB_SLAM2 中 ComputeBoW()
函数作用:把当前 Frame中的所有ORB 描述子,转换成视觉词袋表示。
cpp
void Frame::ComputeBoW()
{
if(mBowVec.empty())
{
vector<cv::Mat> vCurrentDesc = Converter::toDescriptorVector(mDescriptors);
mpORBvocabulary->transform(vCurrentDesc,mBowVec,mFeatVec,4);
}
}
变量含义
cpp
transform(输入描述子, 输出BoW向量, 输出Feature向量, 深度);
mDescriptors 表示当前帧中所有特征点的描述子
mBowVec是什么?
mBowVec 通常是:BowVector mBowVec;它不是简单的:vector<float>,而是一个稀疏的:视觉词 ID → 权重
例如:
Word 10 → 0.12
Word 53 → 0.08
Word 128 → 0.25
Word 456 → 0.15
Word 900 → 0.40
为什么不是统计次数而是权重?
假设:
图像A:
Word 1 → 100次
Word 2 → 10次
Word 3 → 1次
简单统计:
[100, 10, 1]
但是有些视觉词特别常见。例如:
墙
边缘
角点
纹理
这些可能在很多图像里都出现。如果一个视觉词几乎每张图都出现,那么它对区分两张图的作用其实很小。这时候就会引入类似:TF-IDF的思想。
TF:Term Frequency。表示:这个视觉词在当前图像中出现得多不多?
IDF:Inverse Document Frequency。表示:这个视觉词在所有图像中常不常见?
于是:
常见视觉词
↓
权重低
而:
稀有视觉词
↓
权重高
所以最终:mBowVec 不是简单的出现次数,而是经过权重计算后的
视觉词 → 权重
mBowVec与mFeatVec的区别
mBowVec表示:这张图像整体上有哪些视觉词? 它主要用于:**判断两张图像是否相似。**例如:
Word 10 → 0.12
Word 53 → 0.08
Word 128 → 0.25
...
mFeatVec表示:**每个视觉词对应了哪些 ORB 特征?**例如:
Word 10 → 特征 3、25、100
Word 53 → 特征 5、19、88
Word 128 → 特征 2、8、30、55
深度是什么?
mpORBvocabulary 指向ORBVocabulary,来自于Vocabulary/ORBvoc.txt。本质是哪个是一个巨大的ORB描述子字典,它不是简单的
cpp
Word 0
Word 1
Word 2
...
而是一个树状结构,即Vocabulary Tree(词汇树)。
为什么需要树状结构?
假设 Vocabulary 有:100万个视觉单词。现在来了一个 ORB 描述子:D。最笨的方法:
D 和 Word 0 比较
D 和 Word 1 比较
D 和 Word 2 比较
...
D 和 Word 999999 比较
需要比较:1000000 次,这非常慢。所以 ORB-SLAM2 建立了一棵树。例如:
Root
|
┌──────────┼──────────┐
↓ ↓ ↓
Node1 Node2 Node3
/ | \ / | \ / | \
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
... ... ...
通过树逐层寻找,这样就不用和所有单词比较。
ORB descriptor
↓
第一层
↓
第二层
↓
第三层
↓
第四层
↓
...
↓
Visual Word
三、BoW 相似 ≠ 一定是同一个地方
比如:两个办公室都有墙、桌子、窗户、电脑,那么 BoW 可能比较相似。只能说明可能存在回环,不能直接证明这是在同一个地方。因此,ORB_SLAM2后边还需要进行特征匹配、几何验证、Sim3、回环检测。
四、具体步骤流程
cpp
当前图像
│
↓
ORB特征提取
│
↓
ORB KeyPoints
│
↓
ORB Descriptors
│
↓
mDescriptors(N×32)
│
↓
Converter::toDescriptorVector()
│
↓
vector<cv::Mat>
│
↓
mpORBvocabulary->transform()
│
↓
Vocabulary Tree
│
↓
每个描述子寻找Visual Word
│
┌──────┴──────┐
↓ ↓
mBowVec mFeatVec
│ │
↓ ↓
图像整体表示 特征分组
│ │
↓ ↓
相似度计算 快速特征匹配
│
↓
回环候选检测
│
↓
几何一致性验证
│
↓
回环闭合
五、举例说明
假设当前图像有 10 个 ORB 描述子:
D1
D2
D3
D4
D5
D6
D7
D8
D9
D10
经过 Vocabulary:
D1 → Word 5
D2 → Word 5
D3 → Word 8
D4 → Word 12
D5 → Word 5
D6 → Word 20
D7 → Word 8
D8 → Word 12
D9 → Word 12
D10 → Word 20
统计:
Word 5 → 3
Word 8 → 2
Word 12 → 3
Word 20 → 2
那么这张图就可以表示成:
Word 5 : 3
Word 8 : 2
Word 12 : 3
Word 20 : 2
再经过权重归一化,这就是:mBowVec。
Word 5 : 0.30
Word 8 : 0.20
Word 12 : 0.30
Word 20 : 0.20
两张图像怎么比较?
假设:图像 A
Word 5 : 0.30
Word 8 : 0.20
Word 12 : 0.30
Word 20 : 0.20
图像 B
Word 5 : 0.28
Word 8 : 0.22
Word 12 : 0.32
Word 20 : 0.18
两者非常接近。所以:Similarity(A,B)比较高。
而图像 C
Word 100 : 0.3
Word 200 : 0.2
Word 300 : 0.3
Word 500 : 0.2
和 A 几乎没有共同视觉词。那么:Similarity(A,C)就比较低。