在深度学习在图像识别任务上大放异彩之前,词袋模型 Bag of Features 一直是各类比赛的首选方法。首先我们先来回顾一下 PASCAL VOC 竞赛历年来的最好成绩来介绍物体分类算法的发展。
从上表我们可以发现,在 2012 年之前,词袋模型是 VOC 竞赛分类算法的基本框架,几乎所有算法都是基于词袋模型的,可以这么说,词袋模型在图像分类中统治了很多年。虽然现在深度学习在图像识别任务中的效果更胜一筹,但是我们也不要忘记在 10 年前,Bag of Features 的框架曾经也引领过一个时代。那这篇文章就是要重温 BoF 这个经典框架,并从实践上看看它在图像物体分类中效果到底如何。
其实 Bag of Features 是 Bag of Words 在图像识别领域的延伸,Bag of Words 最初产生于自然处理领域,通过建模文档中单词出现的频率来对文档进行描述与表达。
词包模型还有一个起源就是纹理检测(texture recognition), 有些图像是由一些重复的基础纹理元素图案所组成,所以我们也可以将这些图案做成频率直方图,形成词包模型。
词包模型于 2004 年首次被引入计算机视觉领域,由此开始大量集中于词包模型的研究,在各类图像识别比赛中也大放异彩,逐渐形成了由下面 4 部分组成的标准物体分类框架:
2005 年第一届 PASCAL VOC 竞赛 数据库包含了 4 类物体:摩托车、自行车、人、汽车,训练集加验证集一共 684 张图像,测试集包含 689 张图像,数据规模相对较少。从方法上说,采用 "兴趣点 - SIFT 地城特征描述 - 向量量化编码直方图 - 支持向量机" 得到了最好的物体分类性能,这种方法也就是我们今天所讲的 Bag of Features 方法。
为什么要用 BOF 模型描述图像?
SIFT 特征虽然也能描述一幅图像,但是每个 SIFT 矢量都是 128 维的,而且一幅图像通常都包含成百上千个 SIFT 矢量,在进行相似度计算时,这个计算量是非常大的,更重要的是,每一幅图提取到的 SIFT 特征点数目都不一样,所以我们要将这些特征量化(比如生成统计直方图),这样才能进行相似度计算。通行的做法是用聚类算法对这些矢量数据进行聚类,然后用聚类中的一个簇代表 BOF 中的一个视觉词,将同一幅图像的 SIFT 矢量映射到视觉词序列生成码本,这样每一幅图像只用一个码本矢量来描述,这样计算相似度时效率就大大提高了。
搭建 Bag-of-Features 的步骤:
5. 训练 SVM 分类器
要编码实现 BoF,其实只需严格按照上述讲的步骤进行就可以了,而且 OpenCV 给我们准备了关于 BoF 的相关 API,所以实现起来的难度进一步降低。现在我们要思考的的是,怎么把 opencv 所提供的的这些 API 重新整合在一起,来构成一个分类能力还不错的图像分类器。
今天还是以票据分类任务为例子讲解 BoF 模型。
先观察数据集,我们已经分出了训练集和测试集
每一类图片放在不同的文件夹下面,文件夹的名字就是这个类别的 label
这是我们要分类的 12 种票据
对底层特征,我们选择的还是最为经典的 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得到分类模型*/
- 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;
- }
训练:
预测结果:
可以看出,BoF 模型在这种简单分类任务的效果还可以,更重要的是我每一类只用了 6 张训练样本(小样本集)就可以有这个效果了,如果是采用深度学习做分类,这个估计不行了。
总体而言,2005 年提出来的 Bag-of-Features 的分类效果并不是很好,尤其是一些比较像的类别,它的区分能力还是不足的。那能不能可以做哪些优化进一步提升分类准确率呢?我觉得可以从以下几点入手试一试:
完整的代码可以在 我的 github 上获取。
在今天看来,曾经引领过一个时代的 Bag-of-Features 在普通分类任务上并没有取得让人满意的效果,但我估计它在场景分类或图像检索上还是会比较出色(比如地标)。现在已经全面进入深度学习的时代了,BoF 的概念越来越淡出人们的视野,但 BoF 模型在某些应用场景还是很有潜力的。
来源: http://www.cnblogs.com/skyfsm/p/8097397.html