原创作品 转载请注明出http://blog.csdn.net/always2015/article/details/47107129
上一篇博文对图像分类理论部分做了比较详细的讲解,这一篇主要是对图像分类代码的实现进行分析。理论部分我们谈到了使用BOW模型,但是BOW模型如何构建以及整个步骤是怎么样的呢?可以参考下面的博客http://www.cnblogs.com/yxy8023ustc/p/3369867.html,这一篇博客很详细讲解了BOW模型的步骤了,主要包含以下四个步骤:
对于未知类别的图片,计算它的bag of words,使用训练的分类器进行分类。
上面整个工程步骤所涉及到的函数,我都放在一个类categorizer里,
下面按步骤说明具体实现,程序示例有所省略,完整的程序可看工程源码。
对图片特征的提取包括对每张训练图片的特征提取和每张待检测图片特征的提取,我使用的是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
我训练图片特征提取的示例代码如下:
Mat vocab_descriptors;
// 对于每一幅模板,提取SURF算子,存入到vocab_descriptors中
multimap<string,Mat> ::iterator i=train_set.begin();
for(;i!=train_set.end();i++)
{
vector 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);
}
注意:上述代码只是工程的一个很小的部分,有些变量在类中已经定义,在这里没有贴出来,例如上述的train_set训练图片的映射,定义为:
//从类目名称到训练图集的映射,关键字可以重复出现
multimap<string,Mat> train_set;
将每张图片的特征描述符存储起来vocab_descriptors,然后为后面聚类和构造训练图片词典做准备。
由于opencv封装了一个类BOWKMeansExtractor[2],这一步非常简单,将所有图片的feature vector丢给这个类,然后调用cluster()就可以训练(使用KMeans方法)出指定数量(步骤介绍中提到的n)的类别。输入vocab_descriptors就是第1步计算得到的结果,返回的vocab是一千个向量,每个向量是某个类别的feature的中心点。
示例代码如下:
//将每一副图的Surf特征利用add函数加入到bowTraining中去,就可以进行聚类训练了
bowtrainer->add(vocab_descriptors);
// 对SURF描述子进行聚类
vocab=bowtrainer->cluster();
bowtrainer的定义如下:
bowtrainer=new BOWKMeansTrainer(clusters);
对每张图片的特征点,将其归到前面计算的类别中,统计这张图片各个类别出现的频率,作为这张图片的bag of words。由于opencv封装了BOWImgDescriptorExtractor[2]这个类,这一步也走得十分轻松,只需要把上面计算的vocab丢给它,然后用一张图片的特征点作为输入,它就会计算每一类的特征点的频率。
allsamples_bow这个map的key就是某个类别,value就是这个类别中所有图片的bag of words,即Mat中每一行都表示一张图片的bag of words。
//对每张图片的特征点,统计这张图片各个类别出现的频率,作为这张图片的bag of words
bowDescriptorExtractor->setVocabulary(vocab);
}
// 对于每一幅模板,提取SURF算子,存入到vocab_descriptors中
multimap<string,Mat> ::iterator i=train_set.begin();
for(;i!=train_set.end();i++)
{
vector 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);
}
上面部分变量的定义如下:
//存放所有训练图片的BOW
map<string,Mat> allsamples_bow;
//特征检测器detectors与描述子提取器extractors 泛型句柄类Ptr
Ptr featureDecter;
Ptr descriptorExtractor;
Ptr bowtrainer;
Ptr bowDescriptorExtractor;
Ptr descriptorMacher;
我使用的分类器是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<<"训练分类器..."<;
for(int i=0;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());
}
对于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;
分类
使用某张待分类图片的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 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;
}
上面就是四个主要步骤的部分示例代码,很多其他部分代码没有贴出来,比如说如何遍历文件夹下面的所有不同类别的图片,因为训练图片的样本比较多的话,训练图片是一个时间比较长久的,那么如何在对一张待测图片进行分类的时候,不需要每次都重复训练样本,而是直接读取之前已经训练好的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;
}
左边为输入图片,右边为所匹配的类别模型。准确率为百分之八九十。