【opencv、機器學習】opencv中的SVM圖像分類(二)

上一篇博文對圖像分類理論部分作了比較詳細的講解,這一篇主要是對圖像分類代碼的實現進行分析。理論部分咱們談到了使用BOW模型,可是BOW模型如何構建以及整個步驟是怎麼樣的呢?能夠參考下面的博客http://www.cnblogs.com/yxy8023ustc/p/3369867.html,這一篇博客很詳細講解了BOW模型的步驟了,主要包含如下四個步驟:css

  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,使用訓練的分類器進行分類。 
上面整個工程步驟所涉及到的函數,我都放在一個類categorizer裏, 
下面按步驟說明具體實現,程序示例有所省略,完整的程序可看工程源碼。html

NO.一、特徵提取

對圖片特徵的提取包括對每張訓練圖片的特徵提取和每張待檢測圖片特徵的提取,我使用的是surf,因此使用opencv的SurfFeatureDetector檢測特徵點,而後再用SurfDescriptorExtractor抽取特徵點描述符。對於特徵點的檢測和特徵描述符的講解能夠參考中文opencv中文官網http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/features2d/feature_detection/feature_detection.html#feature-detection以及http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/features2d/feature_description/feature_description.html#feature-description 
我訓練圖片特徵提取的示例代碼以下:c++

Mat vocab_descriptors;

        // 對於每一幅模板,提取SURF算子,存入到vocab_descriptors中
        multimap<string,Mat> ::iterator i=train_set.begin();
        for(;i!=train_set.end();i++)
        {
            vector<KeyPoint>kp;
            Mat templ=(*i).second;
            Mat descrip;
            featureDecter->detect(templ,kp);

            descriptorExtractor->compute(templ,kp,descrip);
            //push_back(Mat);在原來的Mat的最後一行後再加幾行,元素爲Mat時, 其類型和列的數目 必須和矩陣容器是相同的
            vocab_descriptors.push_back(descrip);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

注意:上述代碼只是工程的一個很小的部分,有些變量在類中已經定義,在這裏沒有貼出來,例如上述的train_set訓練圖片的映射,定義爲:ide

//從類目名稱到訓練圖集的映射,關鍵字能夠重複出現
    multimap<string,Mat> train_set;
  • 1
  • 2
  • 1
  • 2

將每張圖片的特徵描述符存儲起來vocab_descriptors,而後爲後面聚類和構造訓練圖片詞典作準備。函數

NO.二、feature聚類

因爲opencv封裝了一個類BOWKMeansExtractor[2],這一步很是簡單,將全部圖片的feature vector丟給這個類,而後調用cluster()就能夠訓練(使用KMeans方法)出指定數量(步驟介紹中提到的n)的類別。輸入vocab_descriptors就是第1步計算獲得的結果,返回的vocab是一千個向量,每一個向量是某個類別的feature的中心點。 
示例代碼以下:測試

//將每一副圖的Surf特徵利用add函數加入到bowTraining中去,就能夠進行聚類訓練了
        bowtrainer->add(vocab_descriptors);
        // 對SURF描述子進行聚類
        vocab=bowtrainer->cluster();
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

bowtrainer的定義以下:ui

bowtrainer=new BOWKMeansTrainer(clusters);
  • 1
  • 1

NO.三、構造bag of words

對每張圖片的特徵點,將其歸到前面計算的類別中,統計這張圖片各個類別出現的頻率,做爲這張圖片的bag of words。因爲opencv封裝了BOWImgDescriptorExtractor[2]這個類,這一步也走得十分輕鬆,只須要把上面計算的vocab丟給它,而後用一張圖片的特徵點做爲輸入,它就會計算每一類的特徵點的頻率。spa

allsamples_bow這個map的key就是某個類別,value就是這個類別中全部圖片的bag of words,即Mat中每一行都表示一張圖片的bag of words。.net

//對每張圖片的特徵點,統計這張圖片各個類別出現的頻率,做爲這張圖片的bag of words
        bowDescriptorExtractor->setVocabulary(vocab);
    }

