OpenCV探索之路(二十八):Bag of Features(BoF)影像分類實踐

weixin_30639719發表於2020-04-05

在深度學習在影像識別任務上大放異彩之前,詞袋模型Bag of Features一直是各類比賽的首選方法。首先我們先來回顧一下PASCAL VOC競賽歷年來的最好成績來介紹物體分類演算法的發展。

1093303-20171224094358006-1636907443.png

從上表我們可以發現,在2012年之前,詞袋模型是VOC競賽分類演算法的基本框架,幾乎所有演算法都是基於詞袋模型的,可以這麼說,詞袋模型在影像分類中統治了很多年。雖然現在深度學習在影像識別任務中的效果更勝一籌,但是我們也不要忘記在10年前,Bag of Features的框架曾經也引領過一個時代。那這篇文章就是要重溫BoF這個經典框架,並從實踐上看看它在影像物體分類中效果到底如何。

Bag of Features理論淺談

其實Bag of Features 是Bag of Words在影像識別領域的延伸,Bag of Words最初產生於自然處理領域,通過建模文件中單詞出現的頻率來對文件進行描述與表達。

1093303-20171224094418412-1861584910.png

詞包模型還有一個起源就是紋理檢測(texture recognition),有些影像是由一些重複的基礎紋理元素圖案所組成,所以我們也可以將這些圖案做成頻率直方圖,形成詞包模型。

1093303-20171224094433600-418663463.png

詞包模型於2004年首次被引入計算機視覺領域,由此開始大量集中於詞包模型的研究,在各類影像識別比賽中也大放異彩,逐漸形成了由下面4部分組成的標準物體分類框架:

  1. 底層特徵提取
  2. 特徵編碼
  3. 特徵匯聚
  4. 使用SVM等分類器進行分類

2005年第一屆PASCAL VOC競賽 資料庫包含了4類物體:摩托車、自行車、人、汽車,訓練集加驗證集一共684張影像,測試集包含689張影像,資料規模相對較少。從方法上說,採用“興趣點-SIFT地城特徵描述-向量量化編碼直方圖-支援向量機”得到了最好的物體分類效能,這種方法也就是我們今天所講的Bag of Features方法。

為什麼要用BOF模型描述影像?

SIFT特徵雖然也能描述一幅影像,但是每個SIFT向量都是128維的,而且一幅影像通常都包含成百上千個SIFT向量,在進行相似度計算時,這個計算量是非常大的,更重要的是,每一幅圖提取到的SIFT特徵點數目都不一樣,所以我們要將這些特徵量化(比如生成統計直方圖),這樣才能進行相似度計算。通行的做法是用聚類演算法對這些向量資料進行聚類,然後用聚類中的一個簇代表BOF中的一個視覺詞,將同一幅影像的SIFT向量對映到視覺詞序列生成碼本,這樣每一幅影像只用一個碼本向量來描述,這樣計算相似度時效率就大大提高了。

搭建Bag-of-Features的步驟:

  1. 特徵提取(在這裡我們使用很穩定的SIFT運算元)

1093303-20171224094502396-595948650.png

1093303-20171224094520365-1754313830.png

  1. K-means聚類。將第一步提取到的特徵向量及進行聚類,得出N個類心。

1093303-20171224094532865-1968535858.png

1093303-20171224094553678-1171225318.png

  1. 量化特徵,形成詞袋

1093303-20171224094613553-749101050.png

  1. 統計每一類別的視覺單詞出現頻率,形成視覺單詞直方圖

1093303-20171224094632303-348220300.png

5.訓練SVM分類器

實踐篇

要編碼實現BoF,其實只需嚴格按照上述講的步驟進行就可以了,而且OpenCV給我們準備了關於BoF的相關API,所以實現起來的難度進一步降低。現在我們要思考的的是,怎麼把opencv所提供的的這些API重新整合在一起,來構成一個分類能力還不錯的影像分類器。

今天還是以票據分類任務為例子講解BoF模型。

先觀察資料集,我們已經分出了訓練集和測試集

1093303-20171224094648037-356667374.png

每一類圖片放在不同的資料夾下面,資料夾的名字就是這個類別的label

1093303-20171224094701568-959079759.png

這是我們要分類的12種票據

