词袋向量的计算原理,可以理解为一个“构建视觉词典”和“用词典描述图像”的过程。
它的核心思想是:把图像中提取的“特征点”类比成文章中的“单词”,通过统计这些“单词”在图像中出现的频率,将一张复杂的图像转换成一个数值向量。
这个计算过程主要分为两大步:离线构建词典和在线计算向量。
🏗️ 第一步:离线构建“视觉词典”
在系统运行前,需要先利用大量图片,通过聚类算法构建一棵“词汇树”(Vocabulary Tree),作为所有图像的通用词典。
提取特征:从大量不同的场景图片中提取ORB特征点及其描述子(Descriptors)。
构建词汇树(K-means聚类):将所有提取到的特征描述子,通过K-means++算法进行层层聚类。
第一层:将所有特征点聚成
k类,得到k个聚类中心,这些中心就是树第一层的节点。下一层:对第一层的每一个节点(类),再将其内部的特征点聚成
k类,生成下一层的节点。重复:这个过程重复
d层,最终得到一棵深度为d、每个节点有k个分支的k-d树。
定义“单词”:这棵词汇树的叶子节点(最底层的节点)就被定义为一个个的“视觉单词”(Visual Word)。整个词典总共可以表示
k^d个不同的视觉单词。(d层,每一层上有K个节点,第d层上共k^d个节点,也就是k^d个单词)
例如,在ORB-SLAM3中,词典的参数为
k=10,d=6,因此可以表达多达10^6 = 1,000,000个不同的视觉单词。
📝 第二步:在线计算图像的“词袋向量”
当系统运行时,对于每一帧新图像(关键帧),都会进行以下操作来生成词袋向量:
提取特征:提取当前图像的ORB特征点和描述子。
寻找单词:对于当前图像中的每一个特征点描述子,从词汇树的根节点开始(遍历词典/词汇树的每一层),计算它与当前层所有
k个节点的距离,选择距离最近的节点进入下一层,直到抵达一个叶子节点。这个叶子节点的索引,就是该特征点对应的视觉单词ID(Word ID)。(当前帧中m个特征点描述子,每个描述子可以查词典得到一个world ID,一个n个world ID ,多个特征点描述子可能查到同一个world ID)统计词频(TF):遍历完当前帧所有特征点后,统计每个视觉单词ID在这张图像中出现的次数(Term Frequency, TF)。(n个world ID ,计算每一个world ID 的次数)
计算权重(TF-IDF):为每个单词的频数乘以一个权重。这个权重由TF-IDF方案决定。
TF (词频):即上一步统计的单词在当前图像中出现的频率。
IDF (逆文档频率):在离线构建词典时就已算好。它衡量(所有训练图片中的每一个)一个单词在所有训练图片中出现的频繁程度。如果一个单词在越多的图片中出现,说明它越普遍,区分度越低,其IDF值就越小。
最终权重:
单词权重 = TF × IDF。(这个权重值就是TF-IDF)
最终,一张图像会被表示为一个稀疏的向量(mBowVec),其中包含了这张图像里出现的所有单词ID及其对应的TF-IDF权重。(mBowVec 中保存的是 world ID 和 其对应的TF-IDF权重)
⚡ 加速索引:正向与逆向索引
为了加速后续的匹配和检索,ORB-SLAM3还会构建两种索引:
逆向索引(Inverse Index):以“单词”为 key,记录包含该单词的所有图像的列表。在闭环检测时,可以通过当前帧的单词,快速找到所有与之有共同单词的历史关键帧。(包含有某一个特征点的所有关键帧)
正向索引(Direct Index):以“图像”为 key,记录该图像中,每个词汇树节点下有哪些特征点。在特征匹配时,可以只对两帧图像中属于同一节点的特征点进行匹配,大大缩小了搜索范围。(某一个关键帧中包含的所有特征点)
💎 总结
词袋向量的计算,本质上是将图像特征(ORB描述子)通过离线构建的词汇树(Vocabulary Tree)量化成离散的视觉单词(Visual Word),再统计这些单词的频率并用TF-IDF加权,最终形成一个可用于高效相似度计算的稀疏向量。
补充:
mBowVec的维度在概念上等同于整个视觉词典的大小,但在实际存储时,它是一个仅包含非零元素的稀疏向量。
具体可以从以下几个方面来理解:
📐 概念维度:等同于词典大小
视觉词典(Vocabulary)是通过离线训练得到的,它包含了数量庞大的“视觉单词”(Visual Word)。mBowVec在概念上是一个维度为N的向量,这个N就是词典中单词的总数。
在ORB-SLAM3中,这个N的典型值是1,000,000(即10^6)。这个数字来源于词典构建时的参数:一个深度为6层、每层有10个分支的词汇树(k=10, d=6),其叶子节点(即单词)总数就是10^6。
🗺️ 实际存储:一个“稀疏”的映射表
虽然概念上维度高达百万,但一张图像只包含有限个特征点(例如几百个),因此它只会命中词典中极少数的单词。如果用一个长度为1,000,000的数组来存储,其中绝大部分元素都是0,会造成巨大的内存浪费。
因此,mBowVec的实际类型是DBoW2::BowVector,它被定义为一个std::map<WordId, WordValue>。
WordId:是命中单词的ID(键)。WordValue:是该单词对应的TF-IDF 权重值(值)。
这种设计意味着mBowVec的实际大小(size())等于该图像中非零权重单词的数量,通常只有几百。它只存储有意义的“单词-权重”对,忽略所有零值,是一种非常高效的存储方式。
💎 总结
概念上,
mBowVec的维度是词典的大小,通常为1,000,000。实际上,它作为一个
std::map,其元素个数等于图像中有效特征点对应的单词数量,通常为几百个。