经典的HaarTraining算法

1. Haar与OpenCV

特征检测专题


人物 来自 所作所为
Bradley, David Princeton Univ. Haar classifier for profile faces
Kruppa, Hannes ETH Zurich Haar classifier for fullbody, lowerbody, upperbody detection.
Schiele, Bernt ETH Zurich Haar classifier for fullbody, lowerbody, upperbody detection.


简单介绍与描述 作者 版本 Haar cascade文件
Frontal Face stump 24x24, 20x20gentle, 20x20tree Rainer Lienhart 1.0 frontalFace10.zip本地下载
Profile Face (20x20) David Bradley 1.0 profileFace10.zip本地下载
Human body, Pedestrian Detection

14x28 full body, 19x23 lower body, 22x18 upper body

David Bradley 1.0 body10.zip本地下载
Frontal eyes (both eyes) Unknown Ref. to author & rights is welcome Old cascade format frontalEyes35x16.zip本地下载
Frontal eyes (both eyes) Yusuf Bediz New cascade Format XML Converted w/HaarConv frontalEyes35x16_-_[XML.zip (此文件无法下载)]
Right Eye 18x12 Modesto Castrillón 1.0 REye18x12.zip本地下载
Left Eye 18x12 Modesto Castrillón 1.0 LEye18x12.zip本地下载
Frontal Eyes 22x5 Modesto Castrillón 1.0 Eyes22x5.zip本地下载
Mouth 25x15 Modesto Castrillón 1.0 Mouth25x15.zip本地下载
Nose 25x15 Modesto Castrillón 1.0 Nose25x15.zip本地下载


2. 网络资源

Viola Jones face detection and tracking explained

http://answers.opencv.org/question/27970/cascadeclassifierload-from-memory/


Adaboost+Haar+Opencv博客

12年的文章,感觉收集的很全了。全部看完也差不多了解了


OpenCV源码中Haar训练及特征提取的代码说明

上面几篇源码分析放到一个页面里了


3. 源码分析

还没有看到有全局调用的解释,即call graph。call graph都需要 profiling(动态)或编译(static analysis)。

好像clang++就可以输出call graph。另外codeViz+graphViz是很好的选择。有空了画一个。现在先手动吧。。。


