特征检测专题
人物 | 来自 | 所作所为 |
---|---|---|
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本地下载 |
http://answers.opencv.org/question/27970/cascadeclassifierload-from-memory/
Adaboost+Haar+Opencv博客
12年的文章,感觉收集的很全了。全部看完也差不多了解了
OpenCV源码中Haar训练及特征提取的代码说明
上面几篇源码分析放到一个页面里了
还没有看到有全局调用的解释,即call graph。call graph都需要 profiling(动态)或编译(static analysis)。
好像clang++就可以输出call graph。另外codeViz+graphViz是很好的选择。有空了画一个。现在先手动吧。。。
函数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 );
}
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 p0, p1, p2, p3;
float weight;
} rect[CV_HAAR_FEATURE_MAX];
} CvFastHaarFeature;
typedef struct CvIntHaarFeatures
{
CvSize winsize;
int count;
CvTHaarFeature* feature;
CvFastHaarFeature* fastfeature;
} CvIntHaarFeatures;
其中CvTHaarFeature和CvFastHaarFeature的区别在于:CvTHaarFeature是标示特征覆盖的窗口的坐标(Cvrect r),CvFastHaarFeature是将特征覆盖的窗口区域拉直,然后计算cvEvalFastHaarFeature.
CV_INLINE float cvEvalFastHaarFeature ( const CvFastHaarFeature* feature,// 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 );
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(反正给个暴力大的数值就对了)
一、树状分类器
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;