首页 > 代码库 > OpenCV中的Haar+Adaboost(七):分类器训练过程

OpenCV中的Haar+Adaboost(七):分类器训练过程

  本节文章讲解OpenCV中Haar+Adaboost的训练过程。此文章假定读者已经了解前面5章的内容,包括Haar特征,弱分类器和强分类器结构,以及GAB等内容。

 

  在opencv_traincascade.exe程序中,有如下参数

技术分享

如上输入的boostParams中的6个参数决用于决定训练过程:

1. 参数bt选择Boosting类型(默认GAB),本系列文章五中已经介绍了

2. minHitRate和maxFalseAlarmRate限定训练过程中各种阈值大小,文章六已经介绍了

3. 参数weightTrimRate非核心内容,只是个小tricks,本文忽略

4. 参数maxDepth限定树状弱分类器的深度,如本系列文章三图3中的为2或图5中为1。

5. 参数maxWeakCount为每个stage中树状弱分类器最大数量,超过此数量会强制break

本节的强分类器stage训练过程将围绕这些参数展开。强烈建议读者在阅读本章节前,自行收集人脸样本并使用程序opencv_traincascade.exe训练一个简单的人脸分类器,以便理解

 

(一)样本收集过程

  首先分析每一个stage训练时如何收集样本,事实上每一个stage训练使用的正负样本都不同。

1. 正样本patches收集过程

  opencv_trancascade.exe使用的正样本是一个vec文件,即由opencv_createsamples.exe把一组固定w x h大小的图片转换为二进制vec文件(只是读取图片并转化为灰度图,并按照二进制格式保存下来而已,不做任何改变)。由于经过如此处理的正样本就是固定w x h大小的patches,所以正样本可以直接进入训练。

2. 负样本patches收集过程

  而使用的负样本就不一样了,是一个包含任意大小图片路径的txt文件。在寻找负样本的过程中,程序会以图像金字塔(pyramid)+滑动窗口的模式(sliding-window)去遍历整个负样本集,以获取w x h大小的负样本patches。

3. 对1和2步骤来中这些自的正负样本的patches进行分类

  在获取到这些w x h固定大小的正负样本patches后,利用已经训练好的stage分类这些patches,并且从正样本中收集TP patches,直到收集够numPos个;从负样本中收集FP patches,直到收集够了numNeg个(为了简化问题,假设样本是足够的)。之后利用TP patches作为正样本,FP patches作为负样本训练下一个stage。

技术分享

图1 每个Stage训练前收集样本示意图

  那么对于第0个stage,直接收集numPos个来自正样本的patches + numNeg个来自负样本的patches进行训练;对于第 i 个stage,则利用已经训练好的 0 到 i - 1 的stage分类这些patches,分别从正样本patches中收集TP,从负样本patches中收集FP(需要说的的是,这里的numPos和numNeg两个值是在opencv_traincascade.exe中设置的)。每一个stage都要进行上述收集+分类过程,所以实际中每一个stage所使用的训练样本也都不一样!

  思考1:看到这里,读者不妨思考一下为什么要用TP和FP训练,而不用FN和TN?

  作者的答案:对于正样本,FN已经被之前的stage拒绝掉了,没法挽救了,所以当前stage也所以没有必要去学习这些FN;而对于负样本,TN已经被之前的stage正确拒绝了,而FP被错误的通过了,所以才要专门学习这些FP。

  看完我的讲解之后再来看看代码,讲解一定要和代码相符。在OpenCV的cascadeclassifier.cpp中,有如下fillPassedSamples()函数负责填充训练样本(修改此函数可以实现下文提到的保存TP和FP)。

 1 int CvCascadeClassifier::fillPassedSamples( int first, int count, bool isPositive, double minimumAcceptanceRatio, int64& consumed )
 2 {
 3     int getcount = 0;
 4     Mat img(cascadeParams.winSize, CV_8UC1);
 5     for( int i = first; i < first + count; i++ )
 6     {
 7         for( ; ; )
 8         {
 9             if( consumed != 0 && ((double)getcount+1)/(double)(int64)consumed <= minimumAcceptanceRatio )
10                 return getcount;
11 
12             bool isGetImg = isPositive ? imgReader.getPos( img ) : imgReader.getNeg( img ); //读取样本
13             if( !isGetImg )
14                 return getcount; //如果不能读取样本(出错or样本消耗光了),返回
15             consumed++; //只要读取到了样本,不管判断结果如何,消耗量consumed增加1
16 
17             //当参数isPositive = true时,填充正样本队列,此时选择TP进入队列
18             //当参数isPositive = false时,填充负样本队列,此时选择FP进入队列
19             featureEvaluator->setImage( img, isPositive ? 1 : 0, i ); //将样本img塞入训练队列中
20             if( predict( i ) == 1 ) //根据isPositive判断是否是TP/FP, 是则break进入下一个;反之继续循环,并覆盖上面setImage的样本
21             {
22                 getcount++; //真正添加进训练队列的数量
23                 printf("%s current samples: %d\r", isPositive ? "POS":"NEG", getcount);
24                 break;
25             }
26         }
27     }
28     return getcount;
29 }