1093303-20171224094720662-292386312.png

一、特徵提取

對底層特徵,我們選擇的還是最為經典的SIFT特徵,用opencv做SIFT特徵提取只需要用到幾個API就可以了。

我們還是老套路,先準備好一些提取SIFT特徵的資料結構和描述SIFT的一些類。

//create Sift feature point extracter
static Ptr<FeatureDetector> detector1(new SiftFeatureDetector());
//create Sift descriptor extractor
static Ptr<DescriptorExtractor> extractor(new SiftDescriptorExtractor);


//To store the keypoints that will be extracted by SIFT
vector<KeyPoint> keypoints;
//To store the SIFT descriptor of current image
Mat descriptor;
//To store all the descriptors that are extracted from all the images
Mat featuresUnclustered;
//The SIFT feature extractor and descriptor
SiftDescriptorExtractor detector;

然後我們對我們的訓練樣本進行遍歷,對每一類的訓練圖片進行SIFT特徵提取,並將提取出來的特徵存進featuresUnclustered裡,用於接下來的k-means聚類。

/*第一步,計算目錄下所有訓練圖片的features,放進featuresUnclustered*/
printf("step1:sift features extracting...\n");
for (int num = 1; num < MAX_TRAINING_NUM; num++)
{
    
    sprintf(filename, ".\\training\\%d\\train.txt", num);
    //首先先檢查一下該類資料夾下有沒有用於train的特徵檔案,有的話就不需要提取特徵點了
    if (_access(filename, 0) == -1)
    {
        printf("extracting features %d class\n", num);
        for (int i = 1; i <= MAX_TRAINING_NUM; i++)
        {
            sprintf(filename, ".\\training\\%d\\%d.jpg", num, i);
            //create the file name of an image
            //open the file
            input = imread(filename, CV_LOAD_IMAGE_GRAYSCALE); //Load as grayscale      
            if (input.empty())
            {
                break;
            }
            //resize:reduce keypoints numbers to accerlate
            resize(input, input, Size(), 0.5, 0.5);
            //detect feature points
            detector.detect(input, keypoints);
            printf("keypoints:%d\n", keypoints.size());
            //compute the descriptors for each keypoint
            detector.compute(input, keypoints, descriptor);
            //save descriptor to file
            char train_name[32] = { 0 };
            sprintf(train_name, ".\\training\\%d\\train.txt", num);
            WriteFeatures2File(train_name, descriptor);
            //put the all feature descriptors in a single Mat object 
            featuresUnclustered.push_back(descriptor);
            //train_features[num][i].push_back(descriptor);

        }
    }
    else
    {
        Mat descriptor;
        load_features_from_file(filename, descriptor);
        featuresUnclustered.push_back(descriptor);
    }


}

需要注意的是,我在特徵提取階段把每一類提取到的特徵都寫進了txt檔案中,只是為了以後增加類別時,我們不再需要再次遍歷提取特徵,而只需讀入我們原先存有特徵向量的txt檔案就可以了,這將大大加快訓練速度。

static int load_features_from_file(const string& file_name,Mat& features)
{
    FILE* fp = fopen(file_name.c_str(), "r");
    if (fp == NULL)
    {
        printf("fail to open %s\n", file_name.c_str());
        return -1;
    }
    printf("loading file %s\n", file_name.c_str());

    vector<float> inData;
    while (!feof(fp))
    {
        float tmp;
        fscanf(fp, "%f", &tmp);
        inData.push_back(tmp);
    }

    //vector to Mat
    int mat_cols = 128;
    int mat_rows = inData.size() / 128;
    features = Mat::zeros(mat_rows, mat_cols, CV_32FC1);
    int count = 0;
    for (int i = 0; i < mat_rows; i++)
    {
        for (int j = 0; j < mat_cols; j++)
        {
            features.at<float>(i, j) = inData[count++];
        }
    }

    return 0;
}

static int WriteFeatures2File(const string& file_name,const Mat& features)
{
    FILE* fp = fopen(file_name.c_str(), "a+");
    if (fp == NULL)
    {
        printf("fail to open %s\n", file_name.c_str());
        return -1;
    }

    for (int i = 0; i < features.rows; i++)
    {
        for (int j = 0; j < features.cols; j++)
        {
            int data = features.at<float>(i, j);
            fprintf(fp, "%d\t", data);
        }
        fprintf(fp,"\n");
    }

    fclose(fp);

    return 0;
}

