opencv中的SVM图像分类(二)

原创作品 转载请注明出http://blog.csdn.net/always2015/article/details/47107129

上一篇博文对图像分类理论部分做了比较详细的讲解,这一篇主要是对图像分类代码的实现进行分析。理论部分我们谈到了使用BOW模型,但是BOW模型如何构建以及整个步骤是怎么样的呢?可以参考下面的博客http://www.cnblogs.com/yxy8023ustc/p/3369867.html,这一篇博客很详细讲解了BOW模型的步骤了,主要包含以下四个步骤:

  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里,
下面按步骤说明具体实现,程序示例有所省略,完整的程序可看工程源码。

NO.1、特征提取

对图片特征的提取包括对每张训练图片的特征提取和每张待检测图片特征的提取,我使用的是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++)
        {
            vectorkp;
            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,然后为后面聚类和构造训练图片词典做准备。

NO.2、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();

bowtrainer的定义如下:

bowtrainer=new BOWKMeansTrainer(clusters);

NO.3、构造bag of words

对每张图片的特征点,将其归到前面计算的类别中,统计这张图片各个类别出现的频率,作为这张图片的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++)
        {
            vectorkp;
            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;

NO.4、训练分类器

我使用的分类器是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;

NO.5、对未知图片进行分类

分类

使用某张待分类图片的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描述子
        vectorkp;
        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;
}

下面来看看我的工程部分运行结果如下:
opencv中的SVM图像分类(二)_第1张图片

部分分类下图所示:
opencv中的SVM图像分类(二)_第2张图片
opencv中的SVM图像分类(二)_第3张图片
opencv中的SVM图像分类(二)_第4张图片
opencv中的SVM图像分类(二)_第5张图片

左边为输入图片,右边为所匹配的类别模型。准确率为百分之八九十。

我的整个工程文件以及我的所有训练的图片存放在这里http://download.csdn.net/detail/always2015/8944973以及http://download.csdn.net/detail/always2015/8944959,需要的可以下载,自己在找训练图片写代码花了很多时间,下载完后自行解压,project data文件夹直接放在D盘就行,里面存放训练的图片和待测试图片,以及训练过程中生成的中间文件,另一个文件夹object_classfication_end则是工程文件,我用的是vs2010打开即可,下面工程里有几个要注意的地方:

1、在这个模块中使用到了c++的boost库,但是在这里有一个版本的限制。这个模块的代码只能在boost版本1.46以上使用,这个版本以下的就不能用了,直接运行就会出错,这是最需要注意的。因为在1.46版本以上中对比CsSVM这个类一些成员函数做了一些私有化的修改,所以在使用该类初始化对象时候需要注意。

2、我的模块所使用到的函数和产生的中间结果都是在一个categorizer类中声明的,由于不同的执行阶段中间结果有很多个,例如:训练图片聚类后所得到单词表矩阵,svm分类器的训练的结果等,中间结果的产生是相当耗时的,所以在刚开始就考虑到第一次运行时候把他以文件XML的格式保存下来,下次使用到的时候在读取。将一个矩阵存入文本的时候可以直接用输出流的方式将一个矩阵存入,但是读取时候如果用输入流直接一个矩阵变量的形式读取,那就肯定报错,因为输入流不支持直接对矩阵的操作,所以这时候只能对矩阵的元素一个一个进行读取了。

3、在测试的时候,如果输入的图片太小,或者全为黑色,当经过特征提取和单词构造完成使用svm进行分类时候会出现错误。经过调试代码,发现上述图片在生成该图片的单词的时候所得到的单词矩阵会是一个空矩阵,即该矩阵的行列数都为0,所以在使用svm分类器时候就出错。所以在使用每个输入图片的单词矩阵的时候先做一个判断,如果该矩阵行列数都为0,那么该图片直接跳过。

你可能感兴趣的:(【opencv应用】)