本文转载了文章(沈阳的博客),目的在于记录自己重复过程中遇到的问题,和更多的人分享讨论。

程序包:猛戳我

物体分类

物体分类是计算机视觉中一个很有意思的问题,有一些已经归类好的图片作为输入,对一些未知类别的图片进行预测。

下面会说明我使用OpenCV实现的两种方法,第一种方法是经典的bag of words的实现;第二种方法基于第一种方法,但使用的分类方法有所不同。

在此之前,有必要说明一下输入的格式,输入训练数据文件夹,和CalTech 101的组织类似。如下所示,每一类图片都放在一个文件夹里,文件夹的名字就是类别的名字,不需要特别的说明文件。

test/
category1/
img01.jpg
img02.jpg

category2/
img01.jpg
img03.jpg

完整的代码和可使用的训练样本可在这里找到,下面代码示例的开头注释为该段代码所在函数。

第一种方法:Bag of words

步骤描述

如[1]所言,这个方法有4个步骤:

  1. 提取训练集中图片的feature。
  2. 将这些feature聚成n类。这n类中的每一类就相当于是图片的“单词”,所有的n个类别构成“词汇表”。我的实现中n取1000,如果训练集很大,应增大取值。
  3. 对训练集中的图片构造bag of words,就是将图片中的feature归到不同的类中,然后统计每一类的feature的频率。这相当于统计一个文本中每一个单词出现的频率。
  4. 训练一个多类分类器,将每张图片的bag of words作为feature vector,将该张图片的类别作为label。

对于未知类别的图片,计算它的bag of words,使用训练的分类器进行分类。

下面按步骤说明具体实现,程序示例有所省略,完整的程序可看源码,我已经很努力地压缩了代码量,而没有降低可读性。

1 提取feature

这一步比较简单,对训练集中的每一张图片,使用opencv的FeatureDetector检测特征点,然后再用DescriptorExtractor抽取特征点描述符。

01 // BuildVocabulary
02 Mat allDescriptors; 
03 loop over each category {
04     loop over each image in current category {
05         Mat image = imread( filepath );
06         vector<KeyPoint> keyPoints;
07         Mat descriptors;
08         detector -> detect( image, keyPoints);
09         extractor -> compute( image, keyPoints, descriptors );
10         allDescriptors.push_back( descriptors );    
11     }
12 }

2 feature聚类

由于opencv封装了一个类BOWKMeansExtractor[2],这一步非常简单,将所有图片的feature vector丢给这个类,然后调用cluster()就可以训练(使用KMeans方法)出指定数量(步骤介绍中提到的n)的类别。输入allDescriptors就是第1步计算得到的结果,返回的vocabulary是一千个向量,每个向量是某个类别的feature的中心点。

由于opencv封装了一个类BOWKMeansExtractor[2],这一步非常简单,将所有图片的feature vector丢给这个类,然后调用cluster()就可以训练(使用KMeans方法)出指定数量(步骤介绍中提到的n)的类别。输入allDescriptors就是第1步计算得到的结果,返回的vocabulary是一千个向量,每个向量是某个类别的feature的中心点。

1 // BuildVocabulary
2 BOWKMeansTrainer bowTrainer( wordCount );
3 Mat vocabulary = bowTrainer.cluster( allDescriptors );

3 构造bag of words

对每张图片的特征点,将其归到前面计算的类别中,统计这张图片各个类别出现的频率,作为这张图片的bag of words。由于opencv封装了BOWImgDescriptorExtractor[2]这个类,这一步也走得十分轻松,只需要把上面计算的vocabulary丢给它,然后用一张图片的特征点作为输入,它就会计算每一类的特征点的频率。

Samples这个map的key就是某个类别,value就是这个类别中所有图片的bag of words,即Mat中每一行都表示一张图片的bag of words。

01 // ComputeBowImageDescriptors
02 map<string, Mat> samples;
03 Ptr<BOWImgDescriptorExtractor> bowExtractor;
04 loop over each category {
05     loop over each image in current category {
06         Mat image = imread( filepath );
07         vector<KeyPoint> keyPoints;
08         detector -> detect( image, keyPoints );
09         Mat imageDescriptor;
10         bowExtractor -> compute( image, keyPoints, imageDescriptor );
11         samples[current category].push_back( imageDescriptor );
12     }
13 }

4 训练分类器

我使用的分类器是svm,用经典的1 vs all方法实现多类分类。对每一个类别都训练一个二元分类器。训练好后,对于待分类的feature vector,使用每一个分类器计算分在该类的可能性,然后选择那个可能性最高的类别作为这个feature vector的类别。

训练二元分类器

  • samples:第3步中得到的结果。
  • category:针对哪个类别训练分类器。
  • svmParams:训练svm使用的参数。
  • svm:针对category的分类器。

属于category的样本,label为1;不属于的为-1。准备好每个样本及其对应的label之后,调用CvSvm的train方法就可以了。