二、特徵聚類

我們將上一步得到的訓練集的所有特徵進行聚類,聚類初始化方式選擇means++,類心數量選擇1000。這裡需要說明一下,聚類的類心數量是一個超引數,是一個需要反覆調整的引數,如果類心過少,那就表示BOF模型的視覺單詞數目很少,即該模型的表達能力很低,很可能在分類任務中不能區分出每一類物體(有點像Deep Learning中說的欠擬合);但類心過多,就會造成視覺單詞過於分散,很可能導致模型在泛化效果不佳(過擬合)。所以,選擇一個合理的類心數目很重要。

/*第二步,定義好聚類的中心數目,進行聚類,並得到詞典dictionary*/
printf("step2:clusting...\n");
int dictionarySize = 1000;  //類心數目,即codebook num
//define Term Criteria
TermCriteria tc(CV_TERMCRIT_ITER, 1000, 0.001);  //最大迭代1000次
//retries number
int retries = 1;
//necessary flags
int flags = KMEANS_PP_CENTERS;  //kmeans++初始化
//Create the BoW (or BoF) trainer
BOWKMeansTrainer bowTrainer(dictionarySize, tc, retries, flags);
//cluster the feature vectors
Mat dictionary = bowTrainer.cluster(featuresUnclustered);  //聚類
//store the vocabulary
FileStorage fs(".\\dictionary1.yml", FileStorage::WRITE); //將聚類後的結果寫入檔案
fs << "vocabulary" << dictionary;
fs.release();
cout << "Saving BoW dictionary\n";

這個聚類時間還是比較長的,大概需要20分鐘。

三、量化特徵,形成詞典直方圖

/*第三步,計算每個類別的詞典直方圖*/
printf("step3:generating dic histogram...\n");
//create a nearest neighbor matcher
Ptr<DescriptorMatcher> matcher(new FlannBasedMatcher);
//create Sift feature point extracter
Ptr<FeatureDetector> detector1(new SiftFeatureDetector());
//create Sift descriptor extractor
Ptr<DescriptorExtractor> extractor(new SiftDescriptorExtractor);
//create BoF (or BoW) descriptor extractor
BOWImgDescriptorExtractor bowDE(extractor, matcher);
//Set the dictionary with the vocabulary we created in the first step
bowDE.setVocabulary(dictionary);

cout << "extracting histograms in the form of BOW for each image " << endl;
Mat labels(0, 1, CV_32FC1);
Mat trainingData(0, dictionarySize, CV_32FC1);
int k = 0;
vector<KeyPoint> keypoint1;
Mat bowDescriptor1;
Mat img2;
//extracting histogram in the form of bow for each image 
for (int num = 1; num <= MAX_TRAINING_NUM; num++)
{
    for (int i = 1; i <= MAX_TRAINING_NUM; i++)
    {
        sprintf(filename, ".\\training\\%d\\%d.jpg", num,i);

        //sprintf(filename, "%d%s%d%s", j, " (", i, ").jpg");
        img2 = cvLoadImage(filename, 0);

        if (img2.empty())
        {
            break;
        }

        resize(img2, img2, Size(), 0.5, 0.5);

        detector.detect(img2, keypoint1);

        bowDE.compute(img2, keypoint1, bowDescriptor1);

        trainingData.push_back(bowDescriptor1);

        labels.push_back((float)num);
    }
}

四、訓練SVM

我們使用SVM作為分類器進行訓練,訓練好的資料以檔案的形式儲存下來,以後預測時直接讀檔案就可以還原模型了。

/*第四步,訓練SVM得到分類模型*/
printf("SVM training...\n"); 
CvSVMParams params;
params.kernel_type = CvSVM::RBF;
params.svm_type = CvSVM::C_SVC;
params.gamma = 0.50625000000000009;
params.C = 312.50000000000000;
params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.000001);
CvSVM svm;

bool res = svm.train(trainingData, labels, cv::Mat(), cv::Mat(), params);

svm.save(".\\svm-classifier1.xml");

