在上一篇文章中图像检索 (2): 均值聚类 - 构建 BoF 中, 简略的介绍了基于 sift 特征点的 BoW 模型的构建, 以及基于轻量级开源库 vlfeat 的一个简单实现.
本文重新梳理了一下 BoW 模型, 并给出不同的实现.
基于 OpenCV 的 BoW 实现
BoWTrainer 的使用
词袋模型开源库 DBoW3
BoW
BoW 模型最初是为解决文档建模问题而提出的, 因为文本本身就是由单词组成的. 它忽略文本的词序, 语法, 句法, 仅仅将文本当作一个个词的集合, 并且假设每个词彼此都是独立的. 这样就可以使用文本中词出现的频率来对文档进行描述, 将一个文档表示成一个一维的向量.
将 BoW 引入到计算机视觉中, 就是将一幅图像看着文本对象, 图像中的不同特征可以看着构成图像的不同词汇. 和文本的 BoW 类似, 这样就可以使用图像特征在图像中出现的频率, 使用一个一维的向量来描述图像.
要将图像表示为 BoW 的向量, 首先就是要得到图像的 "词汇". 通常需要在整个图像库中提取图像的局部特征 (例如, sift,orb 等), 然后使用聚类的方法, 合并相近的特征, 聚类的中心可以看着一个个的视觉词汇 (visual word), 视觉词汇的集合构成视觉词典 (visual vocabulary) . 得到视觉词汇集合后, 统计图像中各个视觉词汇出现的频率, 就得到了图像的 BoW 表示.
总结起来就是:
提取图像库中所有图像的局部特征, 例如 sift, 得到特征集合 \(F\)
对特征集合 \(F\) 进行聚类, 得到 \(k\) 个聚类中心 \(\{C_i|i = 1,\dots,k\}\), 每一个聚类中心 \(C_i\) 代表着一个视觉词汇. 聚类中心的集合就是视觉词典 \(vocabulary = \{C_i|i = 1,\dots,k\}\)
一幅图像的 BoW 表示
提取图像的局部特征, 得到特征集合 \(f = \{f_i |i = 1,\dots,n\}\)
计算特征 \(f_i\) 属于那个词汇 \(C_i\)(到该中心的距离最近)
统计每个词汇 \(C_i\) 在图像中出现的频数, 得到一个一维的向量, 该向量就是图像的 BoW 表示.
综合起来, 取得一幅图像的 BoW 向量的步骤:
构建图像库的视觉词典 Vocabulary
提取图像库中所有图像的局部特征, 如 SIFT.
对提取到的图像特征进行聚类, 如 k-means, 得到聚类中心就是图像库的视觉词汇词典 Vocabulary
计算一幅图像的 BoW 向量
提取图像的局部特征
统计 Vocabulay 中的每个视觉词汇 visual word , 在图像中出现的频率.
基于 OpenCV 的实现
基于 OpenCV 的原生实现
第一步提取图像的 sift 特征. 对 sift 特征的详细讲解, 可以参考其余两篇文章: 再论 SIFT - 基于 vlfeat 实现 和 SIFT 特征详解. 这里不再赘述, 提取特征的代码如下:
- void siftDetecotor::extractFeatures(const std::vector<std::string> &imageFileList,std::vector<cv::Mat> &features)
- {
- int index = 1;
- int count = 0;
- features.reserve(imageFileList.size());
- auto size = imageFileList.size();
- //size = 20;
- //#pragma omp parallel for
- for(size_t i = 0; i <size; i ++){
- auto str = imageFileList[i];
- Mat des;
- siftDetecotor::extractFeatures(str,des);
- features.emplace_back(des);
- count += des.rows;
- index ++ ;
- }
- cout << "Extract #" << index << "# images features done!" << "Count of features:#" << count << endl;
- }
传入 imageFileList 是图像的路径列表,
vector<Mat> featurues
返回提取得到的所有图像的特征.
聚类, 得到 Vocabulary
OpenCV 中 k-means 聚类的接口如下:
- double cv::kmeans ( InputArray data,
- int K,
- InputOutputArray bestLabels,
- TermCriteria criteria,
- int attempts,
- int flags,
- OutputArray centers = noArray()
- )
data 输入数据, 每一行是一条数据.
k 聚类的个数, 这是就是 Vocabulary 的大小 (词汇的个数 u).
bestLabels 每一个输入的数据所属的聚类中心的 index
criteria kmenas 算法是迭代进行, 这里表示迭代的终止条件. 可以是迭代的次数, 或者是结果达到的精度, 也可以是两者的结合, 达到任一条件就结束.
attmepts 算法的次数, 使用不同的初始化方法
flags 算法的初始化方法, 可以选择随机初始化
KMEANS_RANDOM_CENTERS
, 或者 kmeans++ 的方法 KMEANS_PP_CENTERS
centers 聚类的中心组成的矩阵.
得到图像库中图像的所有特征后, 可以将这些特征组成一个大的矩阵输入到 kmeans 算法中, 得到聚类中心, 也就是 Vocabulary
- Mat f;
- vconcat(features,f);
- vector<int> labes;
- kmeans(f,k,labes,TermCriteria(TermCriteria::COUNT + TermCriteria::EPS,100,0.01),3,cv::KMEANS_PP_CENTERS,m_voc);
首先, 使用 vconcat 将提取的特征点沿 Y 方向叠放在一起. k-means 算法的终止条件是
ermCriteria::COUNT + TermCriteria::EPS,100,0.01
, 算法迭代 100 次或者精度达到 0.01 就结束.
图像的 BoW 编码
得到 Vocabulary 后, 统计视觉词汇在每个图像出现的概率就很容易得到图像的 BoW 编码
- void Vocabulary::transform_bow(const cv::Mat &img,std::vector<int> bow)
- {
- auto fdetector = xfeatures2d:: SIFT ::create(0,3,0.2,10);
- vector<KeyPoint> kpts;
- Mat des;
- fdetector->detectAndCompute(img,noArray(),kpts,des);
- Mat f;
- rootSift(des,f);
- // Find the nearest center
- Ptr<FlannBasedMatcher> matcher = FlannBasedMatcher::create();
- vector<DMatch> matches;
- matcher->match(f,m_voc,matches);
- bow = vector<int>(m_k,0);
- // Frequency
- /*for( size_t i = 0; i <matches.size(); i++ ){
- int queryIdx = matches[i].queryIdx;
- int trainIdx = matches[i].trainIdx; // cluster index
- CV_Assert( queryIdx == (int)i );
- bow[trainIdx] ++; // Compute word frequency
- }*/
- // trainIdx => center index
- for_each(matches.begin(),matches.end(),[&bow](const DMatch &match){
- bow[match.trainIdx] ++; // Compute word frequency
- });
- }
在查找图像的某个特征属于的聚类中心时, 本质上就是查找最近的向量, 可以使用 flann 建立索引树来查找; 也可以使用一些特征匹配的方法, 这里使用 flannMatcher. 统计每个词汇在图像中出现的频率, 即可得到图像的 BoW 向量.
BoWTrainer
在 OpenCV 中封装了 3 个关于 BoW 的类.
抽象基类 BOWTrainer, 从图像库中的特征集中构建视觉词汇表 Vobulary
- class CV_EXPORTS_W BOWTrainer
- {
- public:
- BOWTrainer();
- virtual ~BOWTrainer();
- CV_WRAP void add( const Mat& descriptors );
- CV_WRAP const std::vector<Mat>& getDescriptors() const;
- CV_WRAP int descriptorsCount() const;
- CV_WRAP virtual void clear();
- CV_WRAP virtual Mat cluster() const = 0;
- CV_WRAP virtual Mat cluster( const Mat& descriptors ) const = 0;
- protected:
- std::vector<Mat> descriptors;
- int size;
- };
类 BOWKMeansTrainer 基于 k-means 聚类, 实现了 BOWTrainer 的方法. 使用 kmeans 方法, 从特征集中聚类得到视觉词汇表 Vocabulary. 其声明如下:
- class CV_EXPORTS_W BOWKMeansTrainer : public BOWTrainer
- {
- public:
- CV_WRAP BOWKMeansTrainer( int clusterCount, const TermCriteria& termcrit=TermCriteria(),
- int attempts=3, int flags=KMEANS_PP_CENTERS );
- virtual ~BOWKMeansTrainer();
- CV_WRAP virtual Mat cluster() const;
- CV_WRAP virtual Mat cluster( const Mat& descriptors ) const;
- protected:
- int clusterCount;
- TermCriteria termcrit;
- int attempts;
- int flags;
- };
该类的使用也是很简单的, 首先构建一个 BOWKMeansTrainer 的实例, 其第一个参数 clusterCount 是聚类中心的个数, 也就是 Vocabulary 的大小, 余下的几个参数就是使用 kmeans 函数的参数, 具体可参考上面的介绍.
然后, 调用 add 方法, 添加提取到的特征集. 添加特征集的时候, 有两种方法:
- for(int i=0; i<numOfPictures; i++)
- bowTraining.add( descriptors( i ) );
也可以提取好所有图像的特征, 然后将特征合并为一个矩阵添加
- Mat feature_list;
- vconcat(features,feature_list);
- BOWKMeansTrainer bow_trainer(k);
- bow_trainer.add(feature_list);
添加图像特征后, 调用
vocabulary = bow_trainer.cluster();
对特征集进行聚类, 得到的聚类中心就是所要求的视觉词汇表 Vocabulary.
在得到 Vocabulary 后, 就可以对一副图像进行编码, 使用 BoW 向量来表示该图像, 这时候就要使用
BOWImgDescriptorExtractor
. 其声明如下:
- class BOWImgDescriptorExtractor{
- public:
- BOWImgDescriptorExtractor( const Ptr<DescriptorExtractor> &dextractor, const Ptr<DescriptorMatcher> & dmatcher );
- virtual ~BOWImgDescriptorExtractor(){}
- void setVocabulary( const Mat& vocabulary );
- const Mat& getVocabulary() const;
- void compute( const Mat& image, vector<KeyPoint> & keypoints,
- Mat& imgDescriptor,
- vector<vector<int>>* pointIdxOfClusters = 0,
- Mat* descriptors = 0 );
- int descriptorSize() const;
- int descriptorType() const;
- protected:
- Mat vocabulary;
- Ptr<DescriptorExtractor> dextractor;
- Ptr<DescriptorMatcher> dmatcher;
该类实现了一下三个功能:
根据相应的 Extractor 提取图像的特征
找到距离每个特征最近的 visual word
计算图像的 BoW 表示, 并且将其进归一化.
要实例化一个
BOWImgDescriptorExtractor
, 需要提供三个参数
视觉词汇表 Vocabulalry
图像特征提取器
DescriptorExtractor
特征匹配的方法 descriptorMatcher , 用来查找和某个特征最近的 visual word.
其使用也很便利, 使用 Extractor 和 Matcher 实例化一个
BOWImgDescriptorExtractor
, 然后设置 Vocabulary,
- BOWImgDescriptorExtractor bowDE(extractor, matcher);
- bowDE.setVocabulary(dictionary); //dictionary 是通过前面聚类得到的词典;
要求某图像的 BoW, 可以调用 compute 方法
- bowDE.compute(img, keypoints, bow);
- Summary
BOWKMeansTrainer 对提取到的图像特征集进行聚类, 得到视觉词汇表 Vocabulary
BOWImgDescriptorExtractor
在得到视觉词汇表后, 使用该类, 可以很方便的对图像进行 BoW 编码.
DBoW3
DBoW3 是一个开源的 C++ 词袋模型库, 可以很方便将图像转化成视觉词袋表示. 它采用层级树状结构将相近的图像特征在物理存储上聚集在一起, 创建一个视觉词典. DBoW3 还生成一个图像数据库, 带有顺序索引和逆序索引, 可以使图像特征的检索和对比非常快.
DBoW3 是 DBoW2 的增强版, 仅依赖 OpenCV, 能够很方便的使用. 开源的 SLAM 项目 ORB_SLAM2 就是使用 DBoW2 进行回环检测的, 关于 DBoW3 详细介绍, 可以参考浅谈回环检测中的词袋模型 (bag of words) https://blog.csdn.net/qq_24893115/article/details/52629248 .
本文仅介绍下 DBoW3 的使用, DBoW3 的源代码在 github 上 https://github.com/rmsalinas/DBow3, 是基于 CMake 的, 配置好 OpenCV 库后, 直接 cmake 编译即可.
DBoW3 两个比较重要的类是 Vocabulary 和 Database,Vocabulary 表示图像库的视觉词汇表, 并可以将任一的图像转换为 BoW 表示, Database 是一个图像数据库, 能够方便的对图像进行检索.
构建 Vocabulary 的代码如下:
- void vocabulary(const vector<Mat> &features,const string &file_path,int k = 9,int l = 3){
- //Branching factor and depth levels
- const DBoW3::WeightingType weight = DBoW3::TF_IDF;
- const DBoW3::ScoringType score = DBoW3::L2_NORM;
- DBoW3::Vocabulary voc(k,l,weight,score);
- cout <<"Creating a small" << k << "^" << l << "vocabulary..." << endl;
- voc.create(features);
- cout << "...done!" << endl;
- //cout << "Vocabulary infomation:" << endl << voc << endl << endl;
- // save the vocabulary to disk
- cout << endl << "Saving vocabulary..." << endl;
- stringstream ss;
- ss << file_path << "/small_voc.yml.gz";
- voc.save(ss.str());
- cout << "Done" << endl;
- }
传入图像的特征集, 配置聚类树的分支树 (k), 以及深度 (l), 调用 create 进行聚类即得到 Vocabulary. 可以将得到的 Vocabulary 保存成文件, 以便后面使用.
有了 Vocabulary 后, 就可以构建一个 Database 方便图像的查找
- void database(const vector<Mat> &features,const string &file_path){
- // load the vocabulary from disk
- stringstream ss ;
- ss <<file_path <<"/small_voc.yml.gz";
- DBoW3::Vocabulary voc(ss.str());
- DBoW3::Database db(voc, false, 0); // false = do not use direct index
- // add images to the database
- for(size_t i = 0; i < features.size(); i++)
- db.add(features[i]);
- cout << "... done!" << endl;
- cout << "Database information:" << endl << db << endl;
- // we can save the database. The created file includes the vocabulary
- // and the entries added
- cout << "Saving database..." << endl;
- db.save("small_db.yml.gz");
- cout << "... done!" << endl;
- }
需要前面得到 Vocabulary 和图像的特征集来创建 Database, 创建完成后, 也可以将其保存为本地文件, 方便后面的使用.
有了 Database 后, 可以其调用 query 方法, 来查找数据库中是否有相类似的图片.
- //auto fdetector=cv::xfeatures2d::SURF::create(400, 4, 2);
- auto fdetector = xfeatures2d::SIFT::create(0,3,0.2,10);
- vector<KeyPoint> kpts;
- Mat des;
- fdetector->detectAndCompute(img,noArray(),kpts,des);
- db.query(des,ql,max_resuts);
提取图像的特征, 调用 query 该方法使用 QueryResults 返回查询的结构.
Summary
本文重新梳理了下 BoW 模型, 并且介绍了三种的实现方法:
基于 OpenCV 的原生实现, 调用 OpenCV 的特征提取, 聚类, 匹配方法, 获取图像的 BoW 向量.
使用 OpenCV 封装的 BowTrainer 和
BOWImgDescriptorExtractor
类, 更简单的实现 BoW 模型.
使用开源库 DBoW3, 该方法不断能够很简单的创建 Vocabulary, 而且创建了一个图像的 Database, 比较方法的利用 BoW 向量在图像库中查找类似图片.
做了几个月的图像检索, 陆续把这段时间的收获整理下. 本文主要介绍了 BoW 的实现, 下一篇争取实现一个完整的图像检索的流程, 预计有以下几个方面:
- TF-IDF
- root-sift
- vlad
本系列图像检索的代码会 push 到 github 上, 初期一直在写各种 sample, 代码有点乱, 欢迎 fork/start.
地址: https://github.com/brookicv/imageRetrieval
来源: https://www.cnblogs.com/wangguchangqing/p/9435269.html