// 對於每一幅模板,提取SURF算子,存入到vocab_descriptors中
        multimap<string,Mat> ::iterator i=train_set.begin();

        for(;i!=train_set.end();i++)
        {
            vector<KeyPoint>kp;
            string cate_nam=(*i).first;
            Mat tem_image=(*i).second;
            Mat imageDescriptor;
            featureDecter->detect(tem_image,kp);

            bowDescriptorExtractor->compute(tem_image,kp,imageDescriptor);
            //push_back(Mat);在原來的Mat的最後一行後再加幾行,元素爲Mat時, 其類型和列的數目 必須和矩陣容器是相同的
            allsamples_bow[cate_nam].push_back(imageDescriptor);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上面部分變量的定義以下:調試

//存放全部訓練圖片的BOW
    map<string,Mat> allsamples_bow;
    //特徵檢測器detectors與描述子提取器extractors 泛型句柄類Ptr
    Ptr<FeatureDetector> featureDecter;
    Ptr<DescriptorExtractor> descriptorExtractor;

    Ptr<BOWKMeansTrainer> bowtrainer;
    Ptr<BOWImgDescriptorExtractor> bowDescriptorExtractor;
    Ptr<FlannBasedMatcher> descriptorMacher;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

NO.四、訓練分類器

我使用的分類器是svm,用經典的1 vs all方法實現多類分類。對每個類別都訓練一個二元分類器。訓練好後,對於待分類的feature vector,使用每個分類器計算分在該類的可能性,而後選擇那個可能性最高的類別做爲這個feature vector的類別。

訓練二元分類器

allsamples_bow:第3步中獲得的結果。 
category_name:針對哪一個類別訓練分類器。 
svmParams:訓練svm使用的參數。 
stor_svms:針對category_name的分類器。 
屬於category_name的樣本,label爲1;不屬於的爲-1。準備好每一個樣本及其對應的label以後,調用CvSvm的train方法就能夠了。

示例代碼以下:

stor_svms=new CvSVM[categories_size];
        //設置訓練參數
        SVMParams svmParams;
        svmParams.svm_type    = CvSVM::C_SVC;
        svmParams.kernel_type = CvSVM::LINEAR;
        svmParams.term_crit   = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);

        cout<<"訓練分類器..."<<endl;
        for(int i=0;i<categories_size;i++)
        {
            Mat tem_Samples( 0, allsamples_bow.at( category_name[i] ).cols, allsamples_bow.at( category_name[i] ).type() );
            Mat responses( 0, 1, CV_32SC1 );
            tem_Samples.push_back( allsamples_bow.at( category_name[i] ) );
            Mat posResponses( allsamples_bow.at( category_name[i]).rows, 1, CV_32SC1, Scalar::all(1) ); 
            responses.push_back( posResponses );

            for ( auto itr = allsamples_bow.begin(); itr != allsamples_bow.end(); ++itr ) 
            {
                if ( itr -> first == category_name[i] ) {
                    continue;
                }
                tem_Samples.push_back( itr -> second );
                Mat response( itr -> second.rows, 1, CV_32SC1, Scalar::all( -1 ) );
                responses.push_back( response );

            }

            stor_svms[i].train( tem_Samples, responses, Mat(), Mat(), svmParams );
            //存儲svm
            string svm_filename=string(DATA_FOLDER) + category_name[i] + string("SVM.xml");
            stor_svms[i].save(svm_filename.c_str());
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

對於SVM的參數以及函數調用的介紹能夠參考中文官網http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/ml/introduction_to_svm/introduction_to_svm.html#introductiontosvms

部分變量的定義以下:

// 訓練獲得的SVM
    CvSVM *stor_svms;
    //類目名稱,也就是TRAIN_FOLDER設置的目錄名
    vector<string> category_name;
    //類目數目
    int categories_size;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

NO.五、對未知圖片進行分類

分類

使用某張待分類圖片的bag of words做爲feature vector輸入,使用每一類的分類器計算判爲該類的可能性,而後使用可能性最高的那個類別做爲這張圖片的類別。

prediction_category就是結果,test就是某張待分類圖片的bag of words。示例代碼以下:

Mat input_pic=imread(train_pic_path);
        imshow("輸入圖片:",input_pic);
        cvtColor(input_pic,gray_pic,CV_BGR2GRAY);

        // 提取BOW描述子
        vector<KeyPoint>kp;
        Mat test;
        featureDecter->detect(gray_pic,kp);
        bowDescriptorExtractor->compute(gray_pic,kp,test);


float scoreValue = stor_svms[i].predict( test, true );
float classValue = stor_svms[i].predict( test, false );
sign = ( scoreValue < 0.0f ) == ( classValue < 0.0f )? 1 : -1;
curConfidence = sign * stor_svms[i].predict( test, true );
if(curConfidence>best_score)
{
    best_score=curConfidence;
    prediction_category=cate_na;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上面就是四個主要步驟的部分示例代碼,不少其餘部分代碼沒有貼出來,好比說如何遍歷文件夾下面的全部不一樣類別的圖片,由於訓練圖片的樣本比較多的話,訓練圖片是一個時間比較長久的,那麼如何在對一張待測圖片進行分類的時候,不須要每次都重複訓練樣本,而是直接讀取以前已經訓練好的BOW。。。。不少不少。

個人main函數實現以下:

int main(void)
{
    int clusters=1000;
    //初始化
    categorizer c(clusters);
    //特徵聚類
    c.bulid_vacab();
    //構造BOW
    c.compute_bow_image();
    //訓練分類器
    c.trainSvm();
    //將測試圖片分類
    c.category_By_svm();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

下面來看看個人工程部分運行結果以下: 
這裏寫圖片描述

部分分類下圖所示: 
這裏寫圖片描述
這裏寫圖片描述 
這裏寫圖片描述 
這裏寫圖片描述

左邊爲輸入圖片,右邊爲所匹配的類別模型。準確率爲百分之八九十。

個人整個工程文件以及個人全部訓練的圖片存放在這裏http://download.csdn.net/detail/always2015/8944973以及http://download.csdn.net/detail/always2015/8944959,須要的能夠下載,本身在找訓練圖片寫代碼花了不少時間,下載完後自行解壓,project data文件夾直接放在D盤就行,裏面存放訓練的圖片和待測試圖片,以及訓練過程當中生成的中間文件,另外一個文件夾object_classfication_end則是工程文件,我用的是vs2010打開便可,下面工程裏有幾個要注意的地方:

一、在這個模塊中使用到了c++的boost庫,可是在這裏有一個版本的限制。這個模塊的代碼只能在boost版本1.46以上使用,這個版本如下的就不能用了,直接運行就會出錯,這是最須要注意的。由於在1.46版本以上中對比CsSVM這個類一些成員函數作了一些私有化的修改,因此在使用該類初始化對象時候須要注意。

二、個人模塊所使用到的函數和產生的中間結果都是在一個categorizer類中聲明的,因爲不一樣的執行階段中間結果有不少個,例如:訓練圖片聚類後所獲得單詞表矩陣,svm分類器的訓練的結果等,中間結果的產生是至關耗時的,因此在剛開始就考慮到第一次運行時候把他以文件XML的格式保存下來,下次使用到的時候在讀取。將一個矩陣存入文本的時候能夠直接用輸出流的方式將一個矩陣存入,可是讀取時候若是用輸入流直接一個矩陣變量的形式讀取,那就確定報錯,由於輸入流不支持直接對矩陣的操做,因此這時候只能對矩陣的元素一個一個進行讀取了。

三、在測試的時候,若是輸入的圖片過小,或者全爲黑色,當通過特徵提取和單詞構造完成使用svm進行分類時候會出現錯誤。通過調試代碼,發現上述圖片在生成該圖片的單詞的時候所獲得的單詞矩陣會是一個空矩陣,即該矩陣的行列數都爲0,因此在使用svm分類器時候就出錯。因此在使用每一個輸入圖片的單詞矩陣的時候先作一個判斷,若是該矩陣行列數都爲0,那麼該圖片直接跳過。