BoW(SIFT/SURF/...)+SVM/KNN的OpenCV 实现

来源:互联网 发布:2015年淘宝的销售额 编辑:程序博客网 时间:2024/05/16 04:57

本文转载自:http://www.tuicool.com/articles/VnARzi


物体分类

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

下面会说明我使用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. 对训练集中的图片
  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的中心点。

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方法的所有结果,右侧的图是使用第二种方法的所有结果。

clip_image001clip_image002

参考资料

[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

0 0