delete[] filename;
printf("bag-of-features training done!\n");

六、預測

首先我們需要載入我們訓練好的資料(svm-classifier1.xml和dictionary1.yml)

//字典檔案、SVM訓練檔案讀入記憶體
void TrainingDataInit()
{
    FileStorage fs(".\\dictionary1.yml", FileStorage::READ);
    Mat dictionary;
    fs["vocabulary"] >> dictionary;
    fs.release();

    bowDE.setVocabulary(dictionary);

    svm.load(".\\svm-classifier1.xml");

}

然後再寫一個預測函式,用SVM實現線上分類。

//實現發票影像的分類,返回值即預測的分類結果
int invoice_classify(Mat& img)
{
    Mat img2 = img.clone();
    resize(img2, img2, Size(), 0.5, 0.5);
    cvtColor(img2, img2, CV_RGB2GRAY);
    SiftDescriptorExtractor detector;
    vector<KeyPoint> keypoint2;
    Mat bowDescriptor2;

    Mat img_keypoints_2;

    detector.detect(img2, keypoint2);

    bowDE.compute(img2, keypoint2, bowDescriptor2);

    int it = svm.predict(bowDescriptor2);

    return it;
}

現在開始測試,寫一個測試函式,讀入測試集進行預測,計算其準確率

void TestClassify()
{
    int total_count = 0;
    int right_count = 0;
    string tag;
    for (int num = 1; num < 30; num++)
    {
        for (int i = 1; i < 30; i++)
        {
            char path[128] = { 0 };
            sprintf(path, ".\\test\\%d\\%d.jpg", num, i);
            Mat img = imread(path,0);
            if (img.empty())
            {
                continue;
            }
            int type = invoice_classify(img);
            if (type == -1)
            {
                printf("reject image %s\n", path);
                continue;
            }

            total_count++;
            
            if (num == type)
            {
                tag = "CORRECT";
                right_count++;
            }
            else
            {
                tag = "WRRONG";
            }
            printf("[%s]  label: %d   predict: %d, %s\n", path, num, type, tag.c_str());
        }
    }

    printf("total image:%d  acc:%.2f\n", total_count,(float)right_count/total_count);

}

完整的流程如下:先建立BoF模型,然後更新訓練資料,將訓練引數儲存至檔案。當線上預測時,先將訓練引數讀入記憶體,再利用模型對圖片進行分類。模擬測試程式碼如下:

#include "bof.h"


int main()
{
    BuildDictionary(12,6);
    
    TrainingDataInit();
    TestClassify();

    return 0;
}

訓練:

1093303-20171224094757678-1222184700.png

預測結果:

1093303-20171224094808131-1810527984.png

可以看出,BoF模型在這種簡單分類任務的效果還可以,更重要的是我每一類只用了6張訓練樣本(小樣本集)就可以有這個效果了,如果是採用深度學習做分類,這個估計不行了。

再優化

總體而言,2005年提出來的Bag-of-Features的分類效果並不是很好,尤其是一些比較像的類別,它的區分能力還是不足的。那能不能可以做哪些優化進一步提升分類準確率呢?我覺得可以從以下幾點入手試一試:

  1. kmeans類心數目調整
  2. 增加每一類訓練圖片的數目
  3. 可以加入顏色特徵,比如顏色直方圖。個人認為這個措施會有較大效果,因為SIFT特徵點提取時,圖片已經是灰度圖了,所以顏色這個很重要的特徵並沒有用上。
  4. 加入一些全域性特徵做特徵融合,因為SIFT是區域性特徵,所以如果有一些全域性特徵作為補充的話,效果會有比較好的提升。
  5. 空間域金字塔思路(CVPR2006)

1093303-20171224094831428-122178098.png

完整的程式碼可以在我的github上獲取。

總結

在今天看來,曾經引領過一個時代的Bag-of-Features在普通分類任務上並沒有取得讓人滿意的效果,但我估計它在場景分類或影像檢索上還是會比較出色(比如地標)。現在已經全面進入深度學習的時代了,BoF的概念越來越淡出人們的視野,但BoF模型在某些應用場景還是很有潛力的。

轉載於:https://www.cnblogs.com/skyfsm/p/8097397.html

相關文章