接下来看看第20行的predict()函数:

 1 int CvCascadeClassifier::predict( int sampleIdx )
 2 {
 3     CV_DbgAssert( sampleIdx < numPos + numNeg );
 4     for (vector< Ptr<CvCascadeBoost> >::iterator it = stageClassifiers.begin();
 5         it != stageClassifiers.end(); it++ )
 6     {
 7         if ( (*it)->predict( sampleIdx ) == 0.f )
 8             return 0;
 9     }
10     return 1;
11 }

再进入第7行的(*it)->predict()函数:

 1 float CvCascadeBoost::predict( int sampleIdx, bool returnSum = false ) const
 2 {
 3     CV_Assert( weak );
 4     double sum = 0;
 5     CvSeqReader reader;
 6     cvStartReadSeq( weak, &reader );
 7     cvSetSeqReaderPos( &reader, 0 );
 8     for( int i = 0; i < weak->total; i++ )
 9     {
10         CvBoostTree* wtree;
11         CV_READ_SEQ_ELEM( wtree, reader );
12         sum += ((CvCascadeBoostTree*)wtree)->predict(sampleIdx)->value; //stage内的弱分类器wtree输出值求和sum
13     }
14     if( !returnSum )
15         //默认进行sum和stageThreshold比较
16         //当sum<stageThreshold,输出0,否决当前样本;sum>stageThreshold,输出1,通过
17         sum = sum < threshold - CV_THRESHOLD_EPS ? 0.0 : 1.0;
18     return (float)sum; //若returnSum==true则不与stageThreshold比较,直接返回弱分类器输出之和。下文用到
19 }

看到这就很清晰了,默认returnSum为false时每个stage内部弱分类器wtree的输出值加起来和stageTheshold比较,当样本通过时输出1,不通过输出0(参考文系列文章三)。那么对于positive samples,输出1即是TP;对于negative samples,输出1即是FP。至此代码与上述内容对应,over!

 

(二)分类器的训练过程

  为了方便理解,以下章节都是以maxDepth=1为例分析训练过程,其他深度请自行分析代码。在收集到numPos个TP和numNeg个FP后,就可以训分类器了,过程如下:
 
1. 首先计算所有Haar特征对这numPos+numNeg个样本patches的特征值,排序后分别保存在的vector中,如图2
技术分享
图2 分类器训练过程示意图
2. 按照如下方式遍历每个存储特征值的vector
(1) for k= 1 : (numPos+numNeg)
     a. 阈值threshold = 0.5 * (vector[k]+vector[k+1])将vector分为left和right两部分
     b. 计算GAB的输出leftvalue和rightvalue,其中wi为样本的权重,yi为样本标签(1为本 and 0为负)
         (在训练每个stage的首次迭代中初始化wi= 1 / (numPos+numNeg))
技术分享
    c. 计算GAB WSE ERROR
技术分享
(2) 在k的遍历过程中,找到error最小的threshold作为当前vector的Best spilt,以及对应的leftvalue和rightvalue保存下来。可以看出这里的Best spilt threshold就是弱分类器阈值,与Haar特征+leftvalue+rightvalue共同构成一个完成的弱分类器。(注意在第三章中提到的:一个完整的弱分类器包含:Haar特征 + leftValue + rightValue + 弱分类器阈值)
 

下面给出一个计算弱分类器的流程图,即1-2步骤(来自该博客):

技术分享
 
3. 至此,已经有很多弱分类器了。但是哪一个弱分类器最好呢?所以要挑选最优弱分类器放进stage中。
至此,通过步骤(1)-(2)后每一个弱分类器都有一个基于Best split threshold的GAB WSE ERROR,那么显然选择ERROR最小的那个弱分类器作为最优弱分类器放进当前训练的stage中。
 
4. 依照GAB方法更新当前训练的stage中每个样本的权重
对numPos+numNeg个权重按照如下公式更新权重(注意更新后需要对权重进行归一化)。
技术分享
 
