zoukankan      html  css  js  c++  java
  • BoW(SIFT/SURF/...)+SVM/KNN的OpenCV 实现

    本文转载了文章(沈阳的博客),目的在于记录自己重复过程中遇到的问题,和更多的人分享讨论。

    程序包:猛戳我

    物体分类

    物体分类是计算机视觉中一个很有意思的问题,有一些已经归类好的图片作为输入,对一些未知类别的图片进行预测。

    下面会说明我使用OpenCV实现的两种方法,第一种方法是经典的bag of words的实现;第二种方法基于第一种方法,但使用的分类方法有所不同。

    在此之前,有必要说明一下输入的格式,输入训练数据文件夹,和CalTech 101的组织类似。如下所示,每一类图片都放在一个文件夹里,文件夹的名字就是类别的名字,不需要特别的说明文件。

    test/
        category1/
            img01.jpg
            img02.jpg
            …
        category2/
            img01.jpg
            img03.jpg
            …
        …
    

    完整的代码和可使用的训练样本可在这里找到,下面代码示例的开头注释为该段代码所在函数。

    第一种方法:Bag of words

    步骤描述

    如[1]所言,这个方法有4个步骤:

    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,使用训练的分类器进行分类。

    下面按步骤说明具体实现,程序示例有所省略,完整的程序可看源码,我已经很努力地压缩了代码量,而没有降低可读性。

    1 提取feature

    这一步比较简单,对训练集中的每一张图片,使用opencv的FeatureDetector检测特征点,然后再用DescriptorExtractor抽取特征点描述符。

    01 // BuildVocabulary
    02 Mat allDescriptors; 
    03 loop over each category {
    04     loop over each image in current category {
    05         Mat image = imread( filepath );
    06         vector<KeyPoint> keyPoints;
    07         Mat descriptors;
    08         detector -> detect( image, keyPoints);
    09         extractor -> compute( image, keyPoints, descriptors );
    10         allDescriptors.push_back( descriptors );    
    11     }
    12 }

    2 feature聚类

    由于opencv封装了一个类BOWKMeansExtractor[2],这一步非常简单,将所有图片的feature vector丢给这个类,然后调用cluster()就可以训练(使用KMeans方法)出指定数量(步骤介绍中提到的n)的类别。输入allDescriptors就是第1步计算得到的结果,返回的vocabulary是一千个向量,每个向量是某个类别的feature的中心点。

    由于opencv封装了一个类BOWKMeansExtractor[2],这一步非常简单,将所有图片的feature vector丢给这个类,然后调用cluster()就可以训练(使用KMeans方法)出指定数量(步骤介绍中提到的n)的类别。输入allDescriptors就是第1步计算得到的结果,返回的vocabulary是一千个向量,每个向量是某个类别的feature的中心点。

    1 // BuildVocabulary
    2 BOWKMeansTrainer bowTrainer( wordCount );
    3 Mat vocabulary = bowTrainer.cluster( allDescriptors );

    3 构造bag of words

    对每张图片的特征点,将其归到前面计算的类别中,统计这张图片各个类别出现的频率,作为这张图片的bag of words。由于opencv封装了BOWImgDescriptorExtractor[2]这个类,这一步也走得十分轻松,只需要把上面计算的vocabulary丢给它,然后用一张图片的特征点作为输入,它就会计算每一类的特征点的频率。

    Samples这个map的key就是某个类别,value就是这个类别中所有图片的bag of words,即Mat中每一行都表示一张图片的bag of words。

    01 // ComputeBowImageDescriptors
    02 map<string, Mat> samples;
    03 Ptr<BOWImgDescriptorExtractor> bowExtractor;
    04 loop over each category {
    05     loop over each image in current category {
    06         Mat image = imread( filepath );
    07         vector<KeyPoint> keyPoints;
    08         detector -> detect( image, keyPoints );
    09         Mat imageDescriptor;
    10         bowExtractor -> compute( image, keyPoints, imageDescriptor );
    11         samples[current category].push_back( imageDescriptor );
    12     }
    13 }

    4 训练分类器

    我使用的分类器是svm,用经典的1 vs all方法实现多类分类。对每一个类别都训练一个二元分类器。训练好后,对于待分类的feature vector,使用每一个分类器计算分在该类的可能性,然后选择那个可能性最高的类别作为这个feature vector的类别。

    训练二元分类器

    • samples:第3步中得到的结果。
    • category:针对哪个类别训练分类器。
    • svmParams:训练svm使用的参数。
    • svm:针对category的分类器。

    属于category的样本,label为1;不属于的为-1。准备好每个样本及其对应的label之后,调用CvSvm的train方法就可以了。

    01 void TrainSvm( const map<string, Mat>& samples, 
    02                const string& category, 
    03                const CvSVMParams& svmParams, 
    04                CvSVM* svm ) {
    05     Mat allSamples( 0, samples.at( category ).cols, samples.at( category ).type() );
    06     Mat responses( 0, 1, CV_32SC1 );
    07     allSamples.push_back( samples.at( category ) );
    08     Mat posResponses( samples.at( category ).rows, 1, CV_32SC1, Scalar::all(1) ); 
    09     responses.push_back( posResponses );
    10     for ( auto itr = samples.begin(); itr != samples.end(); ++itr ) {
    11         if ( itr -> first == category ) {
    12             continue;
    13         }
    14         allSamples.push_back( itr -> second );
    15         Mat response( itr -> second.rows, 1, CV_32SC1, Scalar::all( -1 ) );
    16         responses.push_back( response );
    17         
    18     }
    19     svm -> train( allSamples, responses, Mat(), Mat(), svmParams );
    20 }

    分类

    使用某张待分类图片的bag of words作为feature vector输入,使用每一类的分类器计算判为该类的可能性,然后使用可能性最高的那个类别作为这张图片的类别。

    category就是结果,queryDescriptor就是某张待分类图片的bag of words。

    01 // ClassifyBySvm
    02 float confidence = -2.0f;
    03 string category;
    04 for( auto itr = samples.begin(); itr != samples.end(); ++itr ) {
    05     CvSVM svm;
    06     TrainSvm( samples, itr->first, svmParams, &svm );
    07     float curConfidence=sign*svm.predict(queryDescriptor, true);
    08     if ( curConfidence > confidence ) {
    09             confidence = curConfidence;
    10             category = itr -> first;
    11     }
    12 }

    第二种方法:相关性排序

    这种方法的前面1-3步和bag of words一样,只是分类的时候有些别出心裁。利用上面的类比,每张图片的bag of words就好比是词汇表中每个单词出现的频率,我们完全有理由相信相同类别的图片的频率直方图比较接近。由此受到启发,可以找出已有数据库待中与待分类的图片的最接近的图片,将该图片的类别作为待分类图片的类别。

    在实现的时候,我并没有仅仅使用一张最接近的图片,而是找出数据库中最接近的9张图片,最后的结果类别就是包含这9张图片中最多张数的那一类。

    01 // ClassifyByMatch 
    02 struct Match{
    03         string category;
    04         float distance;
    05 };
    06 priority_queue<Match, vector<Match> > matchesMinQueue;
    07 Ptr<DescriptorMatcher> histogramMatcher = new BFMatcher(normType );
    08 const int numNearestMatch = 9;
    09 for( auto itr = samples.begin(); itr != samples.end(); ++itr ){
    10     vector<vector<DMatch> > matches;
    11     histogramMatcher -> knnMatch( queryDescriptor, itr ->second, matches, numNearestMatch );
    12     for ( auto itr2 = matches[0].begin(); itr2 !=matches[0].end(); ++ itr2 ) {
    13         matchesMinQueue.push( Match( itr -> first, itr2 ->distance ) );
    14     }
    15 }

    找出包含这9张图片中最多张数的那一类。

    01 // ClassifyByMatch
    02 string category;
    03 int maxCount = 0;
    04 map<string, size_t> categoryCounts;
    05 size_t select = std::min( static_cast<size_t>( numNearestMatch ), matchesMinQueue.size() );
    06 for ( size_t i = 0; i < select; ++i ) {
    07     string& c = matchesMinQueue.top().category;
    08     ++categoryCounts[c];
    09     int currentCount = categoryCounts[c];
    10     if ( currentCount > maxCount ) {
    11         maxCount = currentCount;
    12         category = c;
    13     }
    14     matchesMinQueue.pop();
    15 }

    缓存结果

    该操作出现的函数: main, BuildVocabulary, ComputeBowImageDescriptors。

    在第一次处理之后,我将“词汇表”,每张图片的bag of words,每个类别的svm分别保存在了(相对于结果文件夹)vocabulary.xml.gz,bagOfWords文件夹和svms文件夹中。这样下一次对某张图片进行分类的时候,就可以直接读取这些文件而不必每次都计算,训练样本很多的时候,这些计算十分耗时。

    不足之处

    Bag of words方法没有考虑特征点的相对位置,而每类物体大都有自己特定的结构,这方面的信息没有利用起来。用上面一贯的类比,就好像搜索引擎只使用了单词频率,而没有考虑句子一样,没有结构的分析。

    效果

    对于我打包在作业文件夹中的训练数据和测试数据,第一种方法有80%的图被正确分类,第二种方法有67%的图被正确分类,均高出20%的随机猜测很多。

    左侧的图是使用Bag of words方法的所有结果,右侧的图是使用第二种方法的所有结果。

    clip_image001clip_image002

    参考资料

    [1] Csurka, Gabriella, et al. Visual categorization with bags of keypoints. Workshop on statistical learning in computer vision, ECCV. Vol. 1. 2004.

    [2] http://docs.opencv.org/modules/features2d/doc/object_categorization.html

  • 相关阅读:
    freemark生成静态网页乱码问题
    使用JedisCluster出现异常:java.lang.NumberFormatException
    [程序员代码面试指南]第9章-一种消息接收并打印的结构(链表)
    [程序员代码面试指南]字符串问题-最小包含子串的长度
    [程序员代码面试指南]二叉树问题-判断t1树是否包含t2树的全部拓扑结构、[LeetCode]572. 另一个树的子树
    [程序员代码面试指南]二叉树问题-在二叉树中找到两个节点的最近公共祖先、[LeetCode]235. 二叉搜索树的最近公共祖先(BST)(非递归)
    [Codeforces1174B]Ehab Is an Odd Person
    [CF571B]Minimization(贪心+DP)
    [HDU2577]How to Type(DP)
    [POJ1050]To the Max(最大子段和)
  • 原文地址:https://www.cnblogs.com/yxy8023ustc/p/3369867.html
Copyright © 2011-2022 走看看