在上一篇文章中图像检索(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的原生实现

  1. void siftDetecotor::extractFeatures(const std::vector<std::string> &imageFileList,std::vector<cv::Mat> &features)
  2. {
  3. int index = 1;
  4. int count = 0;
  5. features.reserve(imageFileList.size());
  6. auto size = imageFileList.size();
  7. //size = 20;
  8. //#pragma omp parallel for
  9. for(size_t i = 0; i < size; i ++){
  10. auto str = imageFileList[i];
  11. Mat des;
  12. siftDetecotor::extractFeatures(str,des);
  13. features.emplace_back(des);
  14. count += des.rows;
  15. index ++ ;
  16. }
  17. cout << "Extract #" << index << "# images features done!" << "Count of features:#" << count << endl;
  18. }

传入imageFileList是图像的路径列表,vector<Mat> featurues返回提取得到的所有图像的特征。

  • 聚类,得到Vocabulary

    OpenCV中k-means聚类的接口如下:
  1. double cv::kmeans ( InputArray data,
  2. int K,
  3. InputOutputArray bestLabels,
  4. TermCriteria criteria,
  5. int attempts,
  6. int flags,
  7. OutputArray centers = noArray()
  8. )
  • data输入数据,每一行是一条数据。
  • k聚类的个数,这是就是Vocabulary的大小(词汇的个数u)。
  • bestLabels每一个输入的数据所属的聚类中心的index
  • criteria kmenas算法是迭代进行,这里表示迭代的终止条件。 可以是迭代的次数,或者是结果达到的精度,也可以是两者的结合,达到任一条件就结束。
  • attmepts 算法的次数,使用不同的初始化方法
  • flags 算法的初始化方法,可以选择随机初始化KMEANS_RANDOM_CENTERS,或者kmeans++的方法KMEANS_PP_CENTERS
  • centers 聚类的中心组成的矩阵。

得到图像库中图像的所有特征后,可以将这些特征组成一个大的矩阵输入到kmeans算法中,得到聚类中心,也就是Vocabulary

  1. Mat f;
  2. vconcat(features,f);
  3. vector<int> labes;
  4. 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编码
  1. void Vocabulary::transform_bow(const cv::Mat &img,std::vector<int> bow)
  2. {
  3. auto fdetector = xfeatures2d:: SIFT ::create(0,3,0.2,10);
  4. vector<KeyPoint> kpts;
  5. Mat des;
  6. fdetector->detectAndCompute(img,noArray(),kpts,des);
  7. Mat f;
  8. rootSift(des,f);
  9. // Find the nearest center
  10. Ptr<FlannBasedMatcher> matcher = FlannBasedMatcher::create();
  11. vector<DMatch> matches;
  12. matcher->match(f,m_voc,matches);
  13. bow = vector<int>(m_k,0);
  14. // Frequency
  15. /*for( size_t i = 0; i < matches.size(); i++ ){
  16. int queryIdx = matches[i].queryIdx;
  17. int trainIdx = matches[i].trainIdx; // cluster index
  18. CV_Assert( queryIdx == (int)i );
  19. bow[trainIdx] ++; // Compute word frequency
  20. }*/
  21. // trainIdx => center index
  22. for_each(matches.begin(),matches.end(),[&bow](const DMatch &match){
  23. bow[match.trainIdx] ++; // Compute word frequency
  24. });
  25. }

在查找图像的某个特征属于的聚类中心时,本质上就是查找最近的向量,可以使用flann建立索引树来查找;也可以使用一些特征匹配的方法,这里使用flannMatcher。统计每个词汇在图像中出现的频率,即可得到图像的BoW向量。

BoWTrainer

在OpenCV中封装了3个关于BoW的类。

抽象基类BOWTrainer,从图像库中的特征集中构建视觉词汇表Vobulary

  1. class CV_EXPORTS_W BOWTrainer
  2. {
  3. public:
  4. BOWTrainer();
  5. virtual ~BOWTrainer();
  6. CV_WRAP void add( const Mat& descriptors );
  7. CV_WRAP const std::vector<Mat>& getDescriptors() const;
  8. CV_WRAP int descriptorsCount() const;
  9. CV_WRAP virtual void clear();
  10. CV_WRAP virtual Mat cluster() const = 0;
  11. CV_WRAP virtual Mat cluster( const Mat& descriptors ) const = 0;
  12. protected:
  13. std::vector<Mat> descriptors;
  14. int size;
  15. };

BOWKMeansTrainer基于k-means聚类,实现了BOWTrainer的方法。使用kmeans方法,从特征集中聚类得到视觉词汇表Vocabulary。其声明如下:

  1. class CV_EXPORTS_W BOWKMeansTrainer : public BOWTrainer
  2. {
  3. public:
  4. CV_WRAP BOWKMeansTrainer( int clusterCount, const TermCriteria& termcrit=TermCriteria(),
  5. int attempts=3, int flags=KMEANS_PP_CENTERS );
  6. virtual ~BOWKMeansTrainer();
  7. CV_WRAP virtual Mat cluster() const;
  8. CV_WRAP virtual Mat cluster( const Mat& descriptors ) const;
  9. protected:
  10. int clusterCount;
  11. TermCriteria termcrit;
  12. int attempts;
  13. int flags;
  14. };

该类的使用也是很简单的,首先构建一个BOWKMeansTrainer的实例,其第一个参数clusterCount是聚类中心的个数,也就是Vocabulary的大小,余下的几个参数就是使用kmeans函数的参数,具体可参考上面的介绍。

然后,调用add方法,添加提取到的特征集。 添加特征集的时候,有两种方法:

  1. for(int i=0; i<numOfPictures; i++)
  2. bowTraining.add( descriptors( i ) );

也可以提取好所有图像的特征,然后将特征合并为一个矩阵添加

  1. Mat feature_list;
  2. vconcat(features,feature_list);
  3. BOWKMeansTrainer bow_trainer(k);
  4. bow_trainer.add(feature_list);

添加图像特征后,调用

  1. vocabulary = bow_trainer.cluster();

对特征集进行聚类,得到的聚类中心就是所要求的视觉词汇表Vocabulary

在得到Vocabulary后,就可以对一副图像进行编码,使用BoW向量来表示该图像,这时候就要使用BOWImgDescriptorExtractor。其声明如下:

  1. class BOWImgDescriptorExtractor{
  2. public:
  3. BOWImgDescriptorExtractor( const Ptr<DescriptorExtractor> &dextractor, const Ptr<DescriptorMatcher> & dmatcher );
  4. virtual ~BOWImgDescriptorExtractor(){}
  5. void setVocabulary( const Mat& vocabulary );
  6. const Mat& getVocabulary() const;
  7. void compute( const Mat& image, vector<KeyPoint> & keypoints,
  8. Mat& imgDescriptor,
  9. vector<vector<int> >* pointIdxOfClusters = 0,
  10. Mat* descriptors = 0 );
  11. int descriptorSize() const;
  12. int descriptorType() const;
  13. protected:
  14. Mat vocabulary;
  15. Ptr<DescriptorExtractor> dextractor;
  16. Ptr<DescriptorMatcher> dmatcher;

该类实现了一下三个功能:

  • 根据相应的Extractor提取图像的特征
  • 找到距离每个特征最近的visual word
  • 计算图像的BoW表示,并且将其进归一化。

要实例化一个BOWImgDescriptorExtractor,需要提供三个参数

  • 视觉词汇表Vocabulalry
  • 图像特征提取器 DescriptorExtractor
  • 特征匹配的方法 descriptorMatcher ,用来查找和某个特征最近的visual word。

其使用也很便利,使用ExtractorMatcher实例化一个BOWImgDescriptorExtractor,然后设置Vocabulary

  1. BOWImgDescriptorExtractor bowDE(extractor, matcher);
  2. bowDE.setVocabulary(dictionary); //dictionary是通过前面聚类得到的词典;

要求某图像的BoW,可以调用compute方法

  1. 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).

本文仅介绍下DBoW3的使用,DBoW3的源代码在github上https://github.com/rmsalinas/DBow3, 是基于CMake的,配置好OpenCV库后,直接cmake编译即可。

DBoW3两个比较重要的类是VocabularyDatabaseVocabulary表示图像库的视觉词汇表,并可以将任一的图像转换为BoW表示,Database是一个图像数据库,能够方便的对图像进行检索。

构建Vocabulary的代码如下:

  1. void vocabulary(const vector<Mat> &features,const string &file_path,int k = 9,int l = 3){
  2. //Branching factor and depth levels
  3. const DBoW3::WeightingType weight = DBoW3::TF_IDF;
  4. const DBoW3::ScoringType score = DBoW3::L2_NORM;
  5. DBoW3::Vocabulary voc(k,l,weight,score);
  6. cout << "Creating a small " << k << "^" << l << " vocabulary..." << endl;
  7. voc.create(features);
  8. cout << "...done!" << endl;
  9. //cout << "Vocabulary infomation: " << endl << voc << endl << endl;
  10. // save the vocabulary to disk
  11. cout << endl << "Saving vocabulary..." << endl;
  12. stringstream ss;
  13. ss << file_path << "/small_voc.yml.gz";
  14. voc.save(ss.str());
  15. cout << "Done" << endl;
  16. }

传入图像的特征集,配置聚类树的分支树(k),以及深度(l),调用create进行聚类即得到Vocabulary。 可以将得到的Vocabulary保存成文件,以便后面使用。

有了Vocabulary后,就可以构建一个Database方便图像的查找

  1. void database(const vector<Mat> &features,const string &file_path){
  2. // load the vocabulary from disk
  3. stringstream ss ;
  4. ss << file_path <<"/small_voc.yml.gz";
  5. DBoW3::Vocabulary voc(ss.str());
  6. DBoW3::Database db(voc, false, 0); // false = do not use direct index
  7. // add images to the database
  8. for(size_t i = 0; i < features.size(); i++)
  9. db.add(features[i]);
  10. cout << "... done!" << endl;
  11. cout << "Database information: " << endl << db << endl;
  12. // we can save the database. The created file includes the vocabulary
  13. // and the entries added
  14. cout << "Saving database..." << endl;
  15. db.save("small_db.yml.gz");
  16. cout << "... done!" << endl;
  17. }

需要前面得到Vocabulary和图像的特征集来创建Database,创建完成后,也可以将其保存为本地文件,方便后面的使用。

有了Database后,可以其调用query方法,来查找数据库中是否有相类似的图片。

  1. //auto fdetector=cv::xfeatures2d::SURF::create(400, 4, 2);
  2. auto fdetector = xfeatures2d::SIFT::create(0,3,0.2,10);
  3. vector<KeyPoint> kpts;
  4. Mat des;
  5. fdetector->detectAndCompute(img,noArray(),kpts,des);
  6. db.query(des,ql,max_resuts);

提取图像的特征,调用query该方法使用QueryResults返回查询的结构。

Summary

本文重新梳理了下BoW模型,并且介绍了三种的实现方法:

  • 基于OpenCV的原生实现,调用OpenCV的特征提取,聚类,匹配方法,获取图像的BoW向量。
  • 使用OpenCV封装的BowTrainerBOWImgDescriptorExtractor类,更简单的实现BoW模型。
  • 使用开源库DBoW3,该方法不断能够很简单的创建Vocabulary,而且创建了一个图像的Database,比较方法的利用BoW向量在图像库中查找类似图片。

做了几个月的图像检索,陆续把这段时间的收获整理下。 本文主要介绍了BoW的实现,下一篇争取实现一个完整的图像检索的流程,预计有以下几个方面:

  • TF-IDF
  • root-sift
  • vlad

本系列图像检索的代码会push到github上,初期一直在写各种sample,代码有点乱,欢迎fork/start。

地址: https://github.com/brookicv/imageRetrieval

图像检索(3):BoW实现的更多相关文章

  1. 图像检索中为什么仍用BOW和LSH

    原文链接:http://blog.csdn.net/jwh_bupt/article/details/27713453 去年年底的时候在一篇博客中,用ANN的框架解释了BOW模型[1],并与LSH[2 ...

  2. SIFT+BOW 实现图像检索

    原文地址:https://blog.csdn.net/silence2015/article/details/77374910 本文概述 图像检索是图像研究领域中一个重要的话题,广泛应用于医学,电子商 ...

  3. 计算机视觉中的词袋模型(Bow,Bag-of-words)

    计算机视觉中的词袋模型(Bow,Bag-of-words) Bag-of-words 读 'xw20084898的专栏'的blogBag-of-words model in computer visi ...

  4. Bag of Words(BOW)模型

    原文来自:http://www.yuanyong.org/blog/cv/bow-mode 重复造轮子并不是完全没有意义的. 这几天忙里偷闲看了一些关于BOW模型的知识,虽然自己做图像检索到目前为止并 ...

  5. 图像检索(5):基于OpenCV实现小型的图像数据库检索

    本文对前面的几篇文章进行个总结,实现一个小型的图像检索应用. 一个小型的图像检索应用可以分为两部分: train,构建图像集的特征数据库. retrieval,检索,给定图像,从图像库中返回最类似的图 ...

  6. 图像检索(4):IF-IDF,RootSift,VLAD

    TF-IDF RootSift VLAD TF-IDF TF-IDF是一种用于信息检索的常用加权技术,在文本检索中,用以评估词语对于一个文件数据库中的其中一份文件的重要程度.词语的重要性随着它在文件中 ...

  7. 图像检索(2):均值聚类-构建BoF

    在图像检索时,通常首先提取图像的局部特征,这些局部特征通常有很高的维度(例如,sift是128维),有很多的冗余信息,直接利用局部特征进行检索,效率和准确度上都不是很好.这就需要重新对提取到的局部特征 ...

  8. 第十九节、基于传统图像处理的目标检测与识别(词袋模型BOW+SVM附代码)

    在上一节.我们已经介绍了使用HOG和SVM实现目标检测和识别,这一节我们将介绍使用词袋模型BOW和SVM实现目标检测和识别. 一 词袋介绍 词袋模型(Bag-Of-Word)的概念最初不是针对计算机视 ...

  9. lecture7图像检索-七月在线-cv

    http://blog.csdn.net/u014568921/article/details/52518587 图像相似性搜索的原理 BOW 原理及代码解析 Bag Of Visual Words ...

随机推荐

  1. Java Fork/Join 框架

    简介 从JDK1.7开始,Java提供Fork/Join框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果. 这种思想和MapReduce很像 ...

  2. Luogu_2597_[ZJOI2012]灾难 倍增lca + 构造

    Luogu_2597_[ZJOI2012]灾难 倍增lca + 构造 题意: 我们用一种叫做食物网的有向图来描述生物之间的关系:一个食物网有N个点,代表N种生物,如果生物x可以吃生物y,那么从y向x连 ...

  3. appium 报错:AttributeError:"NoneType' object has no attribute 'XXX'

    报错截图如下: 问题原因: 根据以上报错提示可已看到问题的原因为:logger中没有info此方法的调用,点击"具体报错的位置"上面的链接,可直接定位到具体的报错位置.根据分析所得 ...

  4. RPC基于http协议通过netty支持文件上传下载

    本人在中间件研发组(主要开发RPC),近期遇到一个需求:RPC基于http协议通过netty支持文件上传下载 经过一系列的资料查找学习,终于实现了该功能 通过netty实现文件上传下载,主要在编解码时 ...

  5. Asp.Net Core 轻松学-多线程之取消令牌

    前言     取消令牌(CancellationToken) 是 .Net Core 中的一项重要功能,正确并合理的使用 CancellationToken 可以让业务达到简化代码.提升服务性能的效果 ...

  6. 【招聘】.NET高级开发、前端高级开发、测试工程师

    .NET架构师 工作地点:厦门-湖里区 工作年限:5年及以上 学历要求:大专或以上 工资范围:15000元 - 25000元 福利待遇:五险一金,带薪年休假,年度旅游,丰富的员工团队活动:生日会.中秋 ...

  7. 基础测试jmeter5.0+badboy(从小白到入门)

    1]测试工具jmeter环境安装 1.1]安装jdk环境 1:必须安装jdk8.0(我尝试安装最新版本不行,好像当时没有配置好.之后安装8.0遍可以正常运行)下载地址:单击此段 配置jdk环境:鼠标右 ...

  8. SSM框架视频资料

    SSM框架视频资料 进行过 Java Web 项目开发的同学,可能都知道 SSM 框架,即 Spring + SpringMVC + Mybatis .很多项目的主体框架都是采用这种模式,这也是 Ja ...

  9. nginx + lua 限制ip地址访问

    实验环境:docker + openresty 我限制的5秒钟内允许访问两次效果图: default.conf  代码如下: lua_shared_dict my_limit_count_store ...

  10. 一起学Android之Sqlite

    概述 Android对Sqlite提供了完全友好的支持,在应用程序内部,都可以通过名称访问任何的数据库.建议通过SQLiteOpenHelpe的子类并通过重写onCreate() 方法进行创建数据表. ...