01 void TrainSvm( const map<string, Mat>& samples, 
02                const string& category, 
03                const CvSVMParams& svmParams, 
04                CvSVM* svm ) {
05     Mat allSamples( 0, samples.at( category ).cols, samples.at( category ).type() );
06     Mat responses( 0, 1, CV_32SC1 );
07     allSamples.push_back( samples.at( category ) );
08     Mat posResponses( samples.at( category ).rows, 1, CV_32SC1, Scalar::all(1) ); 
09     responses.push_back( posResponses );
10     for ( auto itr = samples.begin(); itr != samples.end(); ++itr ) {
11         if ( itr -> first == category ) {
12             continue;
13         }
14         allSamples.push_back( itr -> second );
15         Mat response( itr -> second.rows, 1, CV_32SC1, Scalar::all( -1 ) );
16         responses.push_back( response );
17         
18     }
19     svm -> train( allSamples, responses, Mat(), Mat(), svmParams );
20 }

分类

使用某张待分类图片的bag of words作为feature vector输入,使用每一类的分类器计算判为该类的可能性,然后使用可能性最高的那个类别作为这张图片的类别。

category就是结果,queryDescriptor就是某张待分类图片的bag of words。

01 // ClassifyBySvm
02 float confidence = -2.0f;
03 string category;
04 for( auto itr = samples.begin(); itr != samples.end(); ++itr ) {
05     CvSVM svm;
06     TrainSvm( samples, itr->first, svmParams, &svm );
07     float curConfidence=sign*svm.predict(queryDescriptor, true);
08     if ( curConfidence > confidence ) {
09             confidence = curConfidence;
10             category = itr -> first;
11     }
12 }

第二种方法:相关性排序

这种方法的前面1-3步和bag of words一样,只是分类的时候有些别出心裁。利用上面的类比,每张图片的bag of words就好比是词汇表中每个单词出现的频率,我们完全有理由相信相同类别的图片的频率直方图比较接近。由此受到启发,可以找出已有数据库待中与待分类的图片的最接近的图片,将该图片的类别作为待分类图片的类别。

在实现的时候,我并没有仅仅使用一张最接近的图片,而是找出数据库中最接近的9张图片,最后的结果类别就是包含这9张图片中最多张数的那一类。

01 // ClassifyByMatch 
02 struct Match{
03         string category;
04         float distance;
05 };
06 priority_queue<Match, vector<Match> > matchesMinQueue;
07 Ptr<DescriptorMatcher> histogramMatcher = new BFMatcher(normType );
08 const int numNearestMatch = 9;
09 for( auto itr = samples.begin(); itr != samples.end(); ++itr ){
10     vector<vector<DMatch> > matches;
11     histogramMatcher -> knnMatch( queryDescriptor, itr ->second, matches, numNearestMatch );
12     for ( auto itr2 = matches[0].begin(); itr2 !=matches[0].end(); ++ itr2 ) {
13         matchesMinQueue.push( Match( itr -> first, itr2 ->distance ) );
14     }
15 }

找出包含这9张图片中最多张数的那一类。

01 // ClassifyByMatch
02 string category;
03 int maxCount = 0;
04 map<string, size_t> categoryCounts;
05 size_t select = std::min( static_cast<size_t>( numNearestMatch ), matchesMinQueue.size() );
06 for ( size_t i = 0; i < select; ++i ) {
07     string& c = matchesMinQueue.top().category;
08     ++categoryCounts[c];
09     int currentCount = categoryCounts[c];
10     if ( currentCount > maxCount ) {
11         maxCount = currentCount;
12         category = c;
13     }
14     matchesMinQueue.pop();
15 }

缓存结果

该操作出现的函数: main, BuildVocabulary, ComputeBowImageDescriptors。

在第一次处理之后,我将“词汇表”,每张图片的bag of words,每个类别的svm分别保存在了(相对于结果文件夹)vocabulary.xml.gz,bagOfWords文件夹和svms文件夹中。这样下一次对某张图片进行分类的时候,就可以直接读取这些文件而不必每次都计算,训练样本很多的时候,这些计算十分耗时。

不足之处

Bag of words方法没有考虑特征点的相对位置,而每类物体大都有自己特定的结构,这方面的信息没有利用起来。用上面一贯的类比,就好像搜索引擎只使用了单词频率,而没有考虑句子一样,没有结构的分析。

效果

对于我打包在作业文件夹中的训练数据和测试数据,第一种方法有80%的图被正确分类,第二种方法有67%的图被正确分类,均高出20%的随机猜测很多。

左侧的图是使用Bag of words方法的所有结果,右侧的图是使用第二种方法的所有结果。

参考资料

[1] Csurka, Gabriella, et al. Visual categorization with bags of keypoints. Workshop on statistical learning in computer vision, ECCV. Vol. 1. 2004.

[2] http://docs.opencv.org/modules/features2d/doc/object_categorization.html

No related posts.

