BoW(SIFT/SURF/...)+SVM/KNN的OpenCV 實現


本文轉載了文章(沈陽的博客),目的在於記錄自己重復過程中遇到的問題,和更多的人分享討論。

程序包:猛戳我

物體分類

物體分類是計算機視覺中一個很有意思的問題,有一些已經歸類好的圖片作為輸入,對一些未知類別的圖片進行預測。

下面會說明我使用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方法的所有結果,右側的圖是使用第二種方法的所有結果。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM