本文将介绍opencv_haartraining.exe中训练分类器的核心方法cvCreateTreeCascadeClassifier()中参数的具体含义,以及具体实现代码附加详细的注释。最后给出运行截图以作代码阅读的参考
我们还是从具体的例子出发,以一些实际的参数帮助我们理解算法。
一个示例的训练命令,这是我在训练分类器时使用的:
opencv_haartraining.exe -data xml -vecpos.vec -bg neg_image.txt -nstages 20 -nsplits 2 -minhitrate 0.999-maxfalsealarm 0.5 -npos 800 -nneg 2500 -w 24 -h 24 -mem 1024 -mode ALL
haartraining.cpp的main函数与createsamples.cpp类似,不再赘述。
与createsamples.cpp一样,haartraining.cpp调用了一个opencv库中的核心方法——cvCreateTreeCascadeClassifier()
首先结合我实际使用的方法参数对其进行解释
dirname xml 创建xml中间文件的位置
vecfilename pos.vec bgfilename neg_image.txt
npos 800 每一阶段训练所使用的正样本数 nneg 2500
nstages 20 阶段数
numprecalculated 1024 预留的内存 表示允许使用计算机的1280M内存
numsplits 2 弱分类器二叉决策树的分裂数 1表示使用简单stump 分类(只有一个树桩)
minhitrate 0.999 每一阶段所期望的每阶段最小击中率
maxfalsealarm 0.5 每一阶段所期望的最大错误虚警率
weightfraction 默认值0.95 权重调整参数
mode ALL(2) 表示使用haar特征集的种类既有垂直的,又有45度角旋转的
对应表 {“BASIC”,“CORE”,“ALL”}
symmetric 默认值1 表示是否假设训练的目标为垂直对称
equalweights 默认值0 表示是否初始化所有的样本为相同的权重
winwidth 24 样本宽度 winheight 24
boosttype 应用的boost(提升)算法的种类 默认值3(GAB) 对应表{ "DAB","RAB", "LB", "GAB" }
stumperror 如果使用的提升算法是DiscreteAdaBoost,使用的错误类型
默认值0(misclass)对应表{ "misclass", "gini", "entropy" }
maxtreesplits 默认值0 maximum number of nodes in tree. Ifmaxtreesplits < nsplits, tree will not be built 我对这个参数的含义还不明确,但是在人脸识别的应用中,这个参数一般是默认值0,可以暂且不关心。也希望有大牛看到的话可以给我讲解
minpos 默认值500 这个也可以暂且不管
bg_vecfile 默认值false
下面是它的具体实现,沿用http://blog.sina.com.cn/s/blog_5f853eb10100sdgn.html这篇博文中的方法,我只给出关键部分代码,来让整个方法更加简洁,可读性更高。(还有就是这篇博文的注释非常详细,推荐大家看,我在学习时这篇文章对我帮助很大)
注:由于只给出了关键代码,所以有些方法中的参数的定义部分略去了,这并不会影响代码的理解。
void cvCreateTreeCascadeClassifier( const char* dirname,const char* vecfilename,const char* bgfilename,int npos, int nneg, int nstages,int numprecalculated,int numsplits,float minhitrate, float maxfalsealarm,float weightfraction,int mode, int symmetric,int equalweights,int winwidth, int winheight,int boosttype, int stumperror,int maxtreesplits, int minpos, bool bg_vecfile )
{//几个关键的指针CvTreeCascadeClassifier* tcc = NULL; //最终得分类器CvIntHaarFeatures* haar_features = NULL; //记录所有的haar特征CvHaarTrainingData* training_data = NULL; //训练样本数据内存区CV_FUNCNAME( "cvCreateTreeCascadeClassifier" );
//opencv宏: CV_FUNCNAME 定义变量 cvFuncName存放函数名,用于出错时可以报告出错的函数__BEGIN__;
// opencv宏: __BEGIN__ 和__END__配套使用,当出现error时,EXITint i, k;CvTreeCascadeNode* leaves; //代表上一次迭代后的分类器中最后一个强分类器int best_num, cur_num;CvSize winsize; //样本大小char stage_name[PATH_MAX];int total_splits;int poscount;int negcount;int consumed;double false_alarm;double proctime;int nleaves; //叶子结点个数double required_leaf_fa_rate; //叶子虚警率float neg_ratio;int max_clusters;max_clusters = CV_MAX_CLUSTERS; //值为3neg_ratio = (float) nneg / npos; //负样本比重nleaves = 1 + MAX( 0, maxtreesplits );
required_leaf_fa_rate = pow( (double) maxfalsealarm, (double) nstages ) / nleaves;
//最大虚警率的nstages次方,再除以叶子总数即为叶子虚警率printf( "Required leaf false alarm rate: %g\n", required_leaf_fa_rate );// CV_CALL和OPENCV_CALL代替了调用函数和检查状态两个步骤CV_CALL( tcc = (CvTreeCascadeClassifier*)icvLoadTreeCascadeClassifier( dirname, winwidth + 1, &total_splits ) );
// icvLoadTreeCascadeClassifier方法从中间文件dirname中读取已经存在的分类器,一般情况//下我们要训练分类器,并没有已经存在的分类器,所以读到的为null//获取最深的叶子及最后一个训练出的强分类器,因为下一个新的强分类器要接在这个节点的后//面,所以要有一个对当前最深的叶子节点的引用
CV_CALL( leaves = icvFindDeepestLeaves( tcc ) );//将当前分类器的情况打印出来CV_CALL( icvPrintTreeCascade( tcc->root ) );//创建haar特征,构造出所有满足指定参数要求的Haar特征,并分配所需的存储空间
haar_features = icvCreateIntHaarFeatures( winsize, mode, symmetric );printf( "Number of features used : %d\n", haar_features->count );training_data = icvCreateHaarTrainingData( winsize, npos + nneg );/*
创建训练样本数据,分配用于训练的缓冲区
包括正负样本的矩形积分图和倾斜积分图
为赋值
*/if( nstages > 0 ){/* width-first search in the tree */do //第一层循环,分类器训练到指定阶数或满足最小击中率最大虚警率要求后退出{CvTreeCascadeNode* parent;CvTreeCascadeNode* cur_node;CvTreeCascadeNode* last_node;parent = leaves;leaves = NULL;do //第二层循环,循环直至训练出满足要求的强分类器{float posweight, negweight;double leaf_fa_rate;if( parent ) sprintf( buf, "%d", parent->idx );else sprintf( buf, "NULL" );printf( "\nParent node: %s\n\n", buf );printf( "*** 1 cluster ***\n" );/*这里是为了后面样本的过滤方法icvGetHaarTrainingDataFromVec的使用。用来过滤出能够通过前面各个stage强分类器的正负样本。*/tcc->eval = icvEvalTreeCascadeClassifierFilter;/* find path from the root to the node <parent>
设置从根节点到叶子节点的路径*/icvSetLeafNode( tcc, parent );/* load samples
读取正样本,并用tcc->eval计算通过所有前面的stage的正样本数量,用来计算出检测率。并通过前面训练出的分类器过滤掉少量正采样(被认为是非人脸的正样本,即漏识的正样本,因为几率非常小)然后计算积分图
Consume是遍寻过的正样本数,所以poscount/consume就是当前人脸识别率
*/consumed = 0;poscount = icvGetHaarTrainingDataFromVec( training_data, 0, npos,(CvIntHaarClassifier*) tcc, vecfilename, &consumed );printf( "POS: %d %d %f\n", poscount, consumed, ((double) poscount)/consumed );proctime = -TIME( 0 );/*
从文件中将负样本读出,使用前面训练出的分类器进行过滤获得若干被错误划分为正样本的负采样。如果得到的数量达不到neg,则会重复提取这些负样本,以获得足够负样本。因此,当被错分的负样本为0,就会出现死循环,此时训练认为已经收敛,可以退出。
*/nneg = (int) (neg_ratio * poscount);negcount = icvGetHaarTrainingDataFromBG( training_data, poscount, nneg,(CvIntHaarClassifier*) tcc, &false_alarm, bg_vecfile ? bgfilename : NULL );printf( "NEG: %d %g\n", negcount, false_alarm );printf( "BACKGROUND PROCESSING TIME: %.2f\n", (proctime + TIME( 0 )) );if( negcount <= 0 )CV_ERROR( CV_StsError, "Unable to obtain negative samples" );leaf_fa_rate = false_alarm;if( leaf_fa_rate <= required_leaf_fa_rate ) //小于最低虚警率,退出{printf( "Required leaf false alarm rate achieved. ""Branch training terminated.\n" );}else if( nleaves == 1 && tcc->next_idx == nstages ) {
//达到设定的stages退出printf( "Required number of stages achieved. ""Branch training terminated.\n" );}else{//需要训练新一个强分类器CvTreeCascadeNode* single_cluster;CvTreeCascadeNode* multiple_clusters;int single_num;icvSetNumSamples( training_data, poscount + negcount );posweight = (equalweights) ? 1.0F / (poscount + negcount) : (0.5F/poscount);negweight = (equalweights) ? 1.0F / (poscount + negcount) : (0.5F/negcount);//设置每个样本的权重和学习信息(用0,1代表)icvSetWeightsAndClasses( training_data,poscount, posweight, 1.0F, negcount, negweight, 0.0F );/* precalculate feature values
预先计算每个样本的所有特征的特征值,对每一特征,内部调用cvGetSortedIndices,将所有正负样本按特征值升序排序,idx和特征值分别放在training_data的valcache和idxcache中。*/proctime = -TIME( 0 );icvPrecalculate( training_data, haar_features, numprecalculated );printf( "Precalculation time: %.2f\n", (proctime + TIME( 0 )) );/* train stage classifier using all positive samples
训练由多个弱分类器级联的强分类器*/CV_CALL( single_cluster = icvCreateTreeCascadeNode() );fflush( stdout );proctime = -TIME( 0 );single_cluster->stage =(CvStageHaarClassifier*) icvCreateCARTStageClassifier(training_data, NULL, haar_features,minhitrate, maxfalsealarm, symmetric,weightfraction, numsplits, (CvBoostType) boosttype,(CvStumpError) stumperror, 0 );printf( "Stage training time: %.2f\n", (proctime + TIME( 0 )) );single_num = icvNumSplits( single_cluster->stage );best_num = single_num;best_clusters = 1;multiple_clusters = NULL;printf( "Number of used features: %d\n", single_num );if( maxtreesplits >= 0 ){max_clusters = MIN( max_clusters, maxtreesplits - total_splits + 1 );}/* try clustering */vals = NULL;for( k = 2; k <= max_clusters; k++ ){//这段循环中包含大段代码,但在训练无分支的级联分类器中并不需要//用到,为了方便理解就略去了} /* try different number of clusters */cvReleaseMat( &vals );
}
//…} while( parent );//… } while( leaves );/* save the cascade to xml file …*/
}
最后想补充的是,对于这个方法,我最开始看了好久还是一知半解,后来发现一个小技巧。利用代码中的printf输出的内容以及实际执行的结果,先把代码运行的流程捋一捋,思路将会清晰许多。所以后面附上我程序运行时的截图,方便大家使用
其中有个表格的参数要解释下:
BACKGROUNGPROCESSING TIME 是负样本切割时间,一般会占用很长的时间
N 为训练层数
%SMP样本占总样本个数
ST.THR阈值,
HR 击中率,
FA 虚警,只有当每一层训练的FA低于你的命令中声明的maxfalsealarm数值才会进入下一层训练
EXP.ERR经验错误率