首页 > 代码库 > 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!
(二)分类器的训练过程
(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步骤(来自该博客):
至此,通过步骤(1)-(2)后每一个弱分类器都有一个基于Best split threshold的GAB WSE ERROR,那么显然选择ERROR最小的那个弱分类器作为最优弱分类器放进当前训练的stage中。
对numPos+numNeg个权重按照如下公式更新权重(注意更新后需要对权重进行归一化)。
(1) 使用当前的stage中已经训练好的弱分类器去检测样本中的每一TP,计算弱分类器输出值之和保存在eval中(如果不明白,请查阅第三节“并联的弱分类器”)。
(2) 对eval升序排序(3) 利用minHitRate参数估计一个比例thresholdIdx,以eval[thresholdIdx]作为stage阈值stageThreshold,显然TP越多估计的stageThreshold越准确。
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等参数都是完整的
(1) stage中弱分类器的数量 >= maxWeakCount参数(2) 利用当前的stage去检测FP获得当前stage的falseAlarmRate,当falseAlarmRate < maxFalseAlarmRate停止
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训练。
(1) stage数量 >= numStages(2) 所有stage总的falseAlarmRate < pow(maxFalseAlarmRate,numStages)
(三)一个小例子
(四)训练过程总结
其实回顾一下,整个分类器的训练过程可以分为以下几个步骤:
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方法有极强的理论基础,虽然复杂但是非常严谨。
OpenCV中的Haar+Adaboost(七):分类器训练过程