本节研究traincascade的opencv实现.
涉及的源代码位于:
sources\apps\traincascade traincascade实现
sources\modules\ml opencv machine learning部分
sources\data\vec_files\trainingfaces_24-24.vec 正样本
http://blog.csdn.net/njzhujinhua/article/details/38377191
【1】Cascade框架
首先从main入手
int main( int argc, char* argv[] ) { CvCascadeClassifier classifier; string cascadeDirName, vecName, bgName; int numPos = 2000; int numNeg = 1000; int numStages = 20; int precalcValBufSize = 256, precalcIdxBufSize = 256; bool baseFormatSave = false; CvCascadeParams cascadeParams; CvCascadeBoostParams stageParams; PtrfeatureParams[] = { Ptr (new CvHaarFeatureParams), Ptr (new CvLBPFeatureParams), Ptr (new CvHOGFeatureParams) }; int fc = sizeof(featureParams)/sizeof(featureParams[0]); //略 classifier.train( cascadeDirName, vecName, bgName, numPos, numNeg, precalcValBufSize, precalcIdxBufSize, numStages, cascadeParams, *featureParams[cascadeParams.featureType], stageParams, baseFormatSave ); return 0; }
其中的CvCascadeParams 继承自ml的CvParams,
CvCascadeBoostParams 继承自ml的CvBoostParams后者则是CvDTreeParams的子类.
在CvCascadeBoostParams 中将boosttype设为了GENTLE,一类比较高效的AdaBoost
train的各参数:
cascadeDirName, 表示训练结果输出目录
vecName, 正样本的vec文件,由 opencv_createsamples 生成。正样本可以由包含待检测物体的一张图片生成,也可由一系列标记好的图像生成。
bgName, 背景图像的描述文件,文件中包含一系列的图像文件名,这些图像将被随机选作物体的背景
numPos, numNeg, 正负样本的个数
precalcValBufSize, 缓存大小,用于存储预先计算的特征值(feature values),单位为MB。
precalcIdxBufSize 缓存大小,用于存储预先计算的特征索引(feature indices),单位为MB。内存越大,训练时间越短。
numStages, 训练的分类器的级数
cascadeParams, 级联参数,除了默认值外,还可以通过参数指定. 其中stageType智能取值BOOST, featureType则支持haar,LBP,LOG
*featureParams[cascadeParams.featureType], 根据fratureType确定具体使用的FeatureParams
stageParams, boost分类器的参数,
-bt指定boosttype,取值
DAB=Discrete AdaBoost
RAB = Real AdaBoost,
LB = LogitBoost,
GAB = Gentle AdaBoost,默认为GENTLE AdaBoost
-minHitRate
分类器的每一级最小检测率, 默认0.995。总的检测率大约为 min_hit_rate^number_of_stages。
-maxFalseAlarmRate
分类器的每一级允许最大FPR,默认0.5。总的为 max_false_alarm_rate^number_of_stages.
-weightTrimRate
样本权重按大小序累计超过此致的样本保留进入下一轮训练. 默认0.95。 见CvBoost::trim_weights
-maxDepth
弱分类器树最大的深度。默认是1,是二叉树(stumps),只使用一个特征。
-maxWeakCount
每一级中的弱分类器的最大数目。默认100
DAB=Discrete AdaBoost
RAB = Real AdaBoost,
LB = LogitBoost,
GAB = Gentle AdaBoost,默认为GENTLE AdaBoost
-minHitRate
分类器的每一级最小检测率, 默认0.995。总的检测率大约为 min_hit_rate^number_of_stages。
-maxFalseAlarmRate
分类器的每一级允许最大FPR,默认0.5。总的为 max_false_alarm_rate^number_of_stages.
-weightTrimRate
样本权重按大小序累计超过此致的样本保留进入下一轮训练. 默认0.95。 见CvBoost::trim_weights
-maxDepth
弱分类器树最大的深度。默认是1,是二叉树(stumps),只使用一个特征。
-maxWeakCount
每一级中的弱分类器的最大数目。默认100
baseFormatSave 这个参数仅在使用Haar特征时有效。如果指定这个参数,那么级联分类器将以老的格式存储。
CvCascadeClassifier::train()概述了整个Cascade的执行过程。包括训练前的初始化,各Stage的强分类器间的样本集更新及强分类器训练都可看到其踪影,最显眼的还是其中的Stage训练的for大循环。
bool CvCascadeClassifier::train(...) { ... 读取正负样本 if ( !imgReader.create( _posFilename, _negFilename, _cascadeParams.winSize ) ) { cout << "Image reader can not be created from -vec " << _posFilename<< " and -bg " << _negFilename << "." << endl; return false; } 在指定data目录中读取已训练过的stagexml文件 if ( !load( dirName ) ) { 文件不存在则执行初始化过程, cascadeParams = _cascadeParams; 具体特征类型的创建与初始化 featureParams = CvFeatureParams::create(cascadeParams.featureType); featureParams->init(_featureParams); stageParams = new CvCascadeBoostParams; *stageParams = _stageParams; 还是采用main函数里面获取的命令行个参数取值 特征计算器 featureEvaluator = CvFeatureEvaluator::create(cascadeParams.featureType); 特征的初始化,里面生成了24*24正样本区域的162336个特征. featureEvaluator->init( (CvFeatureParams*)featureParams, numPos + numNeg, cascadeParams.winSize ); stageClassifiers.reserve( numStages ); } ...... 打印参数 PARAMETERS: cascadeDirName: . vecFileName: trainingfaces_24-24.vec bgFileName: bg.txt numPos: 100 numNeg: 120 numStages: 20 precalcValBufSize[Mb] : 256 precalcIdxBufSize[Mb] : 256 stageType: BOOST featureType: HAAR 特征类型 sampleWidth: 24 sampleHeight: 24 boostType: GAB GENTLE AdaBoost minHitRate: 0.995 maxFalseAlarmRate: 0.5 weightTrimRate: 0.95 maxDepth: 1 只有一个分支节点的二叉决策树 maxWeakCount: 100 mode: BASIC 已经从文件读取的训练过的stage int startNumStages = (int)stageClassifiers.size(); if ( startNumStages > 1 ) cout << endl << "Stages 0-" << startNumStages-1 << " are loaded" << endl; else if ( startNumStages == 1) cout << endl << "Stage 0 is loaded" << endl; double requiredLeafFARate = pow( (double) stageParams->maxFalseAlarm, (double) numStages ) / (double)stageParams->max_depth; 最终的虚警率 double tempLeafFARate; for( int i = startNumStages; i < numStages; i++ ) { cout << endl << "===== TRAINING " << i << "-stage =====" << endl; cout << "train( (CvFeatureEvaluator*)featureEvaluator, curNumSamples, _precalcValBufSize, _precalcIdxBufSize, *((CvCascadeBoostParams*)stageParams) ); cout << "END>" << endl; if(!isStageTrained) break; stageClassifiers.push_back( tempStage ); //保存阶段性的stage到独立xml文件 ............ } if(stageClassifiers.size() == 0) { cout << "Cascade classifier can't be trained. Check the used training parameters." << endl; return false; } 生成最终的xml文件 save( dirName + CC_CASCADE_FILENAME, baseFormatSave ); return true; }
在介绍AdaBoost的实现前本应该先介绍下CvCascadeClassifier::updateTrainingSet的, 但因为其中的fillPassedSamples用到了CvCascadeClassifier::predict, 所以这个我们还是后面再讲吧。
【2】AdaBoost的训练过程
AdaBoost由CvCascadeBoost类实现, 其继承自ml的CvBoost。
调用位置在CvCascadeClassifier::train中训练各Stage时调用
CvCascadeBoost* tempStage = new CvCascadeBoost; bool isStageTrained = tempStage->train( (CvFeatureEvaluator*)featureEvaluator, curNumSamples, _precalcValBufSize, _precalcIdxBufSize, *((CvCascadeBoostParams*)stageParams) );
参数含义
(CvFeatureEvaluator*)featureEvaluator, 特征提取器实现, HAAR LBP HOG
curNumSamples, 正负样本总数
_precalcValBufSize, _precalcIdxBufSize,前有说明,略
*((CvCascadeBoostParams*)stageParams) Boost实现的一些参数, 包括AdaBoost的类型GENTLE等, 前面已有说明了
在设计实现AdaBoost时最常用的弱分类器是决策树, 一般使用只含有一个节点的决策树就足够了,这就是所谓的stump(树桩).
算法描述中得到的强分类器有一系列改变权重的样本训练得到的弱分类器按一定系数的线性组合,涉及到我们现在分析的CvCascadeBoost实现便是由一系列CvCascadeBoostTree表示的弱分类器。
bool CvCascadeBoost::train(...) { ... 初始化Cascade中该级AdaBoost生成用到的训练数据。 此段比较耗时,后面分析 data = new CvCascadeBoostTrainData(...); weak = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvBoostTree*), storage ); //CvSeq* weak 多个弱分类器CvCascadeBoostTree的序列 初始化第一个弱分类器前的权值 update_weights( 0 ); do { 一个弱分类器及其训练过程 CvCascadeBoostTree* tree = new CvCascadeBoostTree; tree->train( data, subsample_mask, this ) 将训练好的弱分类器加入Seq中 cvSeqPush( weak, &tree ); 更新样本权值 update_weights( tree ); trim_weights(); if( cvCountNonZero(subsample_mask) == 0 ) break; } while( !isErrDesired() && (weak->total < params.weak_count) ); if(weak->total > 0) { data->is_classifier = true; data->free_train_data(); isTrained = true; } else clear(); return isTrained; }
其中的update_weights最开始参数为0时表示初始化权值为1.0/n
在训练完一个弱分类器后的update_weights(tree)则表示对权值的更新
void CvCascadeBoost::update_weights( CvBoostTree* tree )在权值更新流程时主要操作如下
for( int i = 0; i < n; i++ ) weak_eval->data.db[i] *= -orig_response->data.i[i]; cvExp( weak_eval, weak_eval ); for( int i = 0; i < n; i++ ) { double w = weights->data.db[i] * weak_eval->data.db[i]; weights->data.db[i] = w; sumW += w; } // renormalize weights 权值归一化 if( sumW > FLT_EPSILON ) { sumW = 1./sumW; for( int i = 0; i < n; ++i ) weights->data.db[i] *= sumW; }其中weak_eval表示弱分类器的系数, 但看实现是回归树,并不同于公式中的\alpha_k=0.5*ln((1-Ek)/Ek). 具体取值的计算还没搞明白.上述代码第二个for循环及其之前描述的是newweight[i]=oldweight[i]*exp(-weak_eval[i]*orig_response[i])最后的if是权值归一化过程,还是与文档描述很一致的.