BoW(SIFT/SURF/...)+SVM/KNN的OpenCV 实现的更多相关文章

  1. opencv中的SIFT,SURF,ORB,FAST 特征描叙算子比较

    opencv中的SIFT,SURF,ORB,FAST 特征描叙算子比较 参考: http://wenku.baidu.com/link?url=1aDYAJBCrrK-uk2w3sSNai7h52x_ ...

  2. linux/ubuntu下最简单好用的python opencv安装教程 ( 解决 imshow, SIFT, SURF, CSRT使用问题)

    希望这篇文章能彻底帮你解决python opencv安装和使用中的常见问题. 懒人请直奔这一节, 一条命令安装 opencv 使用python-opencv常用的问题 在linux中使用python版 ...

  3. opencv3.0中contrib模块的添加+实现SIFT/SURF算法

    平台:win10 x64 +VS 2015专业版 +opencv-3.x.+CMake+Anaconda3(python3.7.0) Issue说明:Opencv3.0版本已经发布了有一段时间,在这段 ...

  4. [转]SIFT,SURF,ORB,FAST 特征提取算法比较

    转载地址:https://blog.csdn.net/vonzhoufz/article/details/46461849 主要的特征检测方法有以下几种,在一般的图像处理库中(如opencv, VLF ...

  5. 模式匹配之常见匹配算法---SIFT/SURF、haar特征、广义hough变换的特性对比分析

    识别算法概述: SIFT/SURF基于灰度图, 一.首先建立图像金字塔,形成三维的图像空间,通过Hessian矩阵获取每一层的局部极大值,然后进行在极值点周围26个点进行NMS,从而得到粗略的特征点, ...

  6. 机器学习:scikit-learn 做笑脸识别 (SVM, KNN, Logisitc regression)

    scikit-learn 是 Python 非常强大的一个做机器学习的包,今天介绍scikit-learn 里几个常用的分类器 SVM, KNN 和 logistic regression,用来做笑脸 ...

  7. 基于OpenCV全景拼接(Python)SIFT/SURF

    一.实验内容: 利用sift算法,实现全景拼接算法,将给定的两幅图片拼接为一幅. 二.实验环境: 主机配置: CPU :intel core i5-7300 2.50GHZ RAM :8.0GB 运行 ...

  8. 特征提取算法的综合实验(多种角度比较sift/surf/brisk/orb/akze)

    一.基本概念: 作用:特征点提取在"目标识别.图像拼接.运动跟踪.图像检索.自动定位"等研究中起着重要作用: 主要算法: •FAST ,Machine Learning forHi ...

  9. paper 130:MatLab分类器大全(svm,knn,随机森林等)

    train_data是训练特征数据, train_label是分类标签.Predict_label是预测的标签.MatLab训练数据, 得到语义标签向量 Scores(概率输出).1.逻辑回归(多项式 ...

随机推荐

  1. thymeleaf和artTemplate

    Company最近项目中使用了两个模板引擎,分别是Java服务器端的模板引擎Thymeleaf和前端的模板引擎artTemplate, 其实对于这两个模板引擎 理论上应该是不应该放在一起记录的,但是b ...

  2. Web Best Practices

    Web Best Practices General Google WebFundamentals - Github JavaScript Style Guide - Github Google In ...

  3. Scrapy 应用之爬取《盗墓笔记》

    爬取<盗墓笔记>和爬取<宦海沉浮>原理一样,但是使用了两种不同的追踪链接的方式,<盗墓笔记>使用的是跟踪下一页链接,直至没有下一页为止,<宦海沉浮>则是 ...

  4. PHP 获取文件名和扩展名的方法

    dirname(path) path: 代表你的文件路径,必须为绝对路径,可以使用__FILE__, 表示列出当前文件的绝对路径,包含文件名 函数会返回当前文件的上一级路径,也就是除了文件名称的路径 ...

  5. vim 第三章 插入模式

    vim 第三章  插入模式 在普通模式下可以删除  复制   及粘贴的命令    在插入模式下也存在以中方便快捷的方式    能够粘贴寄存器中文本   两种方式来插入键盘上不存在的非常用字符 替换模式 ...

  6. 第一阶段Sprint 对其他团队评价

    咱们的team 针对对“小学生网页四则运算”这个产品的评审,本人提出建议:1.第一阶段的产品Sprint不够好,无任务看板.无燃尽图.希望完善该产品的的Sprint,第二阶段的Sprint要认真写好. ...

  7. Python之回调函数

    在计算机程序设计中,回调函数,或简称回调(Callback),是指通过函数参数传递到其它代码的,某一块可执行代码的引用.这一设计允许了底层代码调用在高层定义的子程序. 有两种类型的回调函数:即bloc ...

  8. SPOJ QTREE Query on a tree V ——动态点分治

    [题目分析] QTREE4的弱化版本 建立出分治树,每个节点的堆表示到改点的最近白点距离. 然后分治树上一直向上,取min即可. 正确性显然,不用担心出现在同一子树的情况(不会是最优解),请自行脑补. ...

  9. 表单禁用复制、粘贴、及右击菜单(contextmenu、oncopy、oncut、onpaste、onselectstart)

    禁用右键菜单,可以使用oncontextmenu属性: <textarea oncontextmenu="return false"></textarea> ...

  10. 【HDOJ6223】Infinite Fraction Path(后缀数组,倍增)

    题意: 给一个长度为n的字符串s[0..n-1],但i的后继不再是i+1,而是(i*i+1)%n,求所有长度为n的“子串”中,字典序最大的是谁 n<=150000,s[i]=0..9 思路:后缀 ...