3.1 opencv haartraining 分析三:icvCreateCARTStageClassifier (cvhaartraining.cpp

函数icvCreateCARTStageClassifier负责训练一个强分类器 


trainer = cvBoostStartTraining( &data->cls, weakTrainVals, &data->weights,
                 sampleIdx, boosttype );//data->cls 一个1*m的矩阵,元素为0.0或1.0,分别代表背景和有行人,本程序sampleIdx = NULL


cart = (CvCARTClassifier*) cvCreateCARTClassifier( data->valcache,
                        flags,
                        weakTrainVals, 0, 0, 0, trimmedIdx,
                        &(data->weights),
                        (CvClassifierTrainParams*) &trainParams );//开始构建cart树弱分类器

        classifier = (CvCARTHaarClassifier*) icvCreateCARTHaarClassifier( numsplits ); //初始化

        icvInitCARTHaarClassifier( classifier, cart, haarFeatures );//把caar和haarFeature数据放到classifier中

if( falsealarm > maxfalsealarm )
    {
        stage = NULL;
    }
    else
    {
        stage = (CvStageHaarClassifier*) icvCreateStageHaarClassifier( seq->total,
                                                                       threshold );
        cvCvtSeqToArray( seq, (CvArr*) stage->classifier );
    }

3.2  opencv haartraining 分析一:cvCreateTreeCascadeClassifier(cvhaartraining.cpp


haar_features = icvCreateIntHaarFeatures( winsize, mode, symmetric );//这个是计算haar特征的数目以及相关参数(计算每个特征的计算公式),

//CvTreeCascadeNode包含CvStageHaarClassifier* stage;也就是说找最后一个stage作为最深的叶leaf;
CV_CALL( leaves = icvFindDeepestLeaves( tcc ) );

CV_CALL( icvPrintTreeCascade( tcc->root ) );
//根据模式和对称性以及winsize获得haar特征,每个特征由最多三个矩形矩形加减形成,
//这里包含了所有的允许特征,允许特征是可能特征的一部分,过滤掉的是面积比较小的特征
haar_features = icvCreateIntHaarFeatures( winsize, mode, symmetric );

printf( "Number of features used : %d\n", haar_features->count );
//分配用于训练的缓冲区,包括正负样本的矩形积分图和倾斜积分图

typedef struct CvTHaarFeature

{

    char desc[CV_HAAR_FEATURE_DESC_MAX];

    int  tilted;

    struct

    {

        CvRect r;

        float weight;

    } rect[CV_HAAR_FEATURE_MAX];

CvTHaarFeature;

 

typedef struct CvFastHaarFeature

{

    int tilted;

    struct

    {

        int p0p1p2p3;

        float weight;

    } rect[CV_HAAR_FEATURE_MAX];

CvFastHaarFeature;

 

typedef struct CvIntHaarFeatures

{

    CvSize winsize;

    int count;

    CvTHaarFeaturefeature;

    CvFastHaarFeaturefastfeature;

CvIntHaarFeatures;

其中CvTHaarFeature和CvFastHaarFeature的区别在于:CvTHaarFeature是标示特征覆盖的窗口的坐标(Cvrect r),CvFastHaarFeature是将特征覆盖的窗口区域拉直,然后计算cvEvalFastHaarFeature.

CV_INLINE float cvEvalFastHaarFeature ( const CvFastHaarFeature* feature,
                                                                             const sum_type* sum, const sum_type* tilted )
{
        const sum_type* img = feature->tilted ? tilted : sum;//此处img是判断是否是旋转后的。如果不是,那么这个是已经计算了每个位置的像素积分和的。

// CvMat normfactor;
// CvMat cls;
// CvMat weights;
training_data = icvCreateHaarTrainingData( winsize, npos + nneg );
sprintf( stage_name, "%s/", dirname );
suffix = stage_name + strlen( stage_name );
//获得背景信息,包括读取背景信息里背景文件的文件名信息并索引该文件,


//读取正样本,并计算通过所有的前面的stage的正采样数量,这样可以计算出检测率
//调用函数情况
//icvGetHaarTrainingDataFromVec内部调用icvGetHaarTrainingData
//icvGetHaarTrainingData,从规定的回调函数里icvGetHaarTraininDataFromVecCallback获得数据,
//并通过前面训练出的分类器过滤掉少量正采样,然后计算积分图,
//积分图存放在training_data结构中
poscount = icvGetHaarTrainingDataFromVec( training_data, 0, npos,
(CvIntHaarClassifier*) tcc, vecfilename, &consumed );


//读负采样,并返回虚警率
//从文件中将负采样读出,并用前面的训练出的stage进行过滤获得若干被错误划分为正采样的负采样,如果得到的数量达不到nneg
//则会重复提取这些负样本,以获得nneg个负采样,所以如果当被错误划分为正采样的负采样在当前的stage后为0,则会出现死循环
//解决方法可以通过reader的round值判断。
//这种情况应该是训练收敛,因为虚警率为0,符合条件if( leaf_fa_rate <= required_leaf_fa_rate ),可以考虑退出训练
nneg = (int) (neg_ratio * poscount);
//icvGetHaarTrainingDataFromBG内部调用
//icvGetBackgroundImage获得数据并计算积分图,将其放在training_data结构分配的内存,位置是在poscount开始nneg个数量
//training_data分配了npos + nneg个积分图内存以及权值
negcount = icvGetHaarTrainingDataFromBG( training_data, poscount, nneg,
(CvIntHaarClassifier*) tcc, &false_alarm );
printf( "NEG: %d %g\n", negcount, false_alarm );


icvSetNumSamples( training_data, poscount + negcount );
posweight = (equalweights) ? 1.0F / (poscount + negcount) : (0.5F/poscount);
negweight = (equalweights) ? 1.0F / (poscount + negcount) : (0.5F/negcount);
//这里将正样本设置为1,负样本设置为0,在后面将用于分辨样本类型,统计检测率和虚警率
//这里也设置加权值
icvSetWeightsAndClasses( training_data,
poscount, posweight, 1.0F, negcount, negweight, 0.0F );


//预先计算每个样本(包括正负样本的前面numprecalculated特征)
//内部调用cvGetSortedIndices将所有样本的每一个特征按其特征值升序排序,idx和特征值分别放在training_data
//的 *data->valcache和*data->idxcache中;
icvPrecalculate( training_data, haar_features, numprecalculated );


/训练由多个弱分类器级连的强分类器
single_cluster->stage =
(CvStageHaarClassifier*) icvCreateCARTStageClassifier


printf( "Cluster: %d\n", cluster );
last_pos = negcount;
//将分类好的正样本根据cluster与负样本组合,则训练出k个node,
//与前面不一样的是正样本放后面负样本放前面
//重新计算加权
icvSetWeightsAndClasses( training_data,
poscount, posweight, 1.0F, negcount, negweight, 0.0F );


3.3 另一篇不错的独立分析

1.结构:
程序的总体结构是一棵多叉树,每个节点多少个叉由初始设定的maxtreesplits决定

树节点结构:
typedef struct CvTreeCascadeNode
{
CvStageHaarClassifier* stage; // 指向该节点stage强分类器的指针

struct CvTreeCascadeNode* next; // 指向同层下一个节点的指针
struct CvTreeCascadeNode* child; // 指向子节点的指针
struct CvTreeCascadeNode* parent; // 指向父节点的指针

struct CvTreeCascadeNode* next_same_level;//最后一层叶节点之间的连接
struct CvTreeCascadeNode* child_eval; //用于连接最终分类的叶节点和根节点
int idx; //表示该节点是第几个节点
int leaf; //从来没有用到过的参数
} CvTreeCascadeNode;

这里需要说明的是child_eval这个指针,虽说人脸检测是一个单分类问题,程序中的maxtreesplits的设置值为0,没有分叉,但是树本身 是解决多分类问题的,它有多个叶节点,也就有多个最终的分类结果。但是我们使用的时候,虽然是一个多分类的树,也可能我们只需要判断是或者不是某一类。于 是我们就用root_eval和child_eval把这个分类上的节点索引出来,更方便地使用树结构。当然,这一点在本程序中是没有体现的。

分类器结构:
每个树节点中都包含了一个CvStageHaarClassifier强分类器,而每个CvStageHaarClassifier包含了多个 CvIntHaarClassifier弱分类器。当CvIntHaarClassifier被使用的时候,被转化为 CvCARTHaarClassifier,也就是分类树与衰减数分类器作为一个弱分类器。

typedef struct CvCARTHaarClassifier
{
CV_INT_HAAR_CLASSIFIER_FIELDS()

int count;
int* compidx; //特征序号
CvTHaarFeature* feature; //选出的特征。数组
CvFastHaarFeature* fastfeature;

float* threshold;
int* left;
int* right;
float* val;
} CvCARTHaarClassifier;

CvCARTHaarClassifier结构中包含了弱分类器的左值右值阈值等数组,在我们的程序中CART只选用了一个特征进行分类,即退化成了stump。这里的数组里面就只存有一个元了

那么这里为什么要使用一个如此复杂的结构呢。大体来说有两个好处:
1、 方便弱分类器之间的切换,当我们不选用CART而是其他的弱分类器结构的时候,就可以调用CvIntHaarClassifier时转换成其他的指针
2、 这样方便了Haar训练的过程和Boost过程的衔接。

特征的结构:


2.OpenCV的HaarTraining程序中一种常用的编程方法:
在这个程序中,函数指针是一种很常用的手法。函数指针的转换使读程序的人更难把握程序的脉络,在这里举一个最极端的例子,来说明程序中这种手法的应用。

我们在cvBoost.cpp文件中的cvCreateMTStumpClassifier函数(这是一个生成多阈值(Multi-threshold)stump分类器的函数)下看到了一个这样的调用:
findStumpThreshold_16s[stumperror](……….)
这里对应的stumperror值是2

在cvboost.cpp中我们找到了一个这样的数组
CvFindThresholdFunc findStumpThreshold_16s[4] = {
icvFindStumpThreshold_misc_16s,
icvFindStumpThreshold_gini_16s,
icvFindStumpThreshold_entropy_16s,
icvFindStumpThreshold_sq_16s
};
这个数组的类型是一个类型定义过的函数指针typedef int (*CvFindThresholdFunc)(…..)

因此这个数组中的四项就是四个指针,我们在cvCreateMTStumpClassifier中调用的也就是其中的第三项icvFindStumpThreshold_entropy_16s。

然后我们发现这个函数指针没有直接的显性的实现。那么问题出在哪里呢?
它是通过宏实现的:
程序中定义了一个这样的宏:
#define ICV_DEF_FIND_STUMP_THRESHOLD_SQ( suffix, type )
ICV_DEF_FIND_STUMP_THRESHOLD( sq_##suffix, type,


curlerror = wyyl + curleft * curleft * wl - 2.0F * curleft * wyl;
currerror = (*sumwyy) - wyyl + curright * curright * wr - 2.0F * curright * wyr;
)

和一个这样的宏:
#define ICV_DEF_FIND_STUMP_THRESHOLD( suffix, type, error )
CV_BOOST_IMPL int icvFindStumpThreshold_##suffix(…..)
{
……..
}

这两个宏中,后者是函数的主体部分,而函数的定义通过前者完成。即:
ICV_DEF_FIND_STUMP_THRESHOLD_ENTROPY( 16s, short ),这样的形式完成。这相当于给前者的宏传递了两个参数,前者的宏将第一个参数转换成sq_16s后和第二个参数一起传到后者的宏。(##是把前后两个 string连接到一起,string是可变的两,在这里suffix就放入了16s和sq_结合成了sq_16s)
后者的宏接收到参数以后就进行了函数的定义:
CV_BOOST_IMPL int icvFindStumpThreshold_sq_16s

这样icvFindStumpThreshold_sq_16s就被定义了。这样做的好处是,12个非常相似的函数可以通过两个宏和12个宏的调用来实现,而不需要直接定义12个函数。

3.训练结果中数据的含义:
- <feature>
- <rects>
<_>6 4 12 9 -1.</_>
//矩阵。前四个数值是矩阵四个点的位置,最后一个数值是矩阵像素和的权值
<_>6 7 12 3 3.</_>
//矩阵。前四个数值是矩阵四个点的位置,最后一个是像素和的权值,这样两个矩阵就形成了一个Haar特征
</rects>
<tilted>0</tilted> //是否是倾斜的Haar特征
</feature>
<threshold>-0.0315119996666908</threshold> //阈值
<left_val>2.0875380039215088</left_val> //小于阈值时取左值
<right_val>-2.2172100543975830</right_val> //大于阈值时取右值


4. 训练过程中使用的算法
这里主要讲弱分类器算法

•矩形特征值:Value[i][j], 1≤i≤n代表所有的Haar特征,1≤j≤m代表所有的样本
•FAULT = (curlerror + currerror)表示当前分类器的错误率的最小值,初始设置:curlerror currerror= 1000000000000000000000000000000000000000000000000(反正给个暴力大的数值就对了)


3.4 又一篇不错的独立分析

一、树状分类器
1、构造一棵决策树CvCARTClassifier,树状分类器
//层次关系:CvCARTClassifier CvCARTNode CvStumpClassifier
//CvCARTClassifier由count个CvCARTNode组成,每个CvCARTNode有一个CvStumpClassifier,

CvClassifier* cvCreateCARTClassifier( CvMat* trainData,//所有样本的所有特征值
int flags, //标识矩阵按行或列组织
CvMat* trainClasses,
CvMat* typeMask,
CvMat* missedMeasurementsMask,
CvMat* compIdx,
CvMat* sampleIdx,//选择部分样本时的样本号
CvMat* weights,
CvClassifierTrainParams* trainParams )


#define CV_CLASSIFIER_FIELDS() \
int flags; \
float(*eval)( struct CvClassifier*, CvMat* ); \
void (*tune)( struct CvClassifier*, CvMat*, int flags, CvMat*, CvMat*, CvMat*, \
CvMat*, CvMat* ); \
int (*save)( struct CvClassifier*, const char* file_name ); \
void (*release)( struct CvClassifier** );

typedef struct CvClassifier
{
CV_CLASSIFIER_FIELDS()
} CvClassifier;

typedef struct CvCARTNode
{
CvMat* sampleIdx;
CvStumpClassifier* stump;
int parent; //父节点索引号
int leftflag; //1:left节点时;0:right节点
float errdrop;//剩余的误差
} CvCARTNode;

//一个弱分类器,所用特征的索引、阈值及哪侧为正样本
typedef struct CvStumpClassifier
{
CV_CLASSIFIER_FIELDS()
int compidx; //对应特征的索引

float lerror;
float rerror;

float threshold; //该特征阈值
float left; //均值或左侧正样本比例,left=p(y=1)/(p(y=1)+p(y=-1)),对分类器若left为正样本则为1,反之为0
float right;
} CvStumpClassifier;

typedef struct CvCARTClassifier
{
CV_CLASSIFIER_FIELDS()
int count;
int* compidx;
float* threshold;
int* left;//当前节点的左子节点为叶节点时,存叶节点序号的负值(从0开始);非叶节点,存该节点序号
int* right;//当前节点的右子节点为叶节点时,存叶节点序号的负值(从0开始);非叶节点,存该节点序号

float* val;//存叶节点的stump->left或stump->right,值为正样本比例p(y=1)/(p(y=1)+p(y=-1))
} CvCARTClassifier;

typedef struct CvCARTTrainParams
{
CV_CLASSIFIER_TRAIN_PARAM_FIELDS()

int count;//节点数
CvClassifierTrainParams* stumpTrainParams;
CvClassifierConstructor stumpConstructor;


//定义了函数指针变量,变量名为splitIdx,将样本按第compidx个特征的threshold分为left和right
void (*splitIdx)( int compidx, float threshold,
CvMat* idx, CvMat** left, CvMat** right,
void* userdata );
void* userdata;
} CvCARTTrainParams;
2、用树状分类器进行检测
//sample只是一个样本,判断该样本在CvCARTClassifier树中的那个叶节点上
//返回该样本所在叶节点的正样本比例p(y=1)/(p(y=1)+p(y=-1))
float cvEvalCARTClassifier( CvClassifier* classifier, CvMat* sample )

//根据树状分类器判断样本在树上的位置,即在哪个叶节点上。返回叶节点序号
float cvEvalCARTClassifierIdx( CvClassifier* classifier, CvMat* sample )


二、boost
1、boost过程流程,将各种类型归为一个函数cvBoostStartTraining/cvBoostNextWeakClassifier,
通过参数区分不同类型的boost。都是在最优弱分类器已知,各样本对该分类器估计值已计算存入weakEvalVals
typedef struct CvBoostTrainer
{
CvBoostType type;
int count;
int* idx;//要么null,要么存样本索引号
float* F;//存logiBoost的F
} CvBoostTrainer;

调用顺序:cvBoostStartTraining ———> startTraining[type] ———> icvBoostStartTraining等
//定义函数cvBoostStartTraining
CvBoostTrainer* cvBoostStartTraining( ...,CvBoostType type )
{
return startTraining[type]( trainClasses, weakTrainVals, weights, sampleIdx, type );
}
//定义函数指针类型的数组变量startTraining[4]
CvBoostStartTraining startTraining[4] = {
icvBoostStartTraining,
icvBoostStartTraining,
icvBoostStartTrainingLB,
icvBoostStartTraining
};
//定义函数指针类型CvBoostStartTraining
typedef CvBoostTrainer* (*CvBoostStartTraining)( CvMat* trainClasses,
CvMat* weakTrainVals,
CvMat* weights,
CvMat* sampleIdx,
CvBoostType type );

调用顺序:cvBoostNextWeakClassifier———> nextWeakClassifier[trainer->type]———> icvBoostNextWeakClassifierLB等
//定义函数cvBoostNextWeakClassifier
float cvBoostNextWeakClassifier( ..., CvBoostTrainer* trainer )
{
return nextWeakClassifier[trainer->type]( weakEvalVals, trainClasses,weakTrainVals, weights, trainer);
}
//定义函数指针类型的数组变量nextWeakClassifier[4]
CvBoostNextWeakClassifier nextWeakClassifier[4] = {
icvBoostNextWeakClassifierDAB,
icvBoostNextWeakClassifierRAB,
icvBoostNextWeakClassifierLB,
icvBoostNextWeakClassifierGAB
};
//定义函数指针类型CvBoostNextWeakClassifier
typedef float (*CvBoostNextWeakClassifier)( CvMat* weakEvalVals,
CvMat* trainClasses,
CvMat* weakTrainVals,
CvMat* weights,
CvBoostTrainer* data );

2、具体的startTraining和NextWeakClassifier
//y*=2y-1,类别标签由{0,1}变为{-1,1},并将它填入weakTrainVals
//返回CvBoostTrainer,其中F = NULL;
CvBoostTrainer* icvBoostStartTraining( CvMat* trainClasses,//类别标签{0,1},
CvMat* weakTrainVals,//类别标签{-1,1},
CvMat* weights,
CvMat* sampleIdx,//要么null,要么存样本索引号
CvBoostType type )

//更新权重,特征的响应函数值weakEvalVals已知,即分类器已确定,分类结果在weakEvalVals
float icvBoostNextWeakClassifierDAB( CvMat* weakEvalVals,//响应函数值{1,-1}
CvMat* trainClasses,//类别标签{0,1},
CvMat* weakTrainVals,//没使用,应该是{1,-1}
CvMat* weights, //将被更新
CvBoostTrainer* trainer )//用于确定要被更新权重的样本

//更新Real AdaBoost权重,特征的响应函数值weakEvalVals已知,即分类器已确定
float icvBoostNextWeakClassifierRAB( CvMat* weakEvalVals,//响应函数值,应该是测对率=(测对数/总数)
CvMat* trainClasses,//类别标签{0,1},
CvMat* weakTrainVals,//没使用
CvMat* weights, //被更新,w=w*[exp(-1/2*log(evaldata/1-evaldata))]
CvBoostTrainer* trainer )//用于确定要被更新的样本

//样本数,权重,类别标签,响应函数值F,z值,样本索引
//由F计算LogitBoost的w和z,z返回到traindata
void icvResponsesAndWeightsLB( int num, uchar* wdata, int wstep,
uchar* ydata, int ystep, //类别标签
uchar* fdata, int fstep, //响应函数值F
uchar* traindata, int trainstep,//用于存z
int* indices ) //样本索引

//初始F=0;得p=1/2,计算w、z
CvBoostTrainer* icvBoostStartTrainingLB( CvMat* trainClasses,//类别标签{0,1},
CvMat* weakTrainVals, //存Z值
CvMat* weights,
CvMat* sampleIdx,//要么null,要么存样本索引号
CvBoostType type )

//已知f,先算F=F+f,再算p=1/(1+exp(-F)),再算z,w
float icvBoostNextWeakClassifierLB( CvMat* weakEvalVals,//f,是对z的回归
CvMat* trainClasses,//类别标签{0,1}
CvMat* weakTrainVals,//存Z值
CvMat* weights,
CvBoostTrainer* trainer )

//Gentle AdaBoost,已知f,算w=w*exp(-yf)
CV_BOOST_IMPL
float icvBoostNextWeakClassifierGAB( CvMat* weakEvalVals,//f=p(y=1|x)-p(y=-1|x)
CvMat* trainClasses,//类别标签{0,1}
CvMat* weakTrainVals,//没使用
CvMat* weights,
CvBoostTrainer* trainer )

typedef struct CvTreeCascadeNode
{
CvStageHaarClassifier* stage; //与节点对应的分类器

struct CvTreeCascadeNode* next;
struct CvTreeCascadeNode* child;
struct CvTreeCascadeNode* parent;

struct CvTreeCascadeNode* next_same_level;
struct CvTreeCascadeNode* child_eval;
int idx;
int leaf;
} CvTreeCascadeNode;



你可能感兴趣的:(经典的HaarTraining算法)