本文轉載了文章(沈陽的博客),目的在於記錄自己重復過程中遇到的問題,和更多的人分享討論。
程序包:猛戳我
物體分類
物體分類是計算機視覺中一個很有意思的問題,有一些已經歸類好的圖片作為輸入,對一些未知類別的圖片進行預測。
下面會說明我使用OpenCV實現的兩種方法,第一種方法是經典的bag of words的實現;第二種方法基於第一種方法,但使用的分類方法有所不同。
在此之前,有必要說明一下輸入的格式,輸入訓練數據文件夾,和CalTech 101的組織類似。如下所示,每一類圖片都放在一個文件夾里,文件夾的名字就是類別的名字,不需要特別的說明文件。
test/ category1/ img01.jpg img02.jpg … category2/ img01.jpg img03.jpg … …
完整的代碼和可使用的訓練樣本可在這里找到,下面代碼示例的開頭注釋為該段代碼所在函數。
第一種方法:Bag of words
步驟描述
如[1]所言,這個方法有4個步驟:
- 提取訓練集中圖片的feature。
- 將這些feature聚成n類。這n類中的每一類就相當於是圖片的“單詞”,所有的n個類別構成“詞匯表”。我的實現中n取1000,如果訓練集很大,應增大取值。
- 對訓練集中的圖片構造bag of words,就是將圖片中的feature歸到不同的類中,然后統計每一類的feature的頻率。這相當於統計一個文本中每一個單詞出現的頻率。
- 訓練一個多類分類器,將每張圖片的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