5. 计算当前的强分类器阈值stageThreshold
(1) 使用当前的stage中已经训练好的弱分类器去检测样本中的每一TP,计算弱分类器输出值之和保存在eval中(如果不明白,请查阅第三节“并联的弱分类器”)。
技术分享





(2) 对eval升序排序
(3) 利用minHitRate参数估计一个比例thresholdIdx,以eval[thresholdIdx]作为stage阈值stageThreshold,显然TP越多估计的stageThreshold越准确。
上述(1)-(3)过程由boost.cpp中的CvCascadeBoost::isErrDesired()函数实现,关键代码如下:
1 int numPos = 0;
2 for( int i = 0; i < sCount; i++ ) //遍历样本
3     if( ((CvCascadeBoostTrainData*)data)->featureEvaluator->getCls( i ) == 1.0F ) //if current sample is TP
4         eval[numPos++] = predict( i, true ); //predict加入true参数后,会返回当前stage中弱分类器输出之和(如上文predict介绍)
5 icvSortFlt( &eval[0], numPos, 0 ); //升序排序
6 int thresholdIdx = (int)((1.0F - minHitRate) * numPos); //按照minHitRate估计一个比例作为index
7 threshold = eval[ thresholdIdx ]; //取该index的值作为stageThreshold

至此,stage中的弱分类器+stageThreshold等参数都是完整的

 
6. 重复1-5步骤,直到满足下列任意一个条件后停止并输出当前的stage
(1) stage中弱分类器的数量 >= maxWeakCount参数
 
(2) 利用当前的stage去检测FP获得当前stage的falseAlarmRate,当falseAlarmRate < maxFalseAlarmRate停止
同样是boost.cpp中的CvCascadeBoost::isErrDesired()函数:
 1 int numNeg = 0;
 2 for( int i = 0; i < sCount; i++ )
 3 {
 4     if( ((CvCascadeBoostTrainData*)data)->featureEvaluator->getCls( i ) == 0.0F )
 5     {
 6         numNeg++;
 7         if( predict( i ) ) //predict==1也就是falseAlarm,虚警,即俗称的误报
 8             numFalse++;
 9     }
10 }
11 float falseAlarm = ((float) numFalse) / ((float) numNeg);
12 return falseAlarm <= maxFalseAlarm;

返回false时,停止当前stage训练。

 
7. 然后重复1-6依次训练每一个stage,直到满足下面任意一个条件:
(1) stage数量 >= numStages
(2) 所有stage总的falseAlarmRate < pow(maxFalseAlarmRate,numStages)
 

(三)一个小例子

  作者选用了约12000张人脸样本,使用opencv_traincascade.exe程序,设置numPos=numNeg=10000,w=h=24,进行一次简单的训练。如下图红线所示,在stage0的时候,程序直接从正负样本中各抽取10000张24x24大小的子图片进行训练,获得了一个包含3个决策树的强分类器。
技术分享
  在stage1的时,扫描了10008张正样本后获得了10000个TP(当前实际hitRate=10000/10008);扫描了很多负样本窗口后获得了10000个FP(比例为acceptanceRatio=0.243908,即是当前实际falseAlarmRate)。训练完成后获得一个包含5个决策树的强分类器。
技术分享
缩进可以看到,在一般情况下,随着训练的进行,acceptancesRatio会越来越低,即直观上看每一级收集的FP会越来越像正样本;那么为了区分TP与FP,每一个stage包含的决策树也会逐渐增多。This is make sense!
(注:保存FP可以通过修改fillPassedSamples()函数实现。)
 

(四)训练过程总结

其实回顾一下,整个分类器的训练过程可以分为以下几个步骤:

1. 寻找TP和FP作为训练样本

2. 计算每个Haar特征在当前权重下的Best split threshold+leftvalue+rightvalue,组成了一个个弱分类器

3. 通过WSE寻找最优的弱分类器

4. 更新权重

5. 按照minHitRate估计stageThreshold

6. 重复上述1-5步骤,直到falseAlarmRate到达要求,或弱分类器数量足够。停止循环,输出stage。

7. 进入下一个stage训练

 

到此,整个系列文章就结束了。

可以看到,相比CNN传统的Boosting方法有极强的理论基础,虽然复杂但是非常严谨。

由于weightTrim不是核心内容,所以留给读者自己探索。
另外此文主旨是分析算法,而非工具使用说明,所以不会写有关如何使用opencv_traincascade.exe内容,请读者谅解。
都看到这里了,还不点个赞?

OpenCV中的Haar+Adaboost(七):分类器训练过程