OpenCV 目标检测与级联分类器的建立( Object Detect )
2023-03-23 ccc
在一般的目标检测中,级联分类器是基本的分类器,其中包括haar、hog、lbp等算法形成的分类器。然而,所谓的分类器实际上就是一个使用特定算法生成的xml文件,比如:haarcascade_frontalface_alt.xml使用haar算法训练生成的人脸检测分类器文件。
我们可以使用opencv提供的开源算法库,自己组织分类器的采样图片,生成不同目的的分类器文件,比如轿车目标检测、轨道线目标检测等,任何你想要的检测的目标都可以采样生成指定目标检测的分类器文件,使用这些分类器文件,就可以在实际应用中检测指定的目标并给出目标在场景中的位置。
怎样才能生成自己的分类器文件呢,下面各节将结合opencv给出的程序代码示例详细讲述建立级联分类器的步骤和构造分类器采样训练集的过程。
注意:本章内容和代码均来自opencv2.4.9版代码,其中或许会有一些实际应用的改动。程序建立在VS2010的c++上,采用win32编程框架,简单的窗口操作代替了控制台。
一、级联分类器采样训练集的建立
在opencv的D:\Program Files (x86)\opencv\sources\apps\haartraining文件夹下给出了Haar算法的级联分类器构造程序,其中createsamples.cpp代码给出了建立分类器采样训练集的基本过程:
a、使用单个图片建立训练采样(训练图片,正采样,包含目标的采样图片)
使用的函数:cvCreateTrainingSamples
b、使用单个图片建立测试采样(背景图片,负采样,不包含目标的采样图片)
使用的函数:cvCreateTestSamples
c、使用图片集(包含训练图片集和背景图片集的采样图片集)建立采样集(包含正负采样)
使用的函数:cvCreateTrainingSamplesFromInfo
注意,级联分类器的建立过程分为采样训练集的建立和级联分类器的建立,采样训练集的建立是收集和整理目标图片的过程。这个过程收集各种不同视角的采样目标(同类目标),比如人脸目标,需要收集各个族群的面孔图片,各种光照、环境条件、颜色空间的场景图片,通过建立采样过程随机变形转换,生成训练集。
在建立采样集的过程中可以使用函数cvShowVecSamples来查看采样集中包含的采样图片。
二、使用 cvShowVecSamples 查看建立采样集文件*.vec中的图片
这个函数在D:\Program Files (x86)\opencv\sources\apps\haartraining路径下的cvsamples.cpp模块中,定义在cvhaartraining.h头文件下。首先声明一个vec类型的文件对象CvVecFile,并用给定的vec文件的路径文件名打开该对象:
CvVecFile file;
file.input = fopen ( vecfilename, "rb" );
然后读vec的头信息:
fread ( &file.count, sizeof( file.count ), 1, file.input );
fread ( &file.vecsize, sizeof( file.vecsize ), 1, file.input );
fread ( &tmp, sizeof( tmp ), 1, file.input );
fread ( &tmp, sizeof( tmp ), 1, file.input );
其中:file.count为文件中的图片总数,file.vecsize为图片尺寸,一般为width * height。
然后根据提供的图片宽高测试是否与文件的file.vecsize尺寸一致。
调用函数:
icvGetHaarTraininDataFromVecCallback( sample, &file );
从文件中顺序读出图片矢量(CV_8UC1类型)。sample是CvMat类型,可使用Mat(sample)转换成Mat类型进行操作。然后使用显示图片窗口显示图片,下面是使用ROI方式贴出的图片组:
从图片组中可以看到每一个面孔类包含10个不同的图片,这10个图片是由一个图像通过createsamples程序随机变换生成的,因此,只需要提供一个原图片,系统则可生成参数指定的不同变形的图片。因此在做特定的应用目标检测时,提供采样图片类是关键,比如车辆、人员、动物等。
三、建立级联分类器(单图片训练文件vec )
建立级联分类器的训练采样文件*.vec,需要如下参数:
char* filename, //采样数据保存的vec文件名(全路径文件名)
const char* imgfilename, //生成采样数据的图像文件名
int bgcolor, //采样时用的背景颜色,一般为0
int bgthreshold, //背景颜色范围阈值,生成采样图像的屏蔽区域
const char* bgfilename, //采样图像的背景贴图文件名
int count, //生成采样图像的个数(随机变形采样图像)
int invert, //生成负片效果的采样
int maxintensitydev, //生成采样图片的最大密度漂移
double maxxangle, //生成变形采样数据的最大转角(x方向)
double maxyangle, //生成变形采样数据的最大转角(y方向)
double maxzangle, //生成变形采样数据的最大转角(z方向)
int showsamples, //是否显示生成的采样图像
int winwidth, //生成的采样图像宽度
int winheight //生成的采样图像高度
建立采样图像文件的过程就是对提供的图像文件进行变形缩放,根据背景参数贴图生成各种形态的采样图像数据,并保存到vec文件。首先使用函数:
icvStartSampleDistortion( imgfilename, bgcolor, bgthreshold, &data )
生成CvSampleDistortionData结构数据的数据data,其中包含有:
typedef struct CvSampleDistortionData
{
IplImage * src; //用于生成采样数据的源图像(灰度)
IplImage * erode; //源图像经过腐蚀变换的图像
IplImage * dilate; //源图像经过扩张变换的图像
IplImage * mask; //根据背景色和阈值计算生成的屏蔽图像
IplImage * img; //用于计算采样数据的图像缓冲
IplImage * maskimg; //当前正在使用的屏蔽图像
int dx;
int dy;
int bgcolor; //参数给出的背景颜色
} CvSampleDistortionData;
如图:
然后根据给出的计算参数使用函数:
icvPlaceDistortedSample( &sample, inverse, maxintensitydev,
maxxangle, maxyangle, maxzangle,
0 /* 如果非0,则不剪裁图像*/,
0.0 /* 如果非0,则产生随机位移*/,
0.0 /* 如果非0,则产生随机尺度缩放*/,
&data );
计算data.img数据,并写入vec文件。如图:
这是用一张图片生成的100个采样数据,实际上没有那么多,一张图片生成10到20个基本就够用了,在生成过程中,可以调节各个参数,使生成的图像更能反映出目标的细节特征。
注意:在计算mask图像的过程中,算法用到了下列图像像素操作:
icvRandomQuad( data->src->width, data->src->height, quad,
maxxangle, maxyangle, maxzangle );
根据给定参数计算形态学变换矩阵double quad[4][2]。
cvWarpPerspective ( data->src, data->img, quad );
cvWarpPerspective ( data->mask, data->maskimg, quad );
对源图和屏蔽图进行透射变换。
cvSmooth ( data->maskimg, data->maskimg, CV_GAUSSIAN, 3, 3 );
对生成的屏蔽图进行高斯平滑处理(此时屏蔽图变成二值图像,使用背景颜色和阈值)。再做尺度调整和前景颜色偏移调整,最后写入vec文件(顺序写)。
注意:在生成src图像时用到下述算法(扩展源图边界):
for( r = 0; r < data->mask->height ; r++ ){
for( c = 0; c < data->mask->width ; c++ ){
pmask = ((uchar *) (data->mask->imageData + r * data->mask->widthStep ) + c );
if( (*pmask) == 0 ){
psrc = ( (uchar *) (data->src->imageData + r * data->src->widthStep ) + c );
perode = ((uchar *) (data->erode->imageData + r * data->erode->widthStep )+c);
pdilate = ( (uchar *)(data->dilate->imageData + r * data->dilate->widthStep ) + c );
de = (uchar )(bgcolor - (*perode));
dd = (uchar )((*pdilate) - bgcolor);
if( de >= dd && de > bgthreshold ){
(*psrc) = (*perode);
}
if( dd > de && dd > bgthreshold ){
(*psrc) = (*pdilate);
}
}
}
}
其中用到腐蚀和扩张图像的像素值。而屏蔽图像的生成完全是依照背景颜色和阈值提供的参数:
/* 生成屏蔽图像 */
for( r = 0; r < data->mask->height ; r++ ){
for( c = 0; c < data->mask->width ; c++ ){
pmask = ( (uchar *) (data->mask->imageData + r * data->mask->widthStep ) + c );
if( bgcolor - bgthreshold <= (int) (*pmask) && (int) (*pmask) <= bgcolor + bgthreshold ){
*pmask = (uchar) 0;
}else{
*pmask = (uchar) 255;
}
}
}
屏蔽图像是一个类二值的图像,将源图的背景范围内像素置为0,否则为255。然后使用屏蔽矩阵将原图像中像素值处于屏蔽范围内的边缘像素补写回src,以防止边缘属性的丢失。
四、建立级联分类器(多图片训练文件,使用info 建立vec )
根据opencv教程opencv_user.pdf中关于info文件格式的解释,可以使用info格式的文件指定系列图像文件建立vec采样文件。Opencv使用的函数是:
int cvCreateTrainingSamplesFromInfo( const char* infoname, const char* vecfilename,
int num,
int showsamples,
int winwidth, int winheight )
其中参数:
const char* infoname, //采样信息文件名,是包含采样图像和目标位置的文本文件
const char* vecfilename, //采样数据存储的vec文件名
int num, //采样信息中包含的总目标个数(图像中可以包含多个目标)
int showsamples, //是否显示生成的采样数据图像
int winwidth, //生成采样目标的宽度
int winheight //生成采样目标的高度
在使用文本编辑器应用编辑了一个info格式的文件Info.dat:
Img1 2 x11 y11 w11 h11 x12 y12 w12 h12
Img2 1 x21 y21 w21 h21
Img3 2 x31 y31 w31 h31 x32 y32 w32 h32
... ...
之后,使用上面给出的函数就可以建立采样数据文件vec。
注意:此时根据坐标位置在源图上剪裁下来的目标图片不进行各种变形处理,直接写入vec。
综上所述,根据opencv中提供的基本vec生成函数,可以编写出适合实际应用的目标样本收集工具,建立适合各种应用的采样图像建立程序,通过编辑info格式的文本索引生成目标训练文件vec,比如对图形目标进行变换,再与背景图像结合生成采样图片序列:
生成的文件:
lq_info0001_0066_0115_0134_0134.jpg 1 66 115 134 134
lq_info0002_0092_0018_0126_0126.jpg 1 92 18 126 126
lq_info0003_0148_0092_0129_0129.jpg 1 148 92 129 129
lq_info0004_0059_0042_0065_0065.jpg 1 59 42 65 65
lq_info0005_0106_0040_0077_0077.jpg 1 106 40 77 77
lq_info0006_0020_0057_0049_0049.jpg 1 20 57 49 49
lq_info0007_0238_0118_0118_0118.jpg 1 238 118 118 118
这个文件使用如下功能生成(单采样和多背景组合):
单击后弹出对话框输入参数:
其中目标图片为采样的图片文件:
Info文件为生成的info格式文件,可以是任何扩展名。背景文件是info格式的背景图片文件,在背景文件中指定原图片使用的背景。程序根据生成采样的个数顺序使用背景图生成特定背景下的采样图片,并标注采样图的位置信息。也可以使用采样图片的info格式文件和指定的info格式的背景图片组合生成info格式的采样图片系列(多采样和多背景的组合),如图:
单击后弹出对话框:
此时的图片索引info指的是采样图片info格式的索引文件,比如:
lena.jpg 1 183 193 100 105
c68f242c3.jpg 3 265 256 66 72 490 260 73 76 700 230 61 71
lq.png 1 70 72 71 62
untitled7.jpg 1 139 39 52 61
有四个图片的六个目标:
背景索引info和生成索引info分别是使用的背景文件和生成的图片索引文件,程序自动存储相关图片到生成索引info文件指定的路径。背景文件内容为:
1024780.png
calcback04.png
untitled642.png
corner1.png
circle.png
until_dobj0.png
图片如下:
生成的info文件为:
sampinfo0001_0151_0011_0054_0054.jpg 1 151 11 54 54
sampinfo0002_0019_0047_0050_0050.jpg 1 19 47 50 50
sampinfo0003_0149_0159_0158_0158.jpg 1 149 159 158 158
sampinfo0004_0128_0062_0128_0128.jpg 1 128 62 128 128
sampinfo0005_0043_0114_0066_0066.jpg 1 43 114 66 66
sampinfo0006_0093_0102_0136_0136.jpg 1 93 102 136 136
sampinfo0007_0113_0046_0049_0049.jpg 1 113 46 49 49
sampinfo0008_0197_0073_0100_0100.jpg 1 197 73 100 100
sampinfo0009_0128_0065_0039_0039.jpg 1 128 65 39 39
sampinfo0010_0058_0039_0040_0040.jpg 1 58 39 40 40
sampinfo0011_0238_0215_0080_0080.jpg 1 238 215 80 80
sampinfo0012_0101_0043_0056_0056.jpg 1 101 43 56 56
sampinfo0013_0121_0027_0093_0093.jpg 1 121 27 93 93
sampinfo0014_0024_0045_0188_0188.jpg 1 24 45 188 188
sampinfo0015_0179_0169_0112_0112.jpg 1 179 169 112 112
sampinfo0016_0165_0104_0106_0106.jpg 1 165 104 106 106
sampinfo0017_0133_0014_0046_0046.jpg 1 133 14 46 46
sampinfo0018_0155_0089_0184_0184.jpg 1 155 89 184 184
sampinfo0019_0120_0042_0034_0034.jpg 1 120 42 34 34
sampinfo0020_0130_0006_0068_0068.jpg 1 130 6 68 68
sampinfo0021_0059_0072_0152_0152.jpg 1 59 72 152 152
sampinfo0022_0153_0082_0091_0091.jpg 1 153 82 91 91
sampinfo0023_0028_0093_0147_0147.jpg 1 28 93 147 147
sampinfo0024_0059_0139_0267_0267.jpg 1 59 139 267 267
sampinfo0025_0149_0295_0054_0054.jpg 1 149 295 54 54
sampinfo0026_0035_0028_0180_0180.jpg 1 35 28 180 180
sampinfo0027_0108_0107_0027_0027.jpg 1 108 107 27 27
sampinfo0028_0093_0178_0065_0065.jpg 1 93 178 65 65
sampinfo0029_0092_0022_0085_0085.jpg 1 92 22 85 85
sampinfo0030_0099_0174_0142_0142.jpg 1 99 174 142 142
sampinfo0031_0070_0104_0220_0220.jpg 1 70 104 220 220
sampinfo0032_0090_0019_0293_0293.jpg 1 90 19 293 293
sampinfo0033_0086_0160_0032_0032.jpg 1 86 160 32 32
sampinfo0034_0137_0029_0053_0053.jpg 1 137 29 53 53
sampinfo0035_0187_0187_0171_0171.jpg 1 187 187 171 171
sampinfo0036_0030_0055_0159_0159.jpg 1 30 55 159 159
sampinfo0037_0152_0184_0221_0221.jpg 1 152 184 221 221
sampinfo0038_0208_0094_0075_0075.jpg 1 208 94 75 75
sampinfo0039_0017_0076_0276_0276.jpg 1 17 76 276 276
sampinfo0040_0127_0006_0076_0076.jpg 1 127 6 76 76
sampinfo0041_0187_0186_0033_0033.jpg 1 187 186 33 33
sampinfo0042_0036_0103_0103_0103.jpg 1 36 103 103 103
注意,当背景info文件没有指定时,生成的info索引图片数是按照'采样个数'对每一个采样图片进行变换后产生的采样图片个数,默认为10,而如果指定的背景info,采样变形的格式依据背景图片个数进行,每一个背景图片对应一个变形变换的采样图片。在各种info数据生成之后可以使用txt编辑程序,编辑连接所有生成的info形成一个info文件,然后使用从info建立vec即可根据图片生成训练用的vec。对不同图像目标和背景生成的info,进行合并编辑后,可生成具有大数据的训练模型,从而可对实际应用的级联分类器进行训练生成分类器模型xml。
lq_nobg0001_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0002_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0003_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0004_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0005_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0006_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0007_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0008_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0009_0000_0000_0024_0024.jpg 1 0 0 24 24
lq_nobg0010_0000_0000_0024_0024.jpg 1 0 0 24 24
生成的vec文件为:
关于负采样的生成,首先选择各种不同场景的没有人脸的图片,按照识别窗口大小扫描剪裁图片,生成背景图片,并记录info格式的图片信息(文件名,目标数,目标位置),背景图片包含各种场景,如树木,街道,车流,风景,山川河流等,任何可能出现人脸的地方都可以作为背景图,但是,背景图中一定不能出现人脸或要识别的目标。可以将背景图的info也生成负采样的vec文件,在训练中指定为负采样文件。
训练要求正采样至少要大于500,负采样应该大于等于正采样(负采样是同等大小的不包含目标的采样图片)。
五、建立级联分类器文件xml
建立级联分类器cascade,需要从vec开始训练。关于级联分类器的理论原理在网上有许多这方面的文章可以参考,比如,使用Harr特征的级联分类器实现目标检测和通俗易懂理解------Adaboost算法原理,这里就不再赘述了。下面从opencv的建立级联分类器程序源码逐步分解一个级联分类器的建立过程。本分析过程主要目的是探讨分类器训练过程的程序实现算法。
首先是主程序的参数,在opencv的目录下opencv\sources\apps\haartraining的haartraining.cpp文件中:
-data <dir_name>\n" //分类器文件名(xml)
-vec <vec_file_name>\n" //训练数据文件名(vec)
-bg <background_file_name>\n" //背景文件名(作为负采样图像)
-bg-vecfile\n" //背景文件是否为vec文件形式
-npos <number_of_positive_samples = %d>\n" //采样数据中正采样数
-nneg <number_of_negative_samples = %d>\n" //采样数据中负采样数
-nstages <number_of_stages = %d>\n" //分类器的级联分段数(层数)
-nsplits <number_of_splits = %d>\n" //分段决策树stump的分枝(叉)数
-mem <memory_in_MB = %d>\n" //haar属性的存储占用量(缓冲大小)
-sym (default)] [-nonsym\n" //是否对称计算
-minhitrate <min_hit_rate = %f>\n" //最小中标率
-maxfalsealarm <max_false_alarm_rate = %f>\n" //最大脱标报警(负采样的误识率)
-weighttrimming <weight_trimming = %f>\n" //权重取舍值(采样的最小剪枝权重)
-eqw\n" //等值权重(初始时是否使用等值权重)
-mode <BASIC (default) | CORE | ALL>\n" //模式
-w <sample_width = %d>\n" //采样数据宽
-h <sample_height = %d>\n" //采样数据高
-bt <DAB | RAB | LB | GAB (default)>\n" //增强算法
-err <misclass (default)>\n" //残差模式misclass、gini、entropy
-maxtreesplits <%d>\n" //最大树分叉数(检测cluster聚类数)
-minpos <%d>\n" //最小正采样数
其中有些参数的默认值为:
bool bg_vecfile = false; //背景文件为直接图像形式的info格式文件
int npos = 2000; //正采样个数
int nneg = 2000; //负采样个数
int nstages = 14; //级联分段数(cascade最大层数)
int mem = 200; //haar属性存储范围(开辟的valcache的特征数)
int nsplits = 1; //段分类器的stump(桩)分枝数
float minhitrate = 0.995F; //最小中标率
float maxfalsealarm = 0.5F; //最大脱标报警
float weightfraction = 0.95F; //权重修正尾数(1-weightfraction = 剪枝权重)
int mode = 0; //模式,默认为BASIC
int symmetric = 1; //对称计算
int equalweights = 0; //等值权重(初始权重一致)
int width = 24; //采样数据宽
int height = 24; //采样数据高
int boosttype = 3; //增强算法GAB
int stumperror = 0; //残差模式=misclass
int maxtreesplits = 0; //最大分枝数(stage的同层可聚类分枝数)
int minpos = 500; //最小正样本数
程序通过上述参数调用:
cvCreateTreeCascadeClassifier( dirname, vecname, bgname,
npos, nneg, nstages, mem,
nsplits,
minhitrate, maxfalsealarm, weightfraction,
mode, symmetric,
equalweights, width, height,
boosttype, stumperror,
maxtreesplits, minpos, bg_vecfile );
函数,函数声明:
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 )
在cvhaartraining.cpp中实现。这个函数首先装入已存在的分类器数据:
tcc = (CvTreeCascadeClassifier*)icvLoadTreeCascadeClassifier( dirname, winwidth + 1, &total_splits );
这个函数装入经过训练的分类器节点数据(在目录dirname查找n AdaBoostCARTHaarClassifier.txt文件)其中n为分类器树中的节点编号,每个节点表示一个stage段分类器。该函数调用函数icvLoadCARTStageHaarClassifierF()装入各个已存在的分段:
1、读入文件中的count,作为段分类器中包含的haar分类器个数
2、根据count值顺序读入haar分类器参数:
icvLoadCARTHaarClassifier( FILE * file, int step ),注意step是输入参数,为winwidth + 1。首先读入分类器的特征个数count,然后顺序读入各特征及特征参数threshold 、 left 、 right。然后读入特征值。
函数icvLoadHaarFeature()读入特征,每一个特征都由最多3个矩形和相应的权重组成,对于少于3个矩形的特征需要使用0矩形和权重进行填充(补足三个矩形),特征的描述字符用于确定是直或斜矩形,icvConvertToFastHaarFeature()将特征转换成Fast特征。
3、读入各个段分类器的阈值threshold。并通过读入的parent,next值确定当前段分类器的父节点,next(兄弟节点)连接指向节点(节点序号)。
4、计算分类器的分枝数,非叶节点数。
如果没有任何nAdaBoostCARTHaarClassifier.txt文件文件,则函数返回空tcc分类器(仅有一个tcc的结构体,其中的tcc->root=null)。
类器可以通过不断增加采样的方式进行继续训练,此处功能就是装入已有的分类器,继续训练。对于初始训练,该函数返回一个经过初始化的空分类器tcc。然后通过函数icvFindDeepestLeaves找到这个tcc分类器一个叶节点(对于空分类器,开始的叶节点就是根节点):
leaves = icvFindDeepestLeaves( tcc );
这个函数找到tcc的第一个最深的叶节点(child = null,next = null的节点),程序如下:
CvTreeCascadeNode* icvFindDeepestLeaves( CvTreeCascadeClassifier* tcc )
{
CvTreeCascadeNode* leaves;
//CV_FUNCNAME( "icvFindDeepestLeaves" );
BEGIN;
int level, cur_level;
CvTreeCascadeNode* ptr;
CvTreeCascadeNode* last;
leaves = last = NULL;
ptr = tcc->root;
level = -1;
cur_level = 0;
/* 找到最深叶节点 */
while( ptr ){
if( ptr->child ){ //先深探查子节点,找到最左叶节点
ptr = ptr->child; //进入下一层
cur_level++; //层数加一
}else{//没有进一步的子节点
if( cur_level == level ){//当前节点不是层数更深的节点
last->next_same_level = ptr;//last在遍历中生成了最深节点的链表(多个最深)
ptr->next_same_level = NULL;
last = ptr;
}
if( cur_level > level ){//如果当前层数更深
level = cur_level;//设置当前最深层数
leaves = last = ptr;//指向最深层节点。leaves为头一个最深节点
ptr->next_same_level = NULL;
}
while( ptr && ptr->next == NULL ) { //到达同层的最后节点,返回有同层的节点或返回到根
ptr = ptr->parent; //向上返回父节点,如果next=null则继续返回直到根。
cur_level--; //当前层数减一
}
if( ptr ) //while返回如果不是根,则必定有ptr->next不为null
ptr = ptr->next;//进入同层节点,如果是根,则循环退出ptr=null
}
}
END;
return leaves;
}
该函数的leaves变量指向tcc的最深节点,是最左边的最深节点(因为是先深探查),last指向最深节点形成的链表,last->next_same_level形成链接指向,其结果没有被引用,在后面的进一步训练中可能通过next_same_level查询被训练的叶节点链。
函数icvCreateIntHaarFeatures()计算全部haar特征的矩形尺寸及位置并存储(不同位置和不同尺寸的同类特征在算法中属于不同特征):
haar_features = icvCreateIntHaarFeatures( winsize, mode, symmetric );
其中用到常量:
int s0 = 36; /* 基本haar特征的最小总面积*/
int s1 = 12; /* 斜haar特征2的最小总面积*/
int s2 = 18; /* 斜haar特征3的最小总面积*/
int s3 = 24; /* 斜haar特征4的最小总面积*/
在icvCreateIntHaarFeatures()函数中按照给定模式计算haar特征:
for( x = 0; x < winsize.width; x++ ){//水平位置
for( y = 0; y < winsize.height; y++ ){//垂直位置
for( dx = 1; dx <= winsize.width; dx++ ){//宽度
for( dy = 1; dy <= winsize.height; dy++ ){//高度
程序使用for循环同时对所有haar特征类进行指定窗口内的位置、尺寸特征计算:
// haar_x2
if ( (x+dx*2 <= winsize.width) && (y+dy <= winsize.height) ) {
if (dx*2*dy < s0) continue;//排除面积小于最小总面积的基本特征
if (!symmetric || (x+x+dx*2 <=winsize.width)) {
haarFeature = cvHaarFeature( "haar_x2", x, y, dx*2, dy, -1, x+dx, y, dx , dy, +2 );
CV_WRITE_SEQ_ELEM ( haarFeature, writer );
}
}
haar_x2特征是在x方向占两个像素,y方向占一个像素的边缘特征,一个像素为白,一个像素为黑:,根据特征值计算公式,这个特征的值是整个特征图像的像素值之和减去黑像素占的像素值之和,因为整个像素区是黑像素域的二倍,所以黑像素区为+2,而整个像素区为-1(关于+2和-1的解释,黑色区域乘以+2,整个区域乘以-1,然后相加结果值为该特征的特征值。正负号表示了区域灰值和的加减操作)。
Haar特征的结构:
typedef struct CvTHaarFeature{
char desc[CV_HAAR_FEATURE_DESC_MAX];//描述文字,表示正/斜矩形
int tilted;//是斜矩形为1,否则0
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;
cvHaarFeature()函数定义:
CV_INLINE CvTHaarFeature cvHaarFeature( const char* desc,
int x0, int y0, int w0, int h0, float wt0,
int x1, int y1, int w1, int h1, float wt1,
int x2 CV_DEFAULT ( 0 ), int y2 CV_DEFAULT( 0 ),
int w2 CV_DEFAULT ( 0 ), int h2 CV_DEFAULT( 0 ),
float wt2 CV_DEFAULT( 0.0F ) );
CV_INLINE CvTHaarFeature cvHaarFeature( const char* desc,
int x0, int y0, int w0, int h0, float wt0,
int x1, int y1, int w1, int h1, float wt1,
int x2, int y2, int w2, int h2, float wt2 )
对一个haar特征结构进行赋值,计算矩形的顶点位置,并根据desc参数确定是否为斜矩形。
icvCreateIntHaarFeatures()特征计算函数分别计算了:
haar_x2
haar_y2
haar_x3
haar_y3
haar_x4
haar_y4
haar_x2_y2
haar_point
tilted_haar_x2
tilted_haar_y2
tilted_haar_x3
tilted_haar_y3
tilted_haar_x4
tilted_haar_y4
tilted项是对应haar特征转45度角的特征。
注意,在计算中没有tilted_haar_x2_y2和tilted_haar_point特征。
注意,计算的特征是在窗口范围内(20 x 20)模板移动和缩放的特征,每一次移动和每一次缩放都表示一个不同的特征,模板从单个像素开始(按照dx和dy的循环)缩放到整个窗口图像(注意,不是等比缩放,比如对于haar_x2特征,w为2,h可以是winsize.height值,此时特征沿winsize.height被拉长,但是要注意,w一定是2的倍数,因为原始w=2,因此在宽度上的缩放一定是2的倍数)。
在计算过程中,所有特征都是给定四个矩形顶点的坐标位置(根据采样窗口大小,移动和放大),这里的特征是特征的模板,在真正计算特征值时才计算实际特征对应采样图片的特征值。
回到 cvCreateTreeCascadeClassifier()函数,在计算完haar特征的矩形位置之后,函数分配采样数据空间(进行模型训练的数据缓冲对象):
training_data = icvCreateHaarTrainingData( winsize, npos + nneg );
其中npos + nneg为采样总数,winsize为窗口尺寸(CvSize类型)。
函数icvSetLeafNode();根据给定的parent,设置tcc从根到指定节点的路径:
/* 设置tcc树从root到leaf的路径 */
void icvSetLeafNode( CvTreeCascadeClassifier* tcc, CvTreeCascadeNode* leaf )
{
CV_FUNCNAME ( "icvSetLeafNode" );
BEGIN;
CvTreeCascadeNode* ptr;
ptr = NULL;
while( leaf ){
leaf->child_eval = ptr;//设置节点的child->eval作为路径指针
ptr = leaf;沿节点parent指针向上搜索,设置child->eval
leaf = leaf->parent;
}//循环完成后,ptr指向一个父节点(tcc有多个父的同层节点,第一个父节点作为tcc的根节点)
leaf = tcc->root;//根节点指向第一个父节点
while( leaf && leaf != ptr )
leaf = leaf->next;//查找ptr是不是指向一个父节点
if( leaf != ptr )
CV_ERROR ( CV_StsError , "Invalid tcc or leaf node." );//给定的leaf不是合法的节点
tcc->root_eval = ptr;//设置根节点的eval指向ptr这个leaf选定的父节点
END;
}
叶节点初始化当前分类器相关树节点的child_eval指针。在初始条件下,parent就是前面函数icvFindDeepestLeaves()给出的leaves节点(分类器叶节点)。icvSetLeafNode()函数查找当前leaves的根节点(注意,不是分类器的根,而是根的兄弟节点,如果根有多个同层节点),并把这个节点设置为当前活动的根节点root_eval,说明是这个节点(分类器)上的一个分类器分枝需要进行进一步的训练。分类器树结构和节点结构:
typedef struct CvTreeCascadeClassifier
{
CvTreeCascadeNode* root;/* 根节点,根节点有多个兄弟,而root_eval则是当前操作的一个 */
CvTreeCascadeNode* root_eval; /* 根节点,指向当前活动的根节点 */
int next_idx; /* 当前节点的索引(节点文件的序号)
} CvTreeCascadeClassifier;
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;
对于初始训练,分类器是空,因此在dirname目录中没有段号加文件名(dirname+"/"+"段号"+"AdaBoostCARTHaarClassifier.txt")形式的txt文件,此时的parent = null。
使用函数icvGetHaarTrainingDataFromVec();读入采样数据正样本矢量(图像):
icvGetHaarTrainingDataFromVec( training_data,//生成的训练数据
0, //开始采样数(开始位置)
npos, //装入采样数(正采样数)
(CvIntHaarClassifier*) tcc, //被继续训练的分类器,检测采样的可用性
vecfilename,//采样文件名
&consumed );//损耗数,不可用采样数
该函数使用函数icvGetHaarTrainingData()读取采样数据:
static int icvGetHaarTrainingDataFromVec( CvHaarTrainingData* data, //生成的采样数据
int first, //采样数据的开始位置(在训练数据中的)
int count, //文件中的采样数据个数
CvIntHaarClassifier* cascade,//训练的分类器(已初始化)
const char* filename,//采样文件
int* consumed )//采样的损耗
{
int getcount = 0;
CV_FUNCNAME ( "icvGetHaarTrainingDataFromVec" );
BEGIN;
CvVecFile file;
short tmp = 0;
file.input = NULL;
if( filename )
file.input = fopen ( filename, "rb" );
if( file.input != NULL ){
size_t elements_read1 = fread ( &file.count, sizeof( file.count ), 1, file.input);
size_t elements_read2 = fread ( &file.vecsize, sizeof( file.vecsize ), 1, file.input);
size_t elements_read3 = fread ( &tmp, sizeof( tmp ), 1, file.input );
size_t elements_read4 = fread ( &tmp, sizeof( tmp ), 1, file.input );
CV_Assert ( elements_read1 == 1 && elements_read2 == 1 && elements_read3 == 1 && elements_read4 == 1);
if( !feof ( file.input ) ){
if( file.vecsize != data->winsize.width * data->winsize.height ){
fclose ( file.input );
CV_ERROR ( CV_StsError , "Vec file sample size mismatch" );
}
file.last = 0;
file.vector = (short*) cvAlloc ( sizeof( *file.vector ) * file.vecsize );
getcount = icvGetHaarTrainingData(data,first,count,cascade,icvGetHaarTraininDataFromVecCallback,&file, consumed,NULL);
cvFree ( &file.vector );
}
fclose ( file.input );
}
END;
return getcount;
}
在icvGetHaarTrainingData()函数中首先通过CvGetHaarTrainingDataCallback()函数读入vec文件的一个图像img,然后计算该图像的积分图像sumdata和斜积分图像tilteddata及平方和图像,同时计算图像的归一化(normalization)因子,函数如下:
static int icvGetHaarTrainingData(
CvHaarTrainingData* data, int first, int count,//训练数据,开始位置,个数
CvIntHaarClassifier* cascade,//分类器
CvGetHaarTrainingDataCallback callback, //文件读写函数
void* userdata,//采样文件
int* consumed, //采样损耗
double* acceptance_ratio)//可用比率
{
int i = 0;
ccounter_t getcount = 0;
ccounter_t thread_getcount = 0;
ccounter_t consumed_count;
ccounter_t thread_consumed_count;
/* 私有变量 */
CvMat img;
CvMat sum;
CvMat tilted;
CvMat sqsum;
sum_type* sumdata;
sum_type* tilteddata;
float* normfactor;
/* 私有终止 */
assert ( data != NULL );
assert ( first + count <= data->maxnum );
assert ( cascade != NULL );
assert ( callback != NULL );
CCOUNTER_SET_ZERO(getcount);
CCOUNTER_SET_ZERO(thread_getcount);
CCOUNTER_SET_ZERO(consumed_count);
CCOUNTER_SET_ZERO(thread_consumed_count);
#ifdef CV_OPENMP
#pragma omp parallel private(img, sum, tilted, sqsum, sumdata, tilteddata, \
normfactor, thread_consumed_count, thread_getcount)
#endif /* CV_OPENMP */
{
sumdata = NULL;
tilteddata = NULL;
normfactor = NULL;
CCOUNTER_SET_ZERO(thread_getcount);
CCOUNTER_SET_ZERO(thread_consumed_count);
int ok = 1;
//原始图像(分配了图像缓冲)
img = cvMat ( data->winsize.height, data->winsize.width,CV_8UC1,
cvAlloc ( sizeof( uchar )*data->winsize.height*data->winsize.width));
//和图像(数据缓冲为null,使用训练数据的缓冲)
sum = cvMat ( data->winsize.height + 1, data->winsize.width + 1, CV_SUM_MAT_TYPE, NULL );
//斜图像(数据缓冲为null,使用训练数据的缓冲)
tilted = cvMat ( data->winsize.height + 1, data->winsize.width + 1, CV_SUM_MAT_TYPE, NULL );
//平方图像(分配了数据缓冲)
sqsum = cvMat ( data->winsize.height + 1, data->winsize.width + 1, CV_SQSUM_MAT_TYPE,
cvAlloc ( sizeof( sqsum_type ) * (data->winsize.height + 1) * (data->winsize.width + 1) ) );
#ifdef CV_OPENMP
#pragma omp for schedule(static, 1)
#endif /* CV_OPENMP */
for( i = first; (i < first + count); i++ ){//循环读入采样数据
if( !ok )
continue;
for( ; ; ){
ok = callback( &img, userdata );//从文件userdata中读入采样到img
if( !ok )
break;
CCOUNTER_INC(thread_consumed_count);//读入个数加一
//计算和图像指针位置
sumdata = (sum_type*) (data->sum .data.ptr + i * data->sum .step);
//计算斜图像指针位置
tilteddata = (sum_type*) (data->tilted.data.ptr + i * data->tilted.step);
//计算图像归一化数据的数组位置
normfactor = data->normfactor.data.fl + i;
sum.data.ptr = (uchar *) sumdata;//指向训练数据空间
tilted.data.ptr = (uchar *) tilteddata;//指向训练数据空间
icvGetAuxImages( &img, &sum, &tilted, &sqsum, normfactor);//计算采样参数
使用分类器cascade检测采样的可用性
if( cascade->eval(cascade,sumdata,tilteddata,*normfactor) != 0.0F ){
CCOUNTER_INC(thread_getcount);//可用采样数加一
break;
}
}
#ifdef CV_VERBOSE
if( (i - first) % 500 == 0 ){
fprintf ( stderr , "%3d%%\r", (int) ( 100.0 * (i - first) / count ) );
fflush ( stderr );
}
#endif /* CV_VERBOSE */
}
cvFree ( &(img.data.ptr) );
cvFree ( &(sqsum.data.ptr) );
#ifdef CV_OPENMP
#pragma omp critical (c_consumed_count)
#endif /* CV_OPENMP */
{
/* consumed_count += thread_consumed_count; */
CCOUNTER_ADD(getcount, thread_getcount);
CCOUNTER_ADD(consumed_count, thread_consumed_count);
}
} /* omp parallel */
if( consumed != NULL ){
*consumed = (int)consumed_count;
}
if( acceptance_ratio != NULL ){
/* *acceptance_ratio = ((double) count) / consumed_count; */
*acceptance_ratio = CCOUNTER_DIV(count, consumed_count);
}
return static_cast<int>(getcount);
}
计算sum,tilted, sqsum和normfactor是在函数icvGetAuxImages()中完成的,注意,归一化因子的计算可以有不同的算法,在openCV中使用的是:
(*normfactor) = (float) sqrt ( (double) (area * valsqsum - (double)valsum * valsum) );
其中
area为采样图像的面积(wid x hig)
valsqsum为图像像素的平方和的积分图像
valsum为图像像素的积分图像(像素和)
其数学意义是:用整个图像像素的平方和值valsqsum乘以图像面积 a = w x h,即valsqsum x a,然后减去图像像素的和的平方,再开方,其值作为该采样图像的归一化值。每一个采样都有一个归一化值。
注意,area x valsqsum的意义在于用valsqsum值作为图像的像素值计算的像素的和值,其中的valsqsum是图像的像素平方和值(每个像素的平方),即如果图像的像素值为2,而w=h=2,则,valsqsum=4 + 4 + 4 + 4 = 16,而valsum = 2 + 2 + 2 + 2 = 8,该图像的归一化因子f=sqrt(4*16--8*8)=sqrt(64- 64)=0 。这里我们设定图像的所有像素都是相同的,即2,没有波动的图像,计算出的归一因子为0,像素值为n时,valsqsum=4*n*n,valsum=4*n,f=sqrt(4*4*n*n -- 4*n*4*n)=0,因此可以说明,归一化因子是对图像的像素波动进行度量的因子,归一化的是图像灰度的波动特性。
在源码中有一个注释掉的语句为:
(*normfactor) = (float) sqrt( valsqsum / area - ( valsum / area )^2 ) * area
这其中可能有算法改进的数学意义。
这个算式对上面的波动假设是否成立?f=sqrt(16/4 -- 8/4 * 8/4)*4 = 0。对于n灰度,则f=sqrt(4*n*n/4 -- (4*n/4)*(4*n/4))*4=sqrt(n*n -- n*n) * 4 = 0。表示的都是图像灰度的波动属性。
建立分类器的训练过程具体操作如下:
- 从vec文件中顺序取出采样图像
- 计算上述值图像,并存入指针指定存储位置
在对一个采样计算完成后,使用分类器的求值函数
tcc->eval = icvEvalTreeCascadeClassifierFilter()
对当前采样图像的和图像,斜图像和归一化因子进行计算,即对级联分类器树的当前段分类器路径上所有节点求stage_eval函数的返回值float,代码如下:
ptr = ((CvTreeCascadeClassifier*) classifier)->root_eval;//当前活动的分类路径
while( ptr ){
if( ptr->stage->eval((CvIntHaarClassifier*) ptr->stage,sum, tilted, normfactor ) < ptr->stage->threshold - CV_THRESHOLD_EPS){
return 0.0F;
}
ptr = ptr->child_eval;
}
return 1.0F;
其中stage->eval = icvEvalStageHaarClassifier(),在建立stage对象时赋值。在icvEvalStageHaarClassifier()函数中,进一步使用stage分类器中包含的inthaarclassifier分类器的eval函数icvEvalCARTHaarClassifier进行计算,求得对应每一个inthaarclassifier属性的值,其中用到函数cvEvalFastHaarFeature(),代码如下:
int idx = 0;
do{
if( cvEvalFastHaarFeature(((CvCARTHaarClassifier*) classifier)->fastfeature + idx, sum, tilted )
< (((CvCARTHaarClassifier*) classifier)->threshold [idx] * normfactor) ){
idx = ((CvCARTHaarClassifier*) classifier)->left [idx];
}else{
idx = ((CvCARTHaarClassifier*) classifier)->right [idx];
}
} while( idx > 0 );
return ((CvCARTHaarClassifier*) classifier)->val[-idx];
cvEvalFastHaarFeature()函数按照属性的't'特征(正矩形或斜矩形)从sum图像或tilted图像计算属性的特征值,根据特征值与阈值threshold 的比较结果选择左或右子树路径,以此来判断采样值的检测状态,对于正采样,只有正确检测的采样才能作为进一步训练的采样值。函数icvEvalTreeCascadeClassifierFilter()在读取正采样文件时被调用,用来过滤采样文件中的采样数据,并返回实际可用的采样作为训练数据。
函数icvGetHaarTrainingData()通过调用icvEvalTreeCascadeClassifierFilter()过滤掉那些采样图像中不能满足分类器分类指标的图像,并将合格的训练图像存入HaarTrainingData对象(training_data变量)的data->sum .data和data->tilted.data中。至此,icvGetHaarTrainingDataFromVec()函数完成采样数据的装入并计算了每一个正采样(可用采样)的sum和tilted图像,便于属性特征值的快速计算。对于初次训练,由于tcc为空分类器,所以icvGetHaarTrainingDataFromVec()函数的返回值是所有vec的正采样图像数,没有滤波过程。
函数icvGetHaarTrainingDataFromBG();读入负采样数据:
icvGetHaarTrainingDataFromBG( training_data,//训练数据(包含了正采样的数据对象)
poscount, //已装入的正采样数
nneg, //待读入的负采样数(文件指定的)
(CvIntHaarClassifier*) tcc, //待训练的分类器
&false_alarm,//可接受的分类误差(返回值)
bg_vecfile ? bgfilename : NULL );//背景文件类型
注意,BG函数在背景文件为vec类型的背景图格式时读入矢量格式的负采样数据,否则,根据info格式的背景图像读入背景数据到训练数据结构负采样缓冲中,info格式的背景图像通过函数icvInitBackgroundReaders()初始化到背景图像结构对象中。在负采样数据读取的过程中也调用了tcc的icvEvalTreeCascadeClassifierFilter()函数,对负采样进行检测,那些被正确检测出的负采样(此时为负采样的错误识别,检测为人脸)作为进一步训练的负采样,而能够正确检测出的负采样(负采样的正确检测,检测为非人脸)则不再作为训练的负采样(被过滤掉)。
经过上述采样数据和haar属性的初始化后,开始计算分类器当前段(stage)的参数值。本说明以初始训练为起点,即tcc为0个节点的分类器树,此时所有训练采样都来自于vec文件和BG文件。
函数icvSetNumSamples()设置训练数据的采样数:
training_data->sum .rows = training_data->tilted.rows = num;
training_data->normfactor.cols = num;
training_data->cls.cols = training_data->weights.cols = num;
其中num = poscount + negcount(正采样数与负采样数之和),training_data是由icvGetHaarTrainingDataFromVec()函数和icvGetHaarTrainingDataFromBG()函数初始化的采样训练数据。
注意,sum和tilted两个Mat类型变量的行数,它们都是采样数(正负采样),而normfactor ,cls和weights变量则是列数为采样数,因此,cls的列对应的是sum的行,即一个采样行对应一个权重列。
函数icvSetWeightsAndClasses()设置采样的正负标记(1,0)和采样的权重 ,正采样标记为1,负采样标记为0。权重则设置为平均权重,变量equalweights指示是否采用相等权重或者分别正负采样数计算的权重。
函数icvPrecalculate()计算haar属性的特征值,其中numprecalculated变量指定要计算的特征值数,从0开始计数,这个变量是应用指定的变量,numprecalculated在应用中对应的是mem参数,解释为占用的存储器缓冲大小(MB单位),默认值为200,也就是说在应用中输入的缓冲参数为200MB时,在icvPrecalculate()函数中的numprecalculated变量值为200,在icvPrecalculate()函数中,该变量的作用如下:
numprecalculated -= numprecalculated % CV_STUMP_TRAIN_PORTION;
numprecalculated = MIN ( numprecalculated, haarFeatures->count );
其中常量CV_STUMP_TRAIN_PORTION = 100,以100为尺度计算numprecalculated为100的整倍数,这表明在icvPrecalculate()函数中要计算多少个特征值,预计算函数开辟多大的valcache缓冲区,计算缓冲区越大,训练算法越快,反之越慢(这是一个在训练中可以重复使用的数据缓冲)。这里涉及到一个特征在采样空间中所占有的内存数,如果取特征值为浮点数(4字节),则一个特征值占用1MB时,采样空间应该最多容纳1024x1024/4=262144个采样,更多的采样算法没有限制,但是mem参数将不再是MB单位。在icvPrecalculate()函数中生成numprecalculated个特征的缓冲空间和对应采样的特征值valcache,这个值在分类算法的计算中被反复使用,而那些没有被valcache的特征,则在算法计算中被反复计算,因此减缓了算法计算的速度。所以在计算机容量允许的情况下要尽量增大输入的mem值,默认为200,但是一定要注意,训练算法在运行中需要分配大量的内存,都是矩阵类型的存储空间,如果valcache设置的太大可能会影响到算法的执行速度。
函数icvPrecalculate()初始化训练数据的data->valcache和data->idxcache变量,都是Mat类型:
CV_CALL ( data->valcache = cvCreateMat ( m, numprecalculated, CV_32FC1 ) );
CV_CALL ( data->idxcache = cvCreateMat ( numprecalculated, m, CV_IDX_MAT_TYPE ) );
其中m = data->sum .rows ,也就是采样数。因此valcache是每个采样特征值m行numprecalculated列矩阵(行向量是一个采样的所有特征值,列向量是一个特征的所有采样的特征值)。而idxcache则是特征值的索引组成的矩阵(行向量是一个属性的所有采样特征值索引组成的向量,列向量则是一个属性的所有采样特征值索引组成的向量)。然后调用函数icvGetTrainingDataCallback()来初始化valcache数据。
注意,在计算段分类器参数时,使用valcache矩阵的数据,索引idxcache矩阵数据。当特征是valcache数据中的已有数据时直接使用valcache数据,否则使用icvGetTrainingDataCallback()重新计算特征值数据。最小误差的特征参数总是从全部特征数据中挑选,因此除了valcache中预计算的特征外,其他特征的特征值都是要反复计算的,这将消耗很多计算时间。
函数icvGetTrainingDataCallback()利用sum和tiled采样图像计算指定特征的特征值,并根据归一化参数training_data->normfactor.data .fl [i],归一化后存入valcache,注意,归一化参数是采样图像的归一化(0<=i<m采样数)。cvGetSortedIndices()函数对特征的采样特征值进行排序,初始化idxcache变量:
1、按顺序初始化idx一个行,顺序为0,1,2,...,m采样数,注意,idx的行数为特征个数
2、对指定特征的所有采样特征值进行分类(排序,在一个行上按降序重排顺序,此时的索引值指向原特征值位置即valcache中特征值行上的列位置),注意,idx的行列顺序为:
idxcache(numprecalculated, m)
每个特征值一行,每行有采样个数个元素。valcache的行列顺序为:
valcache(m, numprecalculated)
每个采样一行,每行有numprecalculated个元素。由于对每一个特征行的特征值进行排序,此时的idxcache是按升序排列的'特征值-采样'的索引缓冲变量,访问指定采样的特定特征值的方式应该是:
idx[i,j]
为第j个采样图像上的第i个特征的特征值。icvPrecalculate()函数到此返回,并生成了采样的特征值valcache和特征值索引idxcache。
函数icvCreateCARTStageClassifier()建立一个级联分类器的stage段 分类器,一个段分类器是由一系列cart弱分类器组成的树状分类器(其中的每一个cart都是在不同的权重下计算的分类器,因此stage段分类器是一个强分类器),每个cart分类器也是由一系列stump分类器组成的树,一个stump分类器(弱分类)指定了一个分类节点阈值threshold和分类误差left/right(其中的每一个stump都是在相同的权重下计算的分类器)。由给定的分支数确定cart中包含的stump分枝数,由给定的误差值确定cart系列的个数,对于分支数为1,cart仅包含一个dump弱分类器,对于分支数为k,cart包含k个stump组成一个类二叉树的分类路径(每一个分枝代表一个特征,一个cart代表采样空间被一组特征分类,并给出各分类的误差值,概率),其中每一个节点都由left/right指示下一个节点[1]或终止[0],stump的返回stump->left/right有一个置信度判断,设置为[0,+1]。下面是计算段分类器函数的定义:
CvIntHaarClassifier* icvCreateCARTStageClassifier(
CvHaarTrainingData* data, //训练数据,预计算的指定特征数的valcache数据
CvMat * sampleIdx, //采样空间索引数据,这里指定为null
CvIntHaarFeatures* haarFeatures,//特征数据
float minhitrate, //最小中标率
float maxfalsealarm, //最大脱标报警
int symmetric, //对称性
float weightfraction, //剪枝权重(1 - weightfraction),消除剪枝采样条件
int numsplits, //分类器的分枝数(使用多枝stump)
CvBoostType boosttype,
CvStumpError stumperror,
int maxsplits )
在cvCreateTreeCascadeClassifier()中调用此函数,(注意:在cvCreateCascadeClassifier()中也调用此函数建立级联分类器)其中:
Data 为训练数据
NULL 空表示为整个采样空间作为计算用数据集
haar_features 为特征值数据,所有特征的结构数据
minhitrate 函数带入的参数,最小中标率
maxfalsealarm 函数带入的参数,最大脱标报警值
symmetric 函数带入的参数是否进行对称计算
weightfraction 采样保留权重,用于剪枝(当采样权重小于1-weightfraction,消除采样)
numsplits 函数带入的参数,stump分支数
(CvBoostType) boosttype 函数带入的参数(增强类型)
(CvStumpError) stumperror 函数带入的参数(误差类型)
0 //maxsplits,最大聚类分枝数,stage的分枝数,0表示仅一个分枝
该函数初始化stumpTrainParams和trainParams结构,建立并初始化weakTrainVals对象,注意:使用函数cvBoostStartTraining()初始化weakTrainVals时返回一个trainer对象,该对象并未参与分类器的训练循环,但一直存在,直到分类器建立后才被释放,因此它可以被看作是一个缓冲区(其中存储了训练过程中的所有中间数据,比如分类器的阈值,权重,索引等)。
注意,trainer 中包含有采样数据类型(正采样为1,负采样为0)的矩阵,weakTrainVals中也包含有采样的类型数据。初始化完成后,该函数进入循环,调用cvCreateCARTClassifier()函数,其原型声明如下:
CvClassifier* cvCreateCARTClassifier(
CvMat * trainData,
int flags,
CvMat * trainClasses,
CvMat * typeMask,
CvMat * missedMeasurementsMask,
CvMat * compIdx,
CvMat * sampleIdx,
CvMat * weights,
CvClassifierTrainParams* trainParams )
调用参数为:
data->valcache, //trainData,由预计算函数生成的数据
flags, //flags,行列标志
weakTrainVals, //trainClasses,采样类型数据(正负采样标志)
0, //typeMask
0, //missedMeasurementsMask
0, //特征索引为空,valcache的列指定了使用的特征
trimmedIdx, //sampleIdx,剪枝过程初始化的采样索引
&(data->weights), //weights,权重数据
(CvClassifierTrainParams*) &trainParams //trainParams,训练参数
其中:data->valcache为采样特征值的缓存矩阵,由icvPrecalculate()函数给出
flags定义为CV_ROW_SAMPLE常量,表示训练数据为行采样格式存储形式
weakTrainVals是由cvBoostStartTraining()函数初始化的采样类型(正/负)和初始权重矩阵。
trimmedIdx为采样特征值的索引矩阵,判断剪枝权重后返回的采样集索引数据
data->weights为权重矩阵,经过多次迭代后的结果权重(当前生成cart分类器所依据的权重)
trainParams为参数矩阵,由主函数带入的初始数据组成的参数矩阵
下面分析该函数的实现过程(在cvboost.cpp中)。在cvCreateCARTClassifier()中首先分配cart变量:
cart = (CvCARTClassifier*) cvAlloc ( datasize );
memset ( cart, 0, datasize );
其中:
datasize = sizeof( *cart ) + //cart对象结构的尺寸
(sizeof( float ) + 3 * sizeof( int )) * count + //分枝结构数组
sizeof( float ) * (count + 1); //浮点数组
而count的值为:
count = ((CvCARTTrainParams*) trainParams)->count;
在函数icvCreateCARTStageClassifier()可知:
trainParams.count = numsplits
为分类树的分枝数,默认值为1,也就是说由此函数建立的每一个cart弱分类器MTCart仅使用一个特征形成一个二叉树枝stump(cart可以是多分支的stump形成的二叉树,每一个分枝都是在相同权重下的一个特征组成的stump),而一系列这样cart组成了stage分类器(cart与stump的区别在于,每一次迭代过程,cart都根据分类结果误差改变采样的权重,而stump则不改变权重,只改变采样集的大小)。初始化cart:
cart->count = count;//cart的分枝数,stump使用的特征数分枝
cart->eval = cvEvalCARTClassifier; //cart使用的求值函数指针
cart->save = NULL;
cart->release = cvReleaseCARTClassifier; //cart使用的释放函数指针
cart->compidx = (int*) (cart + 1); //存放特征索引数组位置,count个特征变量
cart->threshold = (float*) (cart->compidx + count);//存放threshold变量的位置
cart->left = (int*) (cart->threshold + count);
cart->right = (int*) (cart->left + count);
cart->val = (float*) (cart->right + count);
然后建立CvCARTNode* intnode 和 CvCARTNode* list 缓冲区:
datasize = sizeof( CvCARTNode ) * (count + count);//节点数(两个缓冲的节点数)
intnode = (CvCARTNode*) cvAlloc ( datasize );
memset ( intnode, 0, datasize );//清零操作
list = (CvCARTNode*) (intnode + count);//List缓冲紧随在intnode之后
初始化根节点intnode[0]:
intnode[0].sampleIdx = sampleIdx;
intnode[0].stump = (CvStumpClassifier*)((CvCARTTrainParams*) trainParams)->stumpConstructor(
trainData,
flags,
trainClasses,
typeMask,
missedMeasurementsMask,
compIdx,
sampleIdx,
weights,
((CvCARTTrainParams*) trainParams)->stumpTrainParams );
使用((CvCARTTrainParams*) trainParams)->stumpConstructor()函数建立根结点的stump,一个CvStumpClassifier。Stump是一个树桩分类器(弱分类),根据分枝参数的指定,该分类器可以在同等权重情况下分类选择出分枝个数的特征形成一个多分支的弱分类器。此处的stumpConstructor()在icvCreateCARTStageClassifier()中赋值为:
trainParams.stumpConstructor = cvCreateMTStumpClassifier;
其中的cvCreateMTStumpClassifier()函数定义在cvboost.cpp,建立多阈值桩分类器,原理就是在采样空间中选择出一个特征t,使得t的采样特征值分割采样空间时,获得的检测误差最小(在当前权重下,正采样的正确识别率最大和负采样的错误识别率最大,注意,负采样的错误识别就是正确识别了负采样)。原型为:
CV_BOOST_IMPL CvClassifier* cvCreateMTStumpClassifier(
CvMat * trainData,
int flags,
CvMat * trainClasses,
CvMat * /*typeMask*/,
CvMat * missedMeasurementsMask,
CvMat * compIdx,
CvMat * sampleIdx,
CvMat * weights,
CvClassifierTrainParams* trainParams )
函数中的调用参数为:
trainData, //函数cvCreateCARTClassifier()带入的参数,data->valcache
flags, //数据排列方式(行列方式)
trainClasses, //带入参数,weakTrainVals,其中包含采样样本类型标志1为正采样
typeMask, //带入参数,为0
missedMeasurementsMask, //带入参数,为0
compIdx, //带入参数,为0,表示整个特征空间
sampleIdx, //带入参数,为trimmedIdx,当前使用的采样空间
weights, //带入参数,权重矩阵。data->weights
((CvCARTTrainParams*) trainParams)->stumpTrainParams//训练参数,完整数据
**注意:**trainParams参数的类型变化,带入的类型为CvClassifierTrainParams,强制转换为CvCARTTrainParams类型,然后才能够访问其中的stumpTrainParams分量(类型为CvClassifierTrainParams)。
在cvCreateMTStumpClassifier()的实现中,根据带入的参数计算出一个桩节点stump分类器的对象,并赋值到intnode[0].stump中(在整个采样空间中进行计算,得出根节点的stump)。
下面是函数的实现过程(在cvboost.cpp中),首先测试输入参数的合法性:
CV_Assert ( trainParams != NULL );
CV_Assert ( trainClasses != NULL );
CV_Assert ( CV_MAT_TYPE ( trainClasses->type ) == CV_32FC1 );
CV_Assert ( missedMeasurementsMask == NULL );
CV_Assert ( compIdx == NULL );
然后赋值运算参数:
stumperror = (int) ((CvMTStumpTrainParams*) trainParams)->error;
ydata = trainClasses->data.ptr;
其中stumperror为分类器要求的误差(误差类型,是指用什么算法计算误差,而不是实际误差的大小值,misclass (default) 或 gini 或 entropy),ydata指向实际的训练数据类型(即正采样为1,负采样为0,都是浮点数)。然后根据训练数据的排列方式计算m值和ystep值,其中m为采样数,ystep为每元素的长度(占用的字节数,类型矩阵为m行1列,一行仅一个元素)。
wdata = weights->data.ptr;
为采样值的权重,wdata指向该数据的缓冲区。并且验证权重值的个数与采样数一致,设置wstep为权重值的长度(占用的字节数)。
根据训练参数(CvMTStumpTrainParams*) trainParams)->sortedIdx的数据初始化函数变量:
sortedtype = CV_MAT_TYPE ( ((CvMTStumpTrainParams*) trainParams)->sortedIdx->type );
assert ( sortedtype == CV_16SC1 || sortedtype == CV_32SC1 || sortedtype == CV_32FC1 );
sorteddata = ((CvMTStumpTrainParams*) trainParams)->sortedIdx->data .ptr;
sortedsstep = CV_ELEM_SIZE ( sortedtype );
sortedcstep = ((CvMTStumpTrainParams*) trainParams)->sortedIdx->step;
sortedn = ((CvMTStumpTrainParams*) trainParams)->sortedIdx->rows;
sortedm = ((CvMTStumpTrainParams*) trainParams)->sortedIdx->cols;
其中的sortedtype指定了本函数支持的数据类型。如果sortedIdx=Null则这些参数为函数指定的0值。
初始化函数变量 n 为特征值个数,或者来自trainData或者来自trainParams。
data = trainData->data.ptr;//指向采样数据
cstep = CV_ELEM_SIZE ( trainData->type );//一个采样数据的尺寸(占用字节数);
sstep = trainData->step;//一行/列采样数据的尺寸(向量步长)
datan = n = trainData->cols ;//特征数(此时的trainData指向valcache,是预计算的特征数)
注意:行/列形式是由数据的组织排列方式指定的,由函数输入参数CV_IS_ROW_SAMPLE(flags)指定。如果是行采样,则矩阵行上的每一个元素为一个特征在一个图像上的特征值,而行元素个数为特征值个数,因此datan表示矩阵行元素个数(矩阵列数),每一个列对应一个特征在采样图像上的计算特征值,因此矩阵的行数表示为训练的采样数图像(包括正负图像)。
注意:上面对n值(特征数)进行了赋值:
datan = n = trainData->cols;//特征数(采样数据的行/列数,与计算的特征数)
或
datan = n = trainData->rows;
但是最终无论数据来源如何,n值都是用训练参数给出的值:
n = ((CvMTStumpTrainParams*) trainParams)->numcomp;
并且判定assert ( datan <= n )处理的特征数必需小于等于实际给定的特征数。
根据sampleIdx输入参数初始化采样索引数据:
idxdata = sampleIdx->data.ptr;
idxstep = ( sampleIdx->rows == 1 ) ? CV_ELEM_SIZE ( sampleIdx->type ) : sampleIdx->step;
l = ( sampleIdx->rows == 1 ) ? sampleIdx->cols : sampleIdx->rows;
其中l(字母L)为采样数,与m意义相同。同时根据sorteddata数据分配一个作为滤波的char数组,并设置初始滤波值为1(数字1)。
初始化一个stump(桩分类器,分配一个缓冲)。设置portion变量(每次循环处理的特征值个数),默认值为100。初始的stump参数设置为:
stump->eval = cvEvalStumpClassifier;//求值函数指针
stump->tune = NULL;
stump->save = NULL;
stump->release = cvReleaseStumpClassifier;//释放函数指针
stump->lerror = FLT_MAX;//左分枝误差(初始值为最大浮点数)
stump->rerror = FLT_MAX;//右分支误差
stump->left = 0.0F;//左分支置信度
stump->right = 0.0F;//右分枝置信度
设置训练开始的特征值索引compidx = 0;下面开始进行并行操作的训练(直接执行一个可并行的程序段,如果没有并行功能,则顺序执行该程序段)。程序段如下:
分配一个采样数据矩阵
if( CV_IS_ROW_SAMPLE( flags ) ){
mat = cvMat ( m, portion, CV_32FC1, 0 );
matcstep = CV_ELEM_SIZE ( mat.type );
matsstep = mat.step;
}else{
mat = cvMat ( portion, m, CV_32FC1, 0 );
matcstep = mat.step;
matsstep = CV_ELEM_SIZE ( mat.type );
}
mat.data .ptr = (uchar *) cvAlloc ( sizeof( float ) * mat.rows * mat.cols );
如果数据是过滤后的或分类排序后的采样,则初始化数据的行索引
if( filter != NULL || sortedn < n ){//n为特征数,sortedn为排序后的特征数
t_idx = (int*) cvAlloc ( sizeof( int ) * m );//m为采样数
if( sortedn == 0 || filter == NULL ){
if( idxdata != NULL ){//idxdata为采样索引矩阵
for( ti = 0; ti < l; ti++ ){
//使用已存在的采样索引
t_idx[ti] = (int) *((float*) (idxdata + ti * idxstep));
}
}else{
for( ti = 0; ti < l; ti++ ){
t_idx[ti] = ti;//建立新的采样索引
}
}
}
}
然后设置循环开始的初值
t_compidx = compidx;
compidx += portion;
其中compidx = 0,portion = 100 为默认值。下面是对t_compidx进行的循环
while( t_compidx < n ){
//首先初始化
t_n = portion;
if( t_compidx < datan ){//在预计算的valcache中取值,直接取值计算
t_n = ( t_n < (datan - t_compidx) ) ? t_n : (datan - t_compidx);
t_data = data;
t_cstep = cstep;
t_sstep = sstep;
}else{//重新在采样空间中取值计算,使用mat变量缓冲
t_n = ( t_n < (n - t_compidx) ) ? t_n : (n - t_compidx);
t_cstep = matcstep;
t_sstep = matsstep;
t_data = mat.data .ptr - t_compidx * ((size_t ) t_cstep );
/* calculate components */
((CvMTStumpTrainParams*)trainParams)->getTrainData( &mat,
sampleIdx, compIdx, t_compidx, t_n,
((CvMTStumpTrainParams*)trainParams)->userdata );
}
注意,有些特征值是经过处理的(小于datan特征值索引的,这些值在valcache中,t_data指向valcache),有些需要进行计算(t_data指向mat,每次getTrainData()函数计算portion个特征的值)。此时t_n指定需要处理的特征数,t_compidx指定开始处理的特征序号(索引),t_data指向采样数(计算特征值mat)。而后根据数据类型,调用函数
findStumpThreshold_16s[stumperror]()
findStumpThreshold_32s[stumperror]()
findStumpThreshold_32f[stumperror]()
使用数据
t_data + ti * t_cstep, //指定位置的采样数据(指定特征对指定采样计算的特征值数据)
t_sstep, //采样数据的行步长
wdata, //采样数据的权重数据
wstep, //全重数据的行步长
ydata, //正负采样的标志数据
ystep, //标志数据的行步长
sorteddata + ti * sortedcstep, //指定位置的分类数据(分过类的特征值数据)
sortedsstep, //分类数据的行步长
sortedm, //分类数据的采样数
计算
&lerror,
&rerror,
&threshold,
&left,
&right,
&sumw,
&sumwy,
&sumwyy
因为stumperror指定为默认值,即misclass误差类型,所以函数调用的是对应数据类型的
icvFindStumpThreshold_misc_16s()
icvFindStumpThreshold_misc_32s()
icvFindStumpThreshold_misc_32f()
函数。而对于stumperror指定为gini类型,则计算函数使用
icvFindStumpThreshold_gini_16s()
icvFindStumpThreshold_gini_32s()
icvFindStumpThreshold_gini_32f()。
其原型通过宏定义指向下面函数:
static int icvFindStumpThreshold_##suffix(
uchar * data, //特征值数据(行数为采样数,列数为特征值数)
size_t datastep,//一个特征值行所包含的字节数(行步长,由数据类型确定)
char * wdata, //采样图像的权重
size_t wstep, //权重步长
uchar * ydata, //采样图像的类别(正采样或负采样)
size_t ystep, //类别的步长
uchar * idxdata, //采样的分类索引
size_t idxstep, //分类索引的步长
int num, //总采样数
float* lerror,
float* rerror,
float* threshold,
float* left,
float* right,
float* sumw,
float* sumwy,
float* sumwyy )
其中sumw,sumwy,sumwyy的计算方法为:
*sumw = 0.0F;
*sumwy = 0.0F;
*sumwyy = 0.0F;
for( i = 0; i < num; i++ ){
idx = (int) ( *((type*) (idxdata + i*idxstep)) );
w = (float*) (wdata + idx * wstep);
*sumw += *w; //所有采样的权重和
y = (float*) (ydata + idx * ystep);
wy = (*w) * (*y);//正采样的权重和(因为正采样类型为1,负采样类型0)
*sumwy += wy;
*sumwyy += wy * (*y);
}
*sumw为采样图片的权重和,*sumwy为正采样图片的权重和(y为正负采样标记),sumwyy应该与sumw相同(如果负采样类型为y=-1时y*y=1,否则若负采样类性y=0,则sumwyy=sumwy)。
注意,对于一个特征而言,通过采样及其权重计算出该特征的采样特征值,然后排序特征值(升序),对特征值序列从左到右逐个检测特征值位置点对序列的分割,开始时,第0个元素的左侧特征元素为0个,右侧为全部元素,因此wyl=0,wl=0,wyr=sumwy,wr=sumw。计算序列中的每一个元素的误差(根据stumperror类型)获得该元素的分割误差,比较所有序列元素,获得最小误差的分割点元素,该元素的特征值为当前权重下的threshold,lerror,rerror,left,right。其中的left/right是置信度(分割概率,左边正采样数/总采样数=left,右边正采样数/总采样数=right),表示正采样的正确检测和负采样的错误检测,而lerror/rerror则根据stumperror决定使用的算法(misclass,gini...等)计算。最后得到一个误差使lerror + rerror最小(最佳分割点)。
下面是对指定特征的采样特征值进行误差计算:
1、取得当前特征的一个特征值curval(按图片顺序)。
2、计算权重,初始wyl=0,是因为第一个采样之前没有任何采样,因此左权重和左正采样权重=0
wyr = *sumwy - wyl;//初始值wyl=0,因此wyr=sumwy
wr = *sumw - wl; //初始值wl=0,因此wr=sumw
3、计算curleft和curright
curleft = wyl / wl;//初始值为curleft = 0.0F
curright = wyr / wr;//初始值,因为wr = sumw
4、计算error,这是一个嵌套的宏对于misc算法类型定义为:
float wposl = 0.5F * ( wl + wyl );//初始正采样左权重wposl=0,因为wl和wyl都等于0。
float wposr = 0.5F * ( wr + wyr );//初始正采样右权重wposr=(sumw+sumwy)*0.5
curleft = 0.5F * ( 1.0F + curleft );//初始curleft=0,经过计算curleft变为0.5
curright = 0.5F * ( 1.0F + curright );//初始curright=wyr/wr/2
curlerror = MIN ( wposl, wl - wposl );//MIN((wl+wyl)*0.5F,(wl-wyl)* 0.5F)
currerror = MIN ( wposr, wr - wposr );//MIN((sumw+sumwy)*0.5,(sumw-sumwy)*0.5)
注意,经过第一次计算curleft=0.5,curright=wyr/wr/2,curlerror=0,currerror=(sumw-sumwy)*0.5,这里sumwy>0,此时的currerror意义是所有负采样权重和的一半。对于gini算法,则:
curlerror = 2.0F * wposl * ( 1.0F - curleft );
currerror = 2.0F * wposr * ( 1.0F - curright );
而entropy(熵)算法则
curlerror = currerror = 0.0F;
if( curleft > CV_ENTROPY_THRESHOLD )
curlerror -= wposl * logf ( curleft );
if( curleft < 1.0F - CV_ENTROPY_THRESHOLD )
curlerror -= (wl - wposl) * logf ( 1.0F - curleft );
if( curright > CV_ENTROPY_THRESHOLD )
currerror -= wposr * logf ( curright );
if( curright < 1.0F - CV_ENTROPY_THRESHOLD )
currerror -= (wr - wposr) * logf ( 1.0F - curright );
而sq算法则
/* 计算误差(平方和) */
/* err = sum( w * (y - left(rigt)Val)^2 ) */
curlerror = wyyl + curleft * curleft * wl - 2.0F * curleft * wyl;
currerror = (*sumwyy) - wyyl + curright * curright * wr - 2.0F * curright * wyr;
其中sumwyy相当于sumwy或sumw(对采样类型的初始化,正采样类型为1,负采样类性为0或-1)。在error宏中计算了curleft、curlerror、curright、currerror四个变量。不同算法,对curlerror和currerror的计算有所不同。
注意,当正负采样的类标记不是+1和-1的类别,而是+1和0的标记时,sumw和sumwy的差别是总权重和与正采样权重和的差别,此时在sumwy中负采样的权重被剔除,此时的sumwyy = sumwy而不是sumwyy=sumw(因为负采样标记为0而不是-1)。
判断当前特征值分类误差:
if(curlerror+currerror < (*lerror)+(*rerror)){//注,初始lerror和rerror都是最大浮点数
(*lerror) = curlerror;
(*rerror) = currerror;
*threshold = *curval;
if( i > 0 ){
*threshold = 0.5F * (*threshold + *prevval);//防止浮点数的截断误差影响算法
}
*left = curleft;
*right = curright;
found = 1; \
} \
如果误差更小,则以当前特征值curval作为节点的阈值,对于i>0则取阈值为
*threshold = 0.5F * (*threshold + *prevval);
根据升序排列的原理,threshold<curval。然后更新左权重(对所有特征值等于curval的项相加):
do{
wl += *((float*) (wdata + idx * wstep));
wyl += (*((float*) (wdata + idx * wstep))) * (*((float*) (ydata + idx * ystep)));
wyyl += *((float*) (wdata + idx * wstep)) *
(*((float*) (ydata + idx * ystep))) * (*((float*) (ydata + idx * ystep)));
} while((++i)<num&&(*((float*)(data+(idx=(int)(*((type*)(idxdata+i*idxstep)))) * datastep)) == *curval));
--i;
之所以有--i,是因为循环做了++i的操作,退出循环时i指向的的第一个不等于curval的项,而i应该指向最后一个等于curval的项。最后更新 prevval = curval(注意,wl,wyl和wyyl从一开始的0逐个分界点加到指定的分界点,不是每次从0累加,这也是算法加速的程序结构)
对该特征的每一个采样值进行计算,最后得出一个该特征的Threshold,lerror,rerror,left,right使得lerror,rerror之和最小。
注意 ,Threshold是指定特征在指定采样集中计算出来的特征值(一个采样),该值具有lerror,rerror和最小的属性,此时的left,right分别表示wyl/wl、wyr/wr(左右两边的分类概率),其中左右的分割位置落在采样特征值升序情况下的Threshold值位置上。对于上述过程的循环调用,遍历整个特征空间,就可以找到采样集在当前权重情况下的最小误差特征。
**注意,**整个计算中并不是以特征值的大小来计算误差,而是使用正负采样的权重来进行误差的计算,即对于一个采样的特征值,计算该特征值一侧的正采样和负采样的权重和(一般只计算正采样与总采样的权重)对该特征的所有采样特征值进行误差计算,并找出最小误差,这个误差就是该特征在当前采样权重下的分类误差,阈值和置信度指标(wyl/wl、wyr/wr),可以作为该特征的一个弱分类器使用(即,对一个给定图像计算该特征的特征值,与阈值比较,小于阈值则使用左侧误差和置信度判定该图片的检测结果,否则使用由侧误差和置信度)。
现在我们回溯函数调用的链路:
void cvCreateTreeCascadeClassifier()
CvIntHaarClassifier* icvCreateCARTStageClassifier()
CvClassifier* cvCreateCARTClassifier()
CvClassifier* cvCreateMTStumpClassifier()
icvFindStumpThreshold_gini_32s()或icvFindStumpThreshold_gini_32f()
在函数cvCreateMTStumpClassifier()调用icvFindStumpThreshold_misc_32s()计算指定组特征值的误差参数,并与给定的误差比较,在整个特征空间中对采样集找出误差最小的特征值,对于所有特征值进行操作后,给出最小误差特征值的参数作为stump的分类参数。
注意,icvFindStumpThreshold_misc_32s()对于误差的计算是建立在当前采样权重之下的(采样特征值是升序排列的)。对一个给定的特征进行计算,找出该特征对应的全部采样的特征值并比较该特征各特征值作为分类点的误差,找出一个最小误差的分类点,然后与给定的误差比较确定是否是更小的误差,从而确定该特征是否可以作为stump的分类特征,该特征值作为stump的阈值,误差作为stump的误差,置信度作为stump的置信度(置信度是由分割点左侧的正采样权重所计算的,wl和wyl对每个分割点都重新计算并作为curleft给出)。这个stump实际上是该特征的一个弱分类器。
函数cvCreateMTStumpClassifier()(stumpConstructor)返回一个stump。在函数cvCreateCARTClassifier()中初始被调用建立分类树的根树桩
/* 建立树根,第一个树桩stump */
intnode[0].sampleIdx = sampleIdx;
intnode[0].stump = (CvStumpClassifier*)((CvCARTTrainParams*) trainParams)->stumpConstructor(
trainData, //训练数据,包含valcache
flags,//行列存放标志
trainClasses, //采样对应的类型(正采样为1,负采样为0)
typeMask,
missedMeasurementsMask,
compIdx, //特征索引
sampleIdx, //采样索引(采样集,不一定是全部采样)
weights,//采样权重
((CvCARTTrainParams*) trainParams)->stumpTrainParams );
cart->left[0] = cart->right[0] = 0;//初始化一个stump的置信度
初始化cart的一个stump节点(树根节点intnode[0])。之后根据参数numsplits(trainParams.count)建立根结点的其他分枝节点stump,即使用intnode[i-1]的threshold对采样集进行分割,得到左右两个采样子集,用这两个子集分别获得左右两个stump,循环继续分割,直到指定的分枝数。
注意, 指针函数stumpConstructor()指向cvCreateMTStumpClassifier()函数,其实现的意义是根据指定的采样集(全部采样的一个子集)计算所有特征中误差最小的特征值,并返回该特征的误差(左/右误差),置信度(左/右置信度)和特征索引(指定特征的索引),特征值(阈值),作为stump的参数。
注意, 左右置信度stump->left和stump->right表示如下属性(在作为分类器时表示正确分类的标识)
if( ((CvMTStumpTrainParams*) trainParams)->type == CV_CLASSIFICATION_CLASS ){
stump->left = 2.0F * (stump->left >= 0.5F) - 1.0F;
stump->right = 2.0F * (stump->right >= 0.5F) - 1.0F;
}
计算误差时stump->left表示wyl/wl(在采样阈值分割下的前段正采样的权重与该分割下的前段总采样的权重比),同样stump->right表示wyr/wr(在采样阈值分割下的后段正采样的权重与该分割下的后段总采样的权重比(wyr=wy-wyl、wr=wsum-wl),此处的分段是在采样特征值排序的情况下由阈值分割采样特征值的前后段)。因此可以断定stump->left + stump->right = 1,在变换后,left和right只能是一个等于1,另一个等于-1,1为正确识别,-1位错误识别。对于一个具有最小误差的特征而言,如果其left=1则说明小于该特征值的检测图片正确分类的概率大于错误分类。对于right也是如此。当一张图片通过一个stump分类时,可以给出其正确分类的置信度大小,因此只要置信度=1,无论左右,都表示正确分类。置信度的取值为[-1,+1],如果left=+1则特征值小于阈值的目标(采样)被检测为正确,否则为错误检测(注意,对于正采样,正确检测是人脸目标,此时为正确检测,对于负采样正确检测是人脸目标,此时为错误检测,因为检测时非人脸目标),如果right=+1则特征值大于阈值的目标被检测为正确。
对于分枝的计算,使用采样的左右子集进行分枝stump计算:
listcount = 0;
for( i = 1; i < count; i++ ){... ...}
其中首先调用splitIdxCallback函数指针指定的函数,该指针在初始化时定义为
splitIdxCallback = ((CvCARTTrainParams*) trainParams)->splitIdx;
而splitIdx在函数icvCreateCARTStageClassifier()中初始化为指向icvSplitIndicesCallback()函数,定义如下:
void icvSplitIndicesCallback(
int compidx, //特征索引
float threshold,//阈值
CvMat * idx, //采样索引
CvMat ** left, //分割的采样左子集索引
CvMat ** right,//分割的采样右子集索引
void* userdata )
{
CvHaarTrainingData* data;
CvIntHaarFeatures* haar_features;
int i;
int m;
CvFastHaarFeature* fastfeature;
data = ((CvUserdata*) userdata)->trainingData;
haar_features = ((CvUserdata*) userdata)->haarFeatures;
fastfeature = &haar_features->fastfeature[compidx];
m = data->sum .rows;
*left = cvCreateMat ( 1, m, CV_32FC1 );
*right = cvCreateMat ( 1, m, CV_32FC1 );
(*left)->cols = (*right)->cols = 0;
if( idx == NULL ){//如果采样索引为null,则使用整个采样空间数据
for( i = 0; i < m; i++ ){
if( cvEvalFastHaarFeature( fastfeature, (sum_type*) (data->sum .data.ptr + i * data->sum .step),
(sum_type*) (data->tilted.data.ptr + i * data->tilted.step)) < threshold * data->normfactor.data.fl [i] ){
(*left)->data .fl [(*left)->cols ++] = (float) i;
}else{
(*right)->data .fl [(*right)->cols ++] = (float) i;
}
}
}else{//否则使用给定的索引指定的采样集
uchar * idxdata;
int idxnum;
size_t idxstep;
int index;
idxdata = idx->data.ptr;
idxnum = (idx->rows == 1) ? idx->cols : idx->rows;
idxstep = (idx->rows == 1) ? CV_ELEM_SIZE ( idx->type ) : idx->step;
for( i = 0; i < idxnum; i++ ){
index = (int) *((float*) (idxdata + i * idxstep));
if( cvEvalFastHaarFeature( fastfeature, (sum_type*) (data->sum .data.ptr + index * data->sum .step),
(sum_type*) (data->tilted.data.ptr + index * data->tilted.step)) < threshold * data->normfactor.data.fl [index] ){
(*left)->data .fl [(*left)->cols ++] = (float) index;
}else{
(*right)->data .fl [(*right)->cols ++] = (float) index;
}
}
}
}
此时在调用参数为
intnode[i-1].stump->compidx, //节点特征索引
intnode[i-1].stump->threshold, //节点特征值(索引指定特征的特征值)
intnode[i-1].sampleIdx, //该特征值的采样索引
&lidx, //初始定义为null
&ridx, //初始定义为null
Userdata //数据,包括特征数据和采样数据
该函数根据给定的特征compidx,阈值threshold以及采样集,分解出两个采样子集,分别由lidx和ridx返回,注意,返回的子集是sampleIdx的索引子集,如果sampleIde为null,则使用整个采样集。
icvSplitIndicesCallback()函数通过函数cvEvalFastHaarFeature()计算当前权重下的特征值(是指定特征的特征值),并与阈值进行比较,将该特征的采样特征值分成左右两类,小于阈值的为左类,大于等于的为右类。最后返回左右两个类的采样索引数组。
在函数cvCreateCARTClassifier()中的for循环是针对分支数进行的循环,如果指定的分支数大于1,则进入分支循环,建立stump分枝,在其中进行如下操作,首先计算左误差,此时i-1指向桩节intnode[0],并且通过函数icvSplitIndicesCallback()已经计算出lidx和ridx。intnode[i-1].stump是由stumpConstructor()函数指针(cvCreateMTStumpClassifier())返回的stump对象。根据是否存在lidx或ridx计算子集的stump:
if( intnode[i-1].stump->lerror != 0.0F ){
list[listcount].sampleIdx = lidx;//所有特征值小于根阈值的采样索引
list[listcount].stump = (CvStumpClassifier*)((CvCARTTrainParams*) trainParams)->stumpConstructor(
trainData,
flags,
trainClasses,
typeMask,
missedMeasurementsMask,
compIdx,//特征索引矩阵
list[listcount].sampleIdx,//使用部分采样计算误差最小的特征(找误差最小的特征和值)
weights,
((CvCARTTrainParams*) trainParams)->stumpTrainParams);
list[listcount].errdrop = intnode[i-1].stump->lerror - (list[listcount].stump->lerror + list[listcount].stump->rerror);
list[listcount].leftflag = 1;
list[listcount].parent = i-1;
listcount++;
}else{
cvReleaseMat ( &lidx );
}
整体采样的左误差intnode[i-1].stump->lerror与左半采样的全误差lerror+rerror之差作为errdrop误差(最速下降误差)。分类树节点选出最速下降误差的节点作为树节点。同样对stump的reeror进行检测核计算
if( intnode[i-1].stump->rerror != 0.0F ){
list[listcount].sampleIdx = ridx;
list[listcount].stump = (CvStumpClassifier*)((CvCARTTrainParams*) trainParams)->stumpConstructor(
trainData,
flags,
trainClasses,
typeMask,
missedMeasurementsMask,
compIdx,
list[listcount].sampleIdx,
weights,
((CvCARTTrainParams*) trainParams)->stumpTrainParams );
list[listcount].errdrop = intnode[i-1].stump->rerror - (list[listcount].stump->lerror + list[listcount].stump->rerror);
list[listcount].leftflag = 0;
list[listcount].parent = i-1;
listcount++;
}else{
cvReleaseMat ( &ridx );
}
注意,listcount++;操作,左右两个stump存放在相邻的list中,并且使用leftflag=1/0标记左右。
注意,cvCreateMTStumpClassifier()函数返回stump,是使用指定采样集找出误差最小的特征并求出它的特征值和误差,返回误差值和置信度值以及特征索引和特征值,返回的特征索引指定的特征与前一个stump的特征可能不是一个特征,而此时的误差比较则是两个特征的误差比较,但后者的采样空间是由前者的特征值阈值分割的。函数cvCreateMTStumpClassifier()使用的采样空间可以是全部采样或部分采样,由sampleIdx矩阵决定。
每次循环计算两个stump,分别是由前一个stump的阈值分割指定采样空间(即stump作用的采样空间)所得到的两个(左/右)子采样序列通过函数cvCreateMTStumpClassifier()计算所得。将结果加入list[listcount],并计算list[listcount].errdrop,标记list[listcount].leftflag=(1/0)和list[listcount].parent=i-1。然后在列表list[listcount]中查找最大list[listcount].errdrop(误差下降最快的节点),将其赋值给intnode[i],程序段如下
idx = 0;
maxerrdrop = list[idx].errdrop;
for( j = 1; j < listcount; j++ ) {
if( list[j].errdrop > maxerrdrop ){
idx = j;
maxerrdrop = list[j].errdrop;
}
}
intnode[i] = list[idx];
注意,此时最大maxerrdrop的节点赋值到intnode[i],说明intnode[i]的误差与intnode[i-1]及以前的节点的误差差值最大(误差下降最快),新特征在分割的采样中有更小的误差(分割采样后的stump误差,越小,则errdrop越大)。然后填写cart的置信度(使用节点索引指向节点的置信度)
if( list[idx].leftflag ){
cart->left[list[idx].parent] = i;
}else{
cart->right[list[idx].parent] = i;
}
指向第i个节点的索引。程序判断
if( idx != (listcount - 1) ){
list[idx] = list[listcount - 1];
}
listcount--;
判断errdrop最大节点是不是列表的最后节点(新计算的节点),是则直接退栈,否则赋值覆盖最大节点,然后退栈,这表明一次循环仅产生零或一个列表节点(因为最大节点已经为intnode[i]所指向,故不需要释放内存)。
注意, 最大maxerrdrop的节点不一定是当前stump->lerror和stump->rerror判断所产生的list列表节点之一(程序最终在list链表中剔除误差最大节点,因此,误差最大的值是呈逐渐缩小趋势的,这说明误差最大节点有被分枝的需要,而其他节点则可能是叶节点)。
循环完成之后,生成一个intnode[i]列表,其中的intnode[i].stump是子采样的弱分类器。将其填充到cart的各个分支当中,此时形成的分类器cart各分支的特征是不同的,但它们都表明了指定分枝下采样空间的最小误差属性。注意,cart->left[i]和cart->right[i]分别指向该节点的子节点索引,由下式赋值
cart->left[list[idx].parent] = i;
或
cart->right[list[idx].parent] = i;
其中idx指向具有maxerrdrop的list[]元素,并且该元素作为一个intnode节点加入到intnode[]中。
在建立intnode[]循环中,maxerrdrop不一定是顺序的,可能在一定阶段会回过头来计算前面的分枝,因此,intnode[]是跳跃指向不同分枝的,但是cart->left[i]和cart->right[i]则直接指向子节点索引。
cart->count++;//分支计数
cart->compidx[i] = intnode[i].stump->compidx;//各分支的特征索引(最小误差特征)
cart->threshold [i] = intnode[i].stump->threshold;//各分支的分类阈值(最小误差的特征值)
注意,stump的计算权重数是不变的,所以计算的stump都是当前权重下的基本弱分类器,第一个节点的intnode[0].stump为当前权重下误差最小的分类器根,针对所有采样计算,其后的子节点则是针对部分采样进行计算的。cart树中的每一个叶节点都给出+1/-1的分类结果。
在函数icvCreateCARTStageClassifier()中,函数cvCreateCARTClassifier()返回一个cart分类器(弱分类器,是在一个一定的采样权重下的分类器),该分类器的各个分支给出了具有部分采样误差最小的特征(在当前采样权重下),这是一个具有指定分支数的分类器树(对于指定分支数为1的cart,只有一个桩节点stump,没有子集下的子节点,并且直接给出+1,-1的分类结果)。
使用该分类器的分类结论进行采样空间权重的调整,然后再计算新的cart,形成一个调整采样空间权重下的cart弱分类器序列,从而使检测误差达到minhitrate/maxfalsealarm要求的条件。下面就是这个过程的程序实现。
使用下面函数建立一个haar分类器(空分类器缓冲)
classifier = (CvCARTHaarClassifier*) icvCreateCARTHaarClassifier(numsplits);
其中numsplits是主函数icvCreateCARTStageClassifier()带入的参数(分类器的分枝数)。然后初始化该分类器
icvInitCARTHaarClassifier(classifier, cart, haarFeatures );
使用函数cvCreateCARTClassifier()返回的cart分类器和haarFeatures充填(初始化)classifier。函数原型为:
void icvInitCARTHaarClassifier(
CvCARTHaarClassifier* carthaar,
CvCARTClassifier* cart,
CvIntHaarFeatures* intHaarFeatures)
调用参数为
classifier, //空分类器
cart, //一个stump树(单个弱分类器)
haarFeatures //特征空间
其中cart由函数cvCreateCARTClassifier()给出,haarFeatures是主函数的带入参数。实现过程为
int i;
for( i = 0; i < cart->count ; i++ ){
carthaar->feature[i] = intHaarFeatures->feature[cart->compidx[i]];
carthaar->fastfeature[i] = intHaarFeatures->fastfeature[cart->compidx[i]];
carthaar->threshold[i] = cart->threshold [i];
carthaar->left[i] = cart->left [i];
carthaar->right[i] = cart->right [i];
carthaar->val[i] = cart->val[i];
carthaar->compidx[i] = cart->compidx[i];
}
carthaar->count = cart->count;
carthaar->val[cart->count ] = cart->val[cart->count];
各项意义:
carthaar->feature[i]//分枝特征CvTHaarFeature结构
carthaar->fastfeature[i]//分枝特征CvFastHaarFeature结构
carthaar->threshold[i]//分枝阈值
carthaar->left[i]//左置信度(当>0时为子分枝索引,<=0是为置信度val的索引)
carthaar->right[i]//右置信度(当>0时为子分枝索引,<=0是为置信度val的索引)
carthaar->val[i]//置信度(阈值的权重比wyl/sumw或wyr/sumw,[-1,+1])
carthaar->compidx[i]//特征索引
初始化classifier之后,使用当前分类器计算所有采样的置信度值,生成采样的评估矩阵eval。使用的函数为classifier->eval()(指向函数icvEvalCARTHaarClassifier()):
float icvEvalCARTHaarClassifier( CvIntHaarClassifier* classifier,
sum_type* sum,
sum_type* tilted,
float normfactor )
{
int idx = 0;
do{
if(cvEvalFastHaarFeature(((CvCARTHaarClassifier*) classifier)->fastfeature + idx, sum, tilted) <
(((CvCARTHaarClassifier*) classifier)->threshold [idx] * normfactor)){
idx = ((CvCARTHaarClassifier*) classifier)->left [idx];
}else{
idx = ((CvCARTHaarClassifier*) classifier)->right [idx];
}
}while(idx > 0);
return ((CvCARTHaarClassifier*) classifier)->val[-idx];
}
计算采样的特征值,然后与分类器分枝的阈值比较,并获得进一步的特征值索引,循环计算与比较,直到叶节点(没有进一步的特征索引),此时返回该叶节点索引指定的误差置信度值。eval矩阵就是采样数据相对于当前分类器的一次分类误差置信度值的矩阵。
注意,此时的classifier分类器,是由一系列经过选择的特征(不一定是全部特征,由分枝指定的特征数)组成的分类器,其中的每一个分枝特征,都只是一个初步的stump弱分类器特征(一次循环),初始化的stump除了选择使用的最佳特征之外,还建立了该特征的第一个弱分类器。所选择特征的个数依赖于输入的分支数,也构成了classifier的分枝数。注意,val[-idx]给出的值为[-1,+1],其中-1为错误分类,+1为正确分类(vCreateMTStumpClassifier()函数中给出),对于-1置信度,如果采样为正采样则表示错误分类,如果采样为负则表示正确分类,对于+1,如果采样为正,则表示正确分类,如果采样为负,则表示错误分类,所以,分类的正确与错误,是由两个条件决定的,一个是分类器的返回[-1,+1],一个是采样的正负(人脸目标,非人脸目标)。
在进行进一步的计算中首先生成采样的求值采样数据(因为采样数据是已知的):
for( i = 0; i < numsamples; i++ ){
idx = icvGetIdxAt( sampleIdx, i );
eval.data .fl [idx] = classifier->eval((CvIntHaarClassifier*) classifier,
(sum_type*) (data->sum .data.ptr + idx * data->sum .step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),
data->normfactor.data.fl [idx]);
}
其中eval函数指针指向函数icvEvalCARTHaarClassifier()。 使用函数cvBoostNextWeakClassifier()进行弱分类器的再次迭代,改变采样空间的权重分布,函数定义:
float cvBoostNextWeakClassifier( CvMat * weakEvalVals,
CvMat * trainClasses,
CvMat * weakTrainVals,
CvMat * weights,
CvBoostTrainer* trainer )
{
return nextWeakClassifier[trainer->type](weakEvalVals, trainClasses, weakTrainVals, weights, trainer);
}
调用参数为
&eval, //采样评估数据(置信度指标)
&data->cls, //采样类数据(正采样为1,负采样为0或-1)
weakTrainVals, //弱分类器训练数据(由cvBoostStartTraining()初始化)
&data->weights, //采样权重数据(初始为平均权重值)
trainer //训练数据(由cvBoostStartTraining()生成)
函数nextWeakClassifier[trainer->type]根据训练类型trainer->type指向不同的函数
nextWeakClassifier[4] = {
icvBoostNextWeakClassifierDAB,
icvBoostNextWeakClassifierRAB,
icvBoostNextWeakClassifierLB,
icvBoostNextWeakClassifierGAB
};
本例就默认类型DAB分析函数icvBoostNextWeakClassifierDAB()的实现过程,原型如下
static float icvBoostNextWeakClassifierDAB( CvMat * weakEvalVals, CvMat * trainClasses, CvMat * /*weakTrainVals*/,
CvMat * weights, CvBoostTrainer* trainer)
{
uchar * evaldata;
int evalstep;
int m;
uchar * ydata;
int ystep;
int ynum;
uchar * wdata;
int wstep;
int wnum;
float sumw;
float err;
int i;
int idx;
CV_Assert ( weakEvalVals != NULL );
CV_Assert ( CV_MAT_TYPE ( weakEvalVals->type ) == CV_32FC1 );
CV_Assert ( trainClasses != NULL );
CV_Assert ( CV_MAT_TYPE ( trainClasses->type ) == CV_32FC1 );
CV_Assert ( weights != NULL );
CV_Assert ( CV_MAT_TYPE ( weights ->type ) == CV_32FC1 );
CV_MAT2VEC( *weakEvalVals, evaldata, evalstep, m );
CV_MAT2VEC( *trainClasses, ydata, ystep, ynum );
CV_MAT2VEC( *weights, wdata, wstep, wnum );
CV_Assert ( m == ynum );
CV_Assert ( m == wnum );
sumw = 0.0F;
err = 0.0F;
for( i = 0; i < trainer->count ; i++ ){//采样的检测权重和
idx = (trainer->idx) ? trainer->idx[i] : i;
sumw += *((float*) (wdata + idx*wstep));//总权重
//错误检测的采样权重和(正确检测的正采样和错误检测的负采样权重不添加到err)
err += (*((float*) (wdata + idx*wstep))) *
((*((float*) (evaldata + idx*evalstep))) !=
2.0F * (*((float*) (ydata + idx*ystep))) - 1.0F);
}
err /= sumw;//错误概率
err = -cvLogRatio( err );
for( i = 0; i < trainer->count ; i++ ){
idx = (trainer->idx) ? trainer->idx[i] : i;
//对于错误检测的采样值权重进行更新(正确检测的采样权重保持不变expf(0)=1)
*((float*) (wdata + idx*wstep)) *= expf ( err *
((*((float*) (evaldata + idx*evalstep))) !=
2.0F * (*((float*) (ydata + idx*ystep))) - 1.0F));
sumw += *((float*) (wdata + idx*wstep));//更新后的权重和
}
for( i = 0; i < trainer->count ; i++ ){//归一化处理采样权重,使sumw=1
idx = (trainer->idx) ? trainer->idx[i] : i;
*((float*) (wdata + idx * wstep)) /= sumw;
}
return err;
}
算法首先计算所有采样的权重数之和sumw,和所有错误检测的采样权重之和err。
注意,每一个采样的检测状态标识由eval给出,这是由函数icvEvalCARTHaarClassifier()使用当前cart分类器给出的检测结果。在循环中err的累加过程对于正采样(ydata + idx*ystep)=1,此时如果采样识别正确,则(evaldata + idx*evalstep)=1,因此条件表达式为假=0,反之,如果识别错误,则(evaldata + idx*evalstep)=-1,此时条件表达式为真=1,该采样的权重值将累加到err上。对于负采样(ydata + idx*ystep)=0,此时如果识别错误(即把负样本识别为非人脸),则(evaldata + idx*evalstep)=-1,条件表达式为真=0,反之,如果负采样识别为正确(即把负样本识别为人脸),表明识别错误,因此该负采样的权重也累加到err上。err的计算公式
err=log((err/sumw)/(1-(err/sumw)))
使用结果err修改采样的权重。增加错误识别样本权重量。归一化处理使权重和=1。
函数cvBoostNextWeakClassifier()根据cart的识别结果改变采样集的权重,并返回err(权重变化的基本参数,err是经过log计算后的识别误差),返回数据写入alpha,并计算:
sumalpha += alpha;
for( i = 0; i <= classifier->count; i++ ){
if( boosttype == CV_RABCLASS ){
classifier->val[i] = cvLogRatio(classifier->val[i]);
}
classifier->val[i] *= alpha;
}
注意,对于分类器,val[i]的值为[-1,+1],因此,经过对所有cart分类器分枝的循环,其val[i]的值为[-alpha,+alpha],这个误差值用于计算分类器的最小击中 误差(正确识别正采样的误差)和最大误识负采样的报警误差,此时,采样的权重已经更新,alpha为更新前的分类误差。将当前的cart放入分类器序列中:
cvSeqPush ( seq, (void*) &classifier );
序列seq为弱分类器序列,每一个cart弱分类器由一个一定分支数的stump组成,对于默认的分支数1而言,cart由一个二叉节点组成,其叶节点给出分类检测的结果+1,表示正确目标(人脸),-1表示错误目标(非人脸),分类器的threshold和left/right判断检测结果,lerror/rerror给出检测的误差。
下面是检验已经生成的cart序列中的分类器序列是否能够满足检测要求的误差指标,如果满足则结束生成弱分类器,否则继续。首先计算正采样的识别误差:
numpos = 0;
for( i = 0; i < numsamples; i++ ){//整个采样空间
idx = icvGetIdxAt( sampleIdx, i );
if( data->cls.data .fl [idx] == 1.0F ){//检测正采样
eval.data .fl [numpos] = 0.0F;//所有cart检测结果的和初始化为0
for( j = 0; j < seq->total; j++ ){//使用cart序列检测指定的采样
classifier = *((CvCARTHaarClassifier**) cvGetSeqElem ( seq, j ));
eval.data .fl [numpos] += classifier->eval( (CvIntHaarClassifier*) classifier,
(sum_type*) (data->sum .data.ptr + idx * data->sum .step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),
data->normfactor.data.fl [idx] );//检测结果的误差[-alpha,+alpha]
}
/* eval.data.fl[numpos] = 2.0F * eval.data.fl[numpos] - seq->total; */
numpos++;
}
}
其中的eval.data .fl [numpos]是一个正采样在所有seq->total个classifier分类下的误差值之和,此时的claasifier->eval()函数返回的是classifier->val[idx]值,取[-alpha,+alpha]。对于正确识别的分类器,误差为+alpha,反之为-alpha。注意每一个classifier的alpha值都不一样,其和可能大于0,也可能小于0。最终的numpos值为采样空间中的所有正采样个数。计算eval.data .fl [numpos]的升序排列,和阈值threshold:
icvSort_32f( eval.data .fl , numpos, 0 );//分类排序
threshold = eval.data .fl [(int) ((1.0F - minhitrate) * numpos)];
其中minhitrate为输入的最小命中率,接近于1,因为-alpha是错误识别的误差值,所以一个正采样的eval.data .fl[pos] 值表示综合识别误差,越小表示错误识别的次数越多,越大表示错误识别的次数越少(注意,同一个classifier,对所有采样,其检测的误差值是相同的,只有正负的差别,因此eval.data .fl[i]的值实际上就是所有classifier对第i个采样进行检测后的正确检测次数与错误检测次数的差,每一个classifier[j]总是给出+/-alpha[j])。而(1.0F -- minhitrate)则表示错误识别率(没命中), (1.0F -- minhitrate) * numpos则等于有多少个正采样被错误识别(左侧),表示在eval.data.fl[]中的分割位置threshold。用threshold作为命中率的阈值来分割正采样的命中误差。大于threshold值的采样被判定为正确识别的采样(如果是正采样,则正确检测,如果是负采样则错误检测)。
对于负采样计算误差:
numneg = 0;
numfalse = 0;
for( i = 0; i < numsamples; i++ ){//整个采样空间
idx = icvGetIdxAt( sampleIdx, i );
if( data->cls.data .fl [idx] == 0.0F ){//针对负采样
numneg++;
sum_stage = 0.0F;
for( j = 0; j < seq->total; j++ ){
classifier = *((CvCARTHaarClassifier**) cvGetSeqElem ( seq, j ));
sum_stage += classifier->eval( (CvIntHaarClassifier*) classifier,
(sum_type*) (data->sum .data.ptr + idx * data->sum .step),
(sum_type*) (data->tilted.data.ptr + idx * data->tilted.step),
data->normfactor.data.fl [idx] );
}
/* sum_stage = 2.0F * sum_stage - seq->total; */
if( sum_stage >= (threshold - CV_THRESHOLD_EPS) ){//负采样的识别值大于threshold
numfalse++;//该负采样被识别成人脸目标(错误识别)
}
}
}
falsealarm = ((float) numfalse) / ((float) numneg);//负采样错误识别的概率
对采样空间中的所有负采样计算检测误差classifier->eval(),返回的是classifier的检测误差[-alpha,+alpha],对于负采样(非人脸图像),-alpha表明检测正确,+alpha表明检测错误,而sum_stage为累计误差,越大说明该负采样的检测错误率越大,通过与正采样给出的threshold比较(判别负采样是检测为目标还是非目标)计算出numfalse值,表示采样空间的所有负采样中错误识别的个数。而falsealarm值给出负采样的错误检测比率值。建立弱分类器的循环条件是:
while(falsealarm > maxfalsealarm && (!maxsplits || (num_splits < maxsplits)));
这说明对于负采样计算的falsealarm值限定了建立分类器的循环次数,每一次循环依据当前给定的采样权重建立一个classifier,并加入到分类器序列中,然后计算falsealarm,直到满足falsealarm<= maxfalsealarm或分支数num_splits >= maxsplits。
当num_splits >= maxsplits条件成立时,表明所有特征都已经用完(因为每一个分类器的分枝使用一个特征,但是,要注意,各个分类器的分枝有可能使用相同特征,只是由于采样权重的差别使同样的特征在不同采样子集下满足最小误差条件)。如果循环是以num_splits >= maxsplits条件退出的,此时falsealarm依旧大于maxfalsealarm,因而生成stage失败,也就是在给出的采样空间及误差条件下不能找到满足条件的stage分类器(cart弱分类器系列)。
注意,因为maxsplits为输入参数,此处为0,因此(!maxsplits)=true条件总是成立,因此不能有num_splits>=maxsplits条件成立的事件发生,因此循环总是在falsealarm<=maxfalsealarm时终止。
如果循环在falsealarm<=maxfalsealarm条件下退出,则根据采样空间和给出的误差条件已经找到了一个正确的弱分类器系列,能够组成一个满足条件的强分类器stage。
在循环过程中有一个"剪枝"过程,每次对采样权重的更新都会涉及到有些采样的权重增大,有些采样的权重缩小,在一定次数的权重变换后有些采样的权重可能会缩小到一个可以忽略的范围(1 -- weightfraction,其中weightfraction为指定的权重因子接近于1),此时这些可忽略权重的采样在实际分类器参数计算中起到的作用有限,因此剪除这些采样能够加快分类器参数的计算过程。这个剪枝过程由函数:
trimmedIdx = cvTrimWeights( &data->weights, sampleIdx, weightfraction );
完成,调用参数:
&data->weights, //采样权重数据
sampleIdx, //采样空间索引数据
weightfraction //权重因子,采样剪枝参数为1- weightfraction
返回修正后的采样空间索引trimmedIdx。剪枝过程剪除权重较小的采样数据,形成新的采样空间,实现过程如下:
CV_BOOST_IMPL
CvMat * cvTrimWeights( CvMat * weights, CvMat * idx, float factor )
{
CvMat * ptr = 0;
int i, index, num;
float sum_weights;
uchar * wdata;
size_t wstep;
int wnum;
float threshold;
int count;
float* sorted_weights;
ptr = idx;//指向采样空间索引
sorted_weights = NULL;//用于排序的缓冲区
if( factor > 0.0F && factor < 1.0F ){
size_t data_size;
CV_MAT2VEC( *weights, wdata, wstep, wnum );
num = ( idx == NULL ) ? wnum : MAX ( idx->rows , idx->cols );
data_size = num * sizeof( *sorted_weights );
sorted_weights = (float*) cvAlloc ( data_size );
memset ( sorted_weights, 0, data_size );
sum_weights = 0.0F;
for( i = 0; i < num; i++ ){
index = icvGetIdxAt( idx, i );
sorted_weights[i] = *((float*) (wdata + index * wstep));
sum_weights += sorted_weights[i];
}
icvSort_32f( sorted_weights, num, 0 );//升序排序
sum_weights *= (1.0F - factor);//剪枝权重和(缩小权重总量,factor为保留比率)
i = -1;
do {
sum_weights -= sorted_weights[++i];
}while( sum_weights > 0.0F && i < (num - 1) );//从小头开始循环剪除权重
threshold = sorted_weights[i];//获得保留权重分界线
while( i > 0 && sorted_weights[i-1] == threshold )
i--;//与分界相同的权重都保留
//建立返回的采样空间索引
if( i > 0 || ( idx != NULL && CV_MAT_TYPE ( idx->type ) != CV_32FC1 ) ){
CV_CALL ( ptr = cvCreateMat ( 1, num - i, CV_32FC1 ) );
count = 0;
for( i = 0; i < num; i++ ){
index = icvGetIdxAt( idx, i );
if( *((float*) (wdata + index * wstep)) >= threshold ){
CV_MAT_ELEM ( *ptr, float, 0, count ) = (float) index;
count++;
}
}
assert ( count == ptr->cols );
}
cvFree ( &sorted_weights );
}
return ptr;
}
cvTrimWeights()函数剔除经过循环分类器运算后的权重数较小的采样,然后重新进行弱分类器建立,直到一个系列的弱分类器能够给出满足条件的分类结果为止。
最后判断给定的采样空间是否能够生成一个弱分类器组,满足给定的falsealarm条件。
if( falsealarm > maxfalsealarm ) {
stage = NULL;//找不到合适的分类器组
}else{
stage = (CvStageHaarClassifier*)icvCreateStageHaarClassifier( seq->total,threshold );
cvCvtSeqToArray ( seq, (CvArr *) stage->classifier );
}
函数icvCreateStageHaarClassifier()建立一个指定空间大小的空stage分类器(这是一个强分类器):
datasize = sizeof( *stage ) + sizeof( CvIntHaarClassifier* ) * count;//计算尺寸
stage = (CvStageHaarClassifier*) cvAlloc ( datasize );//分配内存
memset ( stage, 0, datasize );//初始化清空
stage->count = count;//弱分类器classifier数
stage->threshold = threshold;//stage分类器的阈值(正采样分类阈值)
stage->classifier = (CvIntHaarClassifier**) (stage + 1);//若分类器链表指针
stage->eval = icvEvalStageHaarClassifier;
stage->save = icvSaveStageHaarClassifier;
stage->release = icvReleaseStageHaarClassifier;
函数cvCvtSeqToArray ()充填分类器数据,将弱分类器系列seq元素复制到stage->classifier[]中。
函数icvCreateCARTStageClassifier()到此返回一个stage分类器,至此,一个加入级联的强分类器生成完毕,其结构就是由一串弱分类器组成,每个弱分类器使用分支数numsplits指定的特征个数,依据采样权重进行分类(二叉分类),分类器系列从前到后排列,后一个分类器依据前一个分类的误差处理采样的权重值,并根据给定的剪枝权重剔除小权重样本,然后进行threshold和误差计算。整个组合的stage分类器满足给定的minhitrate和maxfalsealarm值(或者达到了指定的分支数限制,注意,因为maxsplits默认为0,因此!maxsplits总为true,循环只能从falsealarm > maxfalsealarm条件为false时退出,即falsealarm <= maxfalsealarm)。
注意,分类器生成时指定的参数,默认值为:
int nstages = 14;//级联stage数
int nsplits = 1;//分支数,表明一个stump仅有一个节点
float minhitrate = 0.995F;//最小命中率,正采样正确识别率
float maxfalsealarm = 0.5F;//最大负采样错误识别警戒
float weightfraction = 0.95F;//剪枝保留的权重因子
int minpos = 500;//最小正采样数
参数nsplits=1表明每一个弱分类的stump桩仅有一个分支,也就是说仅对整个采样空间查找整个特征空间,得到一个识别误差最小的特征,并计算它的threshold及其误差值,threshold作为采样空间的分割阈值,误差作为左右两边的检测误差,left/right取值[-1,+1],表示采样空间的检测结果-1为错误检测,+1为正确检测(注意,正采样的+1检测为正确检测,负采样的+1检测为错误检测,+1检测为目标检测,-1检测为非目标检测)如图:
分支数numsplits=1
分支数numsplits=n
注意,一个cart是同一个权重下的相同或不同特征下的stump序列,各个stump是对上一级stump的一个子采样生成的stump。
建立级联树的节点(类型为CvTreeCascadeNode)single_cluster。single_cluster->stage由函数icvCreateStageHaarClassifier()建立:
single_cluster->stage = (CvStageHaarClassifier*) icvCreateCARTStageClassifier(...)
该函数通过建立一系列cart分类器,形成一个cart系列组成的的强分类器stage,这个stage在指定的采样空间中满足给定的分类误差,对于任意给定图片经过分类器指定的特征计算获得的分类结果val>=stage的threshold值则可判定为正确目标(人脸,具有一定的误差)或val<stage的threshold则可判定为错误目标(非人脸)。
注意,在opencv的训练过程中,stage的生成循环有唯一退出条件falsealarm <= maxfalsealarm,当对给出的样本空间进行训练时,如果这个条件不满足,则生成cart的过程总是进行,只是采样的权重逐渐变化,结合剪枝过程,可以证明循环总能在一定迭代次数后满足falsealarm <= maxfalsealarm条件,因为错误检测的采样权重逐渐被增加,因此被正确识别的概率增加,最终达到正确识别。
下面是对已经建立的stage分类器进行优化,找出使用更少特征的stage组替代这个分类器。初始化下述变量:
single_num = icvNumSplits( single_cluster->stage );//当前stage使用的不同特征个数
best_num = single_num;//最好的特征个数,初始值
best_clusters = 1;//当前stage的聚类数(所有采样特征值都聚成一个集合类)
multiple_clusters = NULL;
其中single_num变量为新建stage分类器中包含的弱分类器数,也就是使用的特征数(注意可能有重复使用的特征)。
注意,stage分类器是一个给定采样空间下的强分类器,其threshold值为stage所包含的所有弱分类器对采样空间分类结果和的排序分割,对于一个采样而言,通过stage的弱分类序列检测获得一串+1和-1的和值,+1为正确检测(采样类,比如人脸,无论采样是否为人脸,检测的+1结果都确认该采样为人脸),-1为错误检测(无论采样是否为人脸,检测的-1结果都确认该采样为非人脸)。stage的threshold值则是全部采样的这个和值的分割点,根据minhitrate判断,因此在stage分类中对于被检测图像进行stage分类计算,获得的值小于threshold,则认定为错误识别0(非人脸),否则为正确识别+1(人脸)。在获得了一个stage分类器后,使用:
if( maxtreesplits >= 0 ){
max_clusters = MIN ( max_clusters, maxtreesplits - total_splits + 1 );
}
试图对新stage进行聚类分析,看一看是不是能分成更简洁的两个或多个stage(形成tcc分类树的同层节点)。聚类分析过程是:
1、使用新stage对正采样进行求值计算,函数icvGetUsedValues()
A、从stage中提取所有不相同的特征索引idx
B、计算正采样数行和特征数列的采样特征值矩阵val(用stage指定的特征计算采样特征值)
C、mornfactor=0的val项设为0值(排除不正常的采样数据)
2、使用cvKMeans2()函数进行聚类均值计算,按参数将val矩阵分成聚类索引形式,即在
cluster->idx矩阵中设置每一个元素对应val值数据的聚类索引,0,1,2...k(对于默认的maxtreesplits=0值,最大cascade分支数为1,不进行聚类分析)。
3、如果某一个聚类的元素个数小于minpos(最小正采样数),则分枝失败(stage不能分裂)
4、对各个聚类从新计算newstage(一个聚类的采样作为正采样,全部负采样作为负采样)
5、判断所有新聚类使用的特征数之和是否小于原stage的特征数(简洁性判断),然后将新形成的stage序列curSplit写入分支列表,其中的项参数为:
curSplit->single_cluster = single_cluster;//原stage
curSplit->multiple_clusters = multiple_clusters;//聚类分成的多stage链表
curSplit->num_clusters = best_clusters;//最佳特征数
curSplit->parent = parent;//原聚类stage的父节点(新聚类stage以该节点为父节点)
curSplit->single_multiple_ratio = (float) single_num / best_num;//使用特征数之比
注意,对于curSplit链表项,需要根据cur_split->num_clusters <= maxtreesplits - total_splits + 1 条件判断释放cur_split->single_cluster或cur_split->multiple_clusters,这样才能在添加树节点时添加正确的stage节点。
/* 选择被分枝的节点*/
do{
float max_single_multiple_ratio;
cur_split = NULL;
max_single_multiple_ratio = 0.0F;
last_split = first_split;
while( last_split ){//依次找出特征比最大的聚类分枝节点
if( last_split->single_cluster && last_split->multiple_clusters &&
last_split->single_multiple_ratio > max_single_multiple_ratio ){
max_single_multiple_ratio = last_split->single_multiple_ratio;
cur_split = last_split;
}
last_split = last_split->next;
}
if( cur_split ){//如果有最大特征比最大的聚类分枝节点
if( maxtreesplits < 0 ||
cur_split->num_clusters <= maxtreesplits - total_splits + 1 ){
//新聚类节点比老节点更好,放弃老节点
cur_split->single_cluster = NULL;
total_splits += cur_split->num_clusters - 1;
}else{
//老节点比新节点好,释放新聚类节点
icvReleaseTreeCascadeNodes( &(cur_split->multiple_clusters) );
cur_split->multiple_clusters = NULL;
}
}
} while( cur_split );//每一个split项中只保留一个聚类节点(或新或老)
最后通过写入AdaBoostCARTHaarClassifier.txt文件的方式将stage节点添加到tcc分类器中,注意,该文件使用前缀和节点索引号进行标识。
/* 连接节点到树 */
leaves = last_node = NULL;
last_split = first_split;//指向聚类分枝链表
while( last_split ){
cur_node = (last_split->multiple_clusters) ? last_split->multiple_clusters : last_split->single_cluster;//取得链表的节点
parent = last_split->parent;//取得节点的父节点指针
if( parent )
parent->child = cur_node;//如果父存在,不为空,则当前节点设置为父节点的子节点
/* 连接同层叶节点,将当前节点和它的聚类节点作为cur_node的同层节点一起连接到树中 */
for( ; cur_node; cur_node = cur_node->next ){
FILE * file;
if( last_node ) //初始设置为null
last_node->next_same_level = cur_node;//设置同层节点(单节点为同层的第一个节点)
else
leaves = cur_node;//第一个新连接到树中的节点设置为循环变量节点
last_node = cur_node;//last_node指向当前节点
cur_node->parent = parent;//设置当前节点的父
cur_node->idx = tcc->next_idx;//设置当前节点的索引号(树节点索引)
tcc->next_idx++;//增加索引号序数
sprintf ( suffix, "%d/%s", cur_node->idx, CV_STAGE_CART_FILE_NAME );//生成文件名
file = NULL;
if( icvMkDir( stage_name ) && (file = fopen ( stage_name, "w" )) != 0 ){
//写入文件
cur_node->stage->save( (CvIntHaarClassifier*) cur_node->stage, file );
fprintf ( file, "\n%d\n%d\n", ((parent) ? parent->idx : -1), ((cur_node->next) ? tcc->next_idx : -1) );
}else{
//文件操作失败
printf ( "Failed to save classifier into %s\n", stage_name );
}
if( file ) fclose ( file );
}
if( parent )
sprintf ( buf, "%d", parent->idx );
else
sprintf ( buf, "NULL" );
printf ( "\nParent node: %s\n", buf );
printf ( "Chosen number of splits: %d\n\n", (last_split->multiple_clusters) ? (last_split->num_clusters - 1) : 0 );
cur_split = last_split;
last_split = last_split->next;//循环聚类分枝链表
cvFree ( &cur_split );//释放聚类分枝连表项
} /* 逐一循环链表的每一项 */
新的节点写入文件后通过装入tcc(读分类器文件)形成新的tcc分类器,通过循环重新对输入的采样进行训练计算,此时的
poscount = icvGetHaarTrainingDataFromVec( training_data, 0, npos, (CvIntHaarClassifier*) tcc, vecfilename, &consumed );
中,tcc含有新生成的分类器节点元素,因此计算出的训练数据是由新tcc过滤的采样集,是能够被tcc正确检测的人脸采样,那些不能被正确检测的人脸采样则被排除掉。这个过程由函数icvEvalTreeCascadeClassifierFilter()完成。同样:
negcount = icvGetHaarTrainingDataFromBG(training_data, poscount, nneg, (CvIntHaarClassifier*) tcc,
&false_alarm, bg_vecfile ? bgfilename : NULL );
也是如此,其中的负采样是由tcc检测为人脸的负采样组成(非人脸采样检测为人脸),这说明采样是检测错误的,因此,函数返回的负采样是当前tcc不能正确检测的负采样。循环终止条件是检测负采样的假报警条件,当negcount返回:
false_alarm < required_leaf_fa_rate
时训练完成。
对于已存在的tcc文件,新给出的采样集都需要经过tcc的过滤,对于不能正确检测的正采样是要被过滤掉的,不是所有给出的正采样都被使用,对于负采样,只有那些错误检测的负采样才能使用(也就是非人脸的采样被检测为人脸)。因此训练过程是一个逐步强化分类器的过程,tcc通过新采样集增加分支数来强化分类过程。然后将新的级联分类器tcc写入xml文件:
/* 级联分类器存入xml文件 */
{
char xml_path[1024];
int len = (int)strlen (dirname);
CvHaarClassifierCascade * cascade = 0;
strcpy ( xml_path, dirname );
if( xml_path[len-1] == '\\' || xml_path[len-1] == '/' )
len--;
strcpy ( xml_path + len, ".xml" );
//在指定目录下装入所有生成的节点txt文件,并根据parent和child关系生成cascade树
cascade = cvLoadHaarClassifierCascade ( dirname, cvSize (winwidth,winheight) );
if( cascade )//保存cascade到xml文件
cvSave ( xml_path, cascade );
cvReleaseHaarClassifierCascade ( &cascade );//释放缓冲
}
最后验证新tcc的性能:
/* 检查cascade的性能 */
tcc->eval = icvEvalTreeCascadeClassifier;//用于采样集的特征求值函数,不是过滤函数
/* 装入采样 */
consumed = 0;
//通过tcc检测正采样数据中有多少个正采样通过检测能正确识别,注意这里使用的eval函数
// icvEvalTreeCascadeClassifier()
poscount = icvGetHaarTrainingDataFromVec( training_data, 0, npos, (CvIntHaarClassifier*) tcc, vecfilename, &consumed );
printf ( "POS: %d %d %f\n", poscount, consumed,(consumed > 0) ? (((float) poscount)/consumed) : 0 );
if( poscount <= 0 )
fprintf ( stderr , "Warning: unable to obtain positive samples\n" );
proctime = -TIME( 0 );
//通过tcc检测有多少负采样被正确识别出来,注意这里使用的eval函数
// icvEvalTreeCascadeClassifier()
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 )
fprintf ( stderr , "Warning: unable to obtain negative samples\n" );
函数icvEvalTreeCascadeClassifier()在性能验证中检测正采样和负采样,如下实现:
float icvEvalTreeCascadeClassifier( CvIntHaarClassifier* classifier, sum_type* sum, sum_type* tilted, float normfactor )
{
CvTreeCascadeNode* ptr;
ptr = ((CvTreeCascadeClassifier*) classifier)->root;//指向分类器的根
while( ptr ){
if( ptr->stage->eval( (CvIntHaarClassifier*) ptr->stage, sum, tilted, normfactor )
>= ptr->stage->threshold - CV_THRESHOLD_EPS ){//判断采样的特征值与stage阈值
ptr = ptr->child;//如果正确,则查看子节点stage
}else{
while( ptr && ptr->next == NULL ) //判断同层节点
ptr = ptr->parent;
if( ptr == NULL )
return 0.0F;//所有节点(包括同层节点)都不正确。返回到根了
ptr = ptr->next;//进入同层节点
}
}
return 1.0F;//有一条路其上所有节点都返回正确,ptr->child=null,到底了。
}
一个被检测图像是正确的目标图像,只有通过tcc后,ptr->child=null,到底了,返回1。而返回0则说明不是目标图像。性能检测过程就是对正采样计算有多少个是正确返回1的,负采样有多少个是正确返回0的。
注意,返回1,是指检测过程在tcc中有一条路直达底部,全都是正确的。而如果没有一条路能够直达底部,则说明目标没有检测到。检测过程遍历tcc树来找出直达底部的路径,全部遍历后还没有找到则视为失败(注意同层过程)。这个逻辑就是stage的级联过程。对于maxtreesplits=0的cascade分类器,因为同层stage->next=null,因此只要有一个stage返回0,这说明检测目标为假,而不必返回到root,这有助于加速输入目标的检测。树状级联分类器的基本结构如下:
其中F/T节点给出stage的检测结果,每一个stage表示一个分类器节点。从根开始如果一个输入能够从检测树中找到一条通向T节点的路径,则分类器树cascade级联给出真结果,如果没有找到通向T节点的路径则给出假结果。注意,一个stagexx返回假结果时,首先查看该stage的下一个同层节点,直到该stage的所有同层节点都返回假结果,再反向上查看其父节点的同层节点,以此类推直到根节点,然后给出假结果。对于最大分枝数为1的cascade树而言,只要有一个stage返回假则自然可以确定结果为假,否则必定为真,因为每一层stage只有一个stage节点,同层节点为null。在上面的算法实现中指定聚类数为1,也就是最大分枝数为0,即可得到这样的cascade,如图:
至此关于树状级联分类器程序cvCreateTreeCascadeClassifier()的分析到此结束。
六、目标识别过程(分类器检测图片中目标的过程)
下面讲解opencv中人脸识别过程。在opencv中通过输入参数指定使用的cascade分类器(*.xml文件)对输入的图片和视频帧进行人脸识别,并给出人脸对应的矩形位置。
在opencv中,首先使用函数face_cascade.load (xml_file)装入cascade分类器,其中xml_file为输入参数指定的cascade分类器文件(这是在上一节中讨论过的级联分类器)。而后处理输入图像:
1、转换图像为灰度图像
2、使用函数cv::equalizeHist (frame_gray, frame_gray)均值化灰度
使用函数:
face_cascade.detectMultiScale (frame_gray, //待识别图像
faces, //返回的人脸矩形位置矢量
1.1, //尺度因子(变换识别窗口尺寸的比例)
2, //最小邻域
0|CV_HAAR_SCALE_IMAGE,//变尺度标记(是否尺度可变)
Size(30, 30));//最小目标尺寸
检测图片中的"人脸",其中人脸位置在faces矢量中返回。这个函数是分类器cascade对象(face_cascade)中的函数,这是对象使用load函数时被初始化的。函数原型为:
void CascadeClassifier ::detectMultiScale(const Mat & image, vector<Rect >& objects,double scaleFactor, int minNeighbors,
int flags, Size minObjectSize, Size maxObjectSize)
{
vector<int> fakeLevels;
vector<double> fakeWeights;
detectMultiScale( image, objects, fakeLevels, fakeWeights, scaleFactor,minNeighbors,flags,minObjectSize, maxObjectSize, false );
}
其中调用了一个重载函数增加了两个矢量参数fakeLevels, fakeWeights和outputRejectLevels=false。这个重载函数真正进入到实际的目标检测过程。在这个检测过程中我们使用的是opencv给出的cascade文件haarcascade_frontalface_alt.xml,检测图片来源于网络为:
右图为源图,中间为灰度图,左图则是在源图上标记的目标检测位置。检测程序首先验证参数的正确性:
CV_Assert ( scaleFactor > 1 && image.depth() == CV_8U );
要求尺度因子大于1,图像深度为CV_8U(灰度图),并且cascade分类器不为空。检查分类器的格式isOldFormatCascade (),本例中opencv给出的分类器为OldFormat 格式。使用函数cvHaarDetectObjectsForROC()检测目标:
CvSeq* cvHaarDetectObjectsForROC (const CvArr* _img,//待检测图像
CvHaarClassifierCascade * cascade, //使用的cascade分类器对象
CvMemStorage* storage,//检测结果返回矢量(元素为矩形)
std ::vector <int>& rejectLevels,//ROC返回
std ::vector <double>& levelWeights,//ROC返回
double scaleFactor, //变尺度因子
int minNeighbors, //最小邻域
int flags,//变尺度标记
CvSize minSize, //最小图片窗口尺寸
CvSize maxSize, //最大图片窗口尺寸
bool outputRejectLevels)//是否计算ROC
程序验证参数的合法性,并设置maxSize:
maxSize.height = img->rows;
maxSize.width = img->cols;
建立一个更有效的内部伴随分类器(create more efficient internal representation of haar classifier cascade)。这个过程由函数icvCreateHidHaarClassifierCascade(cascade)完成。在该函数中,建立一个用于可变特征尺度的临时矩形缓冲(针对分类器中使用的每一个特征)。
在ROC函数中,使用变比因子scaleFactor,采集原图中不同尺寸的矩形图片进行检测:
for( factor = 1; ; factor *= scaleFactor ){
//计算当前使用的窗口尺寸(按尺度因子放大)
CvSize winSize = {cvRound(winSize0.width*factor),cvRound(winSize0.height*factor)};
//待检测图像尺寸(源图像按尺度因子缩小)
CvSize sz = {cvRound(img->cols/factor), cvRound(img->rows/factor)};
//源图像的检测边界(按比例因子)
CvSize sz1 = {sz.width - winSize0.width + 1, sz.height - winSize0.height + 1};
CvRect equRect = {icv_object_win_border, icv_object_win_border, winSize0.width - icv_object_win_border*2,
winSize0.height - icv_object_win_border*2 };
CvMat img1, sum1, sqsum1, norm1, tilted1, mask1;
CvMat* _tilted = 0;
if( sz1.width <= 0 || sz1.height <= 0 )
break;//源图像缩小到检测边界
if( winSize.width > maxSize.width || winSize.height > maxSize.height )
break;//检测窗口已经达到最大
if( winSize.width < minSize.width || winSize.height < minSize.height )
continue;//检测窗口还没有达到开始检测的最小窗口
img1 = cvMat( sz.height, sz.width, CV_8UC1, imgSmall->data.ptr );
sum1 = cvMat( sz.height+1, sz.width+1, CV_32SC1, sum->data.ptr );
sqsum1 = cvMat( sz.height+1, sz.width+1, CV_64FC1, sqsum->data.ptr );
if( tilted ){
tilted1 = cvMat( sz.height+1, sz.width+1, CV_32SC1, tilted->data.ptr );
_tilted = &tilted1;
}
norm1 = cvMat( sz1.height, sz1.width, CV_32FC1, normImg ? normImg->data.ptr : 0 );
mask1 = cvMat( sz1.height, sz1.width, CV_8UC1, temp->data.ptr );
cvResize ( img, &img1, CV_INTER_LINEAR );//缩放源图像
cvIntegral ( &img1, &sum1, &sqsum1, _tilted );//计算和图像,平方和图像和斜图像
//确定扫描范围,这个设定可能会丢失一定的小目标,因为随着缩放比例的增加,小目标在没有//被扫描到的情况下缩小到不可识别了(小于原始识别窗口了,如20x20),因此这一段程序在实//际应用场景中需要考虑,为了增加速度,也增加了风险。
int ystep = factor > 2 ? 1 : 2;
const int LOCS_PER_THREAD = 1000;
int stripCount = ((sz1.width/ystep)*(sz1.height + ystep-1)/ystep + LOCS_PER_THREAD/2)/LOCS_PER_THREAD;
stripCount = std ::min(std ::max(stripCount, 1), 100);
#ifdef HAVE_IPP
if( use_ipp ){
cv::Mat fsum(sum1.rows, sum1.cols, CV_32F, sum1.data.ptr, sum1.step);
cv::Mat (&sum1).convertTo (fsum, CV_32F, 1, -(1<<24));
}else
#endif
cvSetImagesForHaarClassifierCascade (cascade, &sum1, &sqsum1, _tilted, 1.);
cv::Mat _norm1(&norm1), _mask1(&mask1);
cv::parallel_for_ (cv::Range (0, stripCount),
cv::HaarDetectObjects_ScaleImage_Invoker(cascade,
(((sz1.height + stripCount - 1)/stripCount + ystep-1)/ystep)*ystep,
factor, cv::Mat (&sum1), cv::Mat (&sqsum1), &_norm1, &_mask1,
cv::Rect (equRect), allCandidates, rejectLevels, levelWeights, outputRejectLevels, &mtx));
}
其中:
CvSize winSize0 = cascade->orig_window_size;//原始分类器的窗口尺寸20x20
imgSmall = cvCreateMat ( img->rows + 1, img->cols + 1, CV_8UC1 );//待检测图像的缓冲
首先,使用factor逐步改变检测窗口的尺寸,使之满足minSize<winSize<maxSize条件,此时的factor标定位检测图片的缩放因子。sz和sz1是检测图片的两个副本,sz是有原图经过factor缩放而来的图片(采用线性插值),sz1则是在sz的基础上去掉右边界小于原始采样窗口大小的检测图片:
当factor=1时,sz和sz1。equRect是采集窗口去掉边框后的矩形(1,1,18,18)。通过对最小最大检测窗口的条件检测,当factor=1.6105时,开始进入检测循环。
建立检测图片:
cvResize ( img, &img1, CV_INTER_LINEAR );
cvIntegral ( &img1, &sum1, &sqsum1, _tilted );
缩放图片到img1,计算和图像,平方和图像和斜图像,以便在当前因子factor下计算特征值。然后计算stripCount(意义不详)。调用函数:
cvSetImagesForHaarClassifierCascade ( cascade, &sum1, &sqsum1, _tilted, 1. );
建立分类器的特征(特征的缩放比为1,不变)。函数如下:
CV_IMPL void cvSetImagesForHaarClassifierCascade ( CvHaarClassifierCascade * _cascade,
const CvArr* _sum,
const CvArr* _sqsum,
const CvArr* _tilted_sum,
double scale )
{
CvMat sum_stub, *sum = (CvMat*)_sum;
CvMat sqsum_stub, *sqsum = (CvMat*)_sqsum;
CvMat tilted_stub, *tilted = (CvMat*)_tilted_sum;
CvHidHaarClassifierCascade* cascade;
int coi0 = 0, coi1 = 0;
int i;
CvRect equRect;
double weight_scale;
if( !CV_IS_HAAR_CLASSIFIER (_cascade) )
CV_Error ( !_cascade ? CV_StsNullPtr : CV_StsBadArg, "Invalid classifier pointer" );
if( scale <= 0 )
CV_Error ( CV_StsOutOfRange, "Scale must be positive" );
sum = cvGetMat ( sum, &sum_stub, &coi0 );
sqsum = cvGetMat ( sqsum, &sqsum_stub, &coi1 );
if( coi0 || coi1 )
CV_Error ( CV_BadCOI, "COI is not supported" );
if( !CV_ARE_SIZES_EQ( sum, sqsum ))
CV_Error ( CV_StsUnmatchedSizes, "All integral images must have the same size" );
if( CV_MAT_TYPE(sqsum->type) != CV_64FC1 || CV_MAT_TYPE(sum->type) != CV_32SC1 )
CV_Error ( CV_StsUnsupportedFormat,
"Only (32s, 64f, 32s) combination of (sum,sqsum,tilted_sum) formats is allowed" );
if( !_cascade->hid_cascade )
icvCreateHidHaarClassifierCascade(_cascade);
cascade = _cascade->hid_cascade;
if( cascade->has_tilted_features ){
tilted = cvGetMat (tilted, &tilted_stub, &coi1 );
if( CV_MAT_TYPE(tilted->type) != CV_32SC1 )
CV_Error ( CV_StsUnsupportedFormat, "Only (32s, 64f, 32s) combination of (sum,sqsum,tilted_sum) formats is allowed" );
if( sum->step != tilted->step )
CV_Error ( CV_StsUnmatchedSizes, "Sum and tilted_sum must have the same stride (step, widthStep)" );
if( !CV_ARE_SIZES_EQ( sum, tilted ))
CV_Error ( CV_StsUnmatchedSizes, "All integral images must have the same size" );
cascade->tilted = *tilted;
}
_cascade->scale = scale;
_cascade->real_window_size .width=cvRound(_cascade->orig_window_size.width *scale);
_cascade->real_window_size .height=cvRound(_cascade->orig_window_size.height*scale);
cascade->sum = *sum;
cascade->sqsum = *sqsum;
equRect.x = equRect.y = cvRound(scale);
equRect.width = cvRound((_cascade->orig_window_size.width-2)*scale);
equRect.height = cvRound((_cascade->orig_window_size.height-2)*scale);
weight_scale = 1./(equRect.width*equRect.height);
cascade->inv_window_area = weight_scale;
cascade->p0 = sum_elem_ptr(*sum, equRect.y, equRect.x);
cascade->p1 = sum_elem_ptr(*sum, equRect.y, equRect.x + equRect.width );
cascade->p2 = sum_elem_ptr(*sum, equRect.y + equRect.height, equRect.x );
cascade->p3 = sum_elem_ptr(*sum, equRect.y + equRect.height, equRect.x + equRect.width );
cascade->pq0 = sqsum_elem_ptr(*sqsum, equRect.y, equRect.x);
cascade->pq1 = sqsum_elem_ptr(*sqsum, equRect.y, equRect.x + equRect.width );
cascade->pq2 = sqsum_elem_ptr(*sqsum, equRect.y + equRect.height, equRect.x );
cascade->pq3 = sqsum_elem_ptr(*sqsum, equRect.y + equRect.height, equRect.x + equRect.width );
/* 根据实际检测窗口大小和给定的图像指针初始化haar特征指针*/
for( i = 0; i < _cascade->count ; i++ ){//对count个stage分类器进行循环
int j, k, l;
for( j = 0; j < cascade->stage_classifier[i].count; j++ ){//对cart进行循环
for(l = 0;l<cascade->stage_classifier[i].classifier[j].count;l++){stump循环
CvHaarFeature * feature = &_cascade->stage_classifier[i].classifier[j].haar_feature [l];
/* CvHidHaarClassifier* classifier = cascade->stage_classifier[i].classifier + j; */
CvHidHaarFeature* hidfeature = &cascade->stage_classifier[i].classifier[j].node[l].feature;
double sum0 = 0, area0 = 0;
CvRect r[3];
int base_w = -1, base_h = -1;
int new_base_w = 0, new_base_h = 0;
int kx, ky;
int flagx = 0, flagy = 0;
int x0 = 0, y0 = 0;
int nr;
/* 块对齐 */
for( k = 0; k < CV_HAAR_FEATURE_MAX; k++ ){
if( !hidfeature->rect[k].p0 )
break;
r[k] = feature->rect[k].r;//分类器原始特征矩形
base_w = (int)CV_IMIN((unsigned)base_w,(unsigned)(r[k].width-1));
base_w = (int)CV_IMIN((unsigned)base_w,(unsigned)(r[k].x-r[0].x-1));
base_h = (int)CV_IMIN( (unsigned)base_h, (unsigned)(r[k].height-1) );
base_h = (int)CV_IMIN((unsigned)base_h,(unsigned)(r[k].y-r[0].y-1));
}
nr = k;//矩形特征块计数
base_w += 1;
base_h += 1;
kx = r[0].width / base_w;
ky = r[0].height / base_h;
if( kx <= 0 ){
flagx = 1;
new_base_w = cvRound( r[0].width * scale ) / kx;
x0 = cvRound( r[0].x * scale );
}
if( ky <= 0 ){
flagy = 1;
new_base_h = cvRound( r[0].height * scale ) / ky;
y0 = cvRound( r[0].y * scale );
}
for( k = 0; k < nr; k++ ){
CvRect tr;
double correction_ratio;
if( flagx ){
tr.x = (r[k].x - r[0].x) * new_base_w / base_w + x0;
tr.width = r[k].width * new_base_w / base_w;
}else{
tr.x = cvRound( r[k].x * scale );
tr.width = cvRound( r[k].width * scale );
}
if( flagy ){
tr.y = (r[k].y - r[0].y) * new_base_h / base_h + y0;
tr.height = r[k].height * new_base_h / base_h;
}else{
tr.y = cvRound( r[k].y * scale );
tr.height = cvRound( r[k].height * scale );
}
correction_ratio = weight_scale * (!feature->tilted ? 1 : 0.5);
if( !feature->tilted ){
hidfeature->rect[k].p0=sum_elem_ptr(*sum,tr.y,tr.x);
hidfeature->rect[k].p1=sum_elem_ptr(*sum,tr.y,tr.x + tr.width);
hidfeature->rect[k].p2=sum_elem_ptr(*sum,tr.y+tr.height,tr.x);
hidfeature->rect[k].p3=sum_elem_ptr(*sum,tr.y+tr.height,tr.x+tr.width);
}else{
hidfeature->rect[k].p2=sum_elem_ptr(*tilted,tr.y+tr.width,tr.x+tr.width);
hidfeature->rect[k].p3=sum_elem_ptr(*tilted,tr.y+tr.width+tr.height,tr.x+tr.width-tr.height);
hidfeature->rect[k].p0 = sum_elem_ptr(*tilted, tr.y, tr.x);
hidfeature->rect[k].p1 = sum_elem_ptr(*tilted,tr.y+tr.height,tr.x-tr.height);
}
hidfeature->rect[k].weight = (float)(feature->rect[k].weight * correction_ratio);
if( k == 0 )
area0 = tr.width * tr.height;
else
sum0 += hidfeature->rect[k].weight * tr.width * tr.height;
}
hidfeature->rect[0].weight = (float)(-sum0/area0);
} /* l */
} /* j */
}
}
该函数通过scale参数整理分类器的特征weight,注意,特征的缩放不是等比例缩放,比如haar_x2特征,原始w=2,h=1,占两个像素,这说明其缩放规则是x方向为2的倍数缩放,y方向则以1的倍数缩放,因此对于w=6,h=3,也属于haar_x2的一个变异特征,此时dx=2,dy=3。这个函数计算分类器中使用的分类特征在当前图像缩放比例下的初始检测窗口内特征矩形的点位置值(和图像和平方和图像下),以及黑白特征点位所占的比例(由于是图片缩放,因此这里的scale=1,所有特征的值可以直接从特征矩形的p点中进行计算,不需要重新定位)。
通过建立HaarDetectObjects_ScaleImage_Invoker类对象来并行执行特征检测过程:
HaarDetectObjects_ScaleImage_Invoker( const CvHaarClassifierCascade * _cascade,
int _stripSize, double _factor,
Mat & _sum1, const Mat & _sqsum1, Mat * _norm1,
Mat * _mask1, Rect _equRect, std ::vector <Rect >& _vec,
std ::vector <int>& _levels, std ::vector <double>& _weights,
bool _outputLevels, Mutex *_mtx )
{
cascade = _cascade;
stripSize = _stripSize;
factor = _factor;
sum1 = _sum1;
sqsum1 = _sqsum1;
norm1 = _norm1;
mask1 = _mask1;
equRect = _equRect;
vec = &_vec;
rejectLevels = _outputLevels ? &_levels : 0;
levelWeights = _outputLevels ? &_weights : 0;
mtx = _mtx;
}
注意,在进入建立类对象之前,待检测图像按比例factor进行了缩放(图像缩小),分类特征是指在一个检测窗口内指定位置和大小的特征模板,由两个或三个矩形描述(在分类器对象中,指的是原始检测窗口的位置和大小,因为scale=1是常量参数)。
注意,HaarDetectObjects_ScaleImage_Invoker是对待检测图像进行尺度变换(按比例缩放),并对缩放后的图像进行等尺寸检测窗口(原始窗口)下的分类器特征计算,检测其中的人脸,每一次缩放都进行窗口扫描,确定人脸矩形位置(分类器返回)。
在对象中执行operator()运算对图像目标进行检测(对一个缩放比例下的待检测图片进行扫描,检测出所有满足分类器条件的窗口矩形位置):
初始化检测参数:
Size winSize0 = cascade->orig_window_size;
Size winSize(cvRound(winSize0.width *factor), cvRound(winSize0.height *factor));
int y1 = range.start *stripSize, y2 = min (range.end *stripSize, sum1.rows - 1 - winSize0.height);
if (y2 <= y1 || sum1.cols <= 1 + winSize0.width)
return;
Size ssz(sum1.cols - 1 - winSize0.width , y2 - y1);
int x, y, ystep = factor > 2 ? 1 : 2;
其中winSize0为分类器原始检测窗口尺寸,所有特征均以此窗口进行定位。winSize是对应待检测图片缩放比例后,原始图片对应的检测窗口尺寸(在原始图片中定位人脸的窗口尺寸)。y1和y2是开始和终止扫描行计数。sum1.cols-winSize0.width为扫描宽度。ssz的宽和高表示扫描计数。ystep为扫描步长(根据缩放比例因子确定是1像素还是2像素,因为浮点运算有一个截断误差,对于小于2的factor,ystep=1可能存在重复计算)。
循环扫描检测:
for( y = y1; y < y2; y += ystep )
for( x = 0; x < ssz.width ; x += ystep ){
double gypWeight;
int result = cvRunHaarClassifierCascadeSum( cascade, cvPoint(x,y), gypWeight, 0 );
if( rejectLevels ){
if( result == 1 )
result = -1*cascade->count;
if( cascade->count + result < 4 ){
mtx->lock();
vec->push_back (Rect (cvRound(x*factor), cvRound(y*factor), winSize.width , winSize.height));
rejectLevels->push_back (-result);
levelWeights->push_back (gypWeight);
mtx->unlock();
}
}else{
if( result > 0 ){
mtx->lock();
vec->push_back (Rect (cvRound(x*factor), cvRound(y*factor), winSize.width , winSize.height));
mtx->unlock();
}
}
}
通过调用函数:
int result = cvRunHaarClassifierCascadeSum( cascade, cvPoint(x,y), gypWeight, 0 );
对点(x,y)位置的检测窗口进行检测,result>0则检测通过,该位置为人脸矩形,否则为非人脸矩形。对于人脸矩形,则存入vec矢量队列。函数cvRunHaarClassifierCascadeSum()定义如下:
static int cvRunHaarClassifierCascadeSum( const CvHaarClassifierCascade * _cascade,
CvPoint pt, double& stage_sum, int start_stage )
{
bool haveSSE2 = cv::checkHardwareSupport (CV_CPU_SSE2);
int p_offset, pq_offset;
int i, j;
double mean, variance_norm_factor;
CvHidHaarClassifierCascade* cascade;
if( !CV_IS_HAAR_CLASSIFIER (_cascade) )
CV_Error ( !_cascade ? CV_StsNullPtr : CV_StsBadArg, "Invalid cascade pointer" );
cascade = _cascade->hid_cascade;
if( !cascade )
CV_Error ( CV_StsNullPtr, "Hidden cascade has not been created.\n" ,"Use cvSetImagesForHaarClassifierCascade" );
if( pt.x < 0 || pt.y < 0 ||
pt.x + _cascade->real_window_size .width >= cascade->sum .width ||
pt.y + _cascade->real_window_size .height >= cascade->sum .height )
return -1;
p_offset = pt.y * (cascade->sum .step/sizeof(sumtype)) + pt.x;
pq_offset = pt.y * (cascade->sqsum.step/sizeof(sqsumtype)) + pt.x;
mean = calc_sum(*cascade,p_offset)*cascade->inv_window_area;
variance_norm_factor = cascade->pq0[pq_offset] - cascade->pq1[pq_offset] - cascade->pq2[pq_offset] + cascade->pq3[pq_offset];
variance_norm_factor = variance_norm_factor*cascade->inv_window_area - mean*mean;
if( variance_norm_factor >= 0. )
variance_norm_factor = sqrt (variance_norm_factor);
else
variance_norm_factor = 1.;
初始化计算point(x,y)位置上的检测窗口参数。然后进入循环扫描:
if(haveSSE2){//old SSE optimization
for(i = start_stage; i < cascade->count ; i++ ){//start_stage为带入的常量参数0
__m128d vstage_sum = _mm_setzero_pd();
if( cascade->stage_classifier[i].two_rects ){
for( j = 0; j < cascade->stage_classifier[i].count; j++ ){
CvHidHaarClassifier* classifier=cascade->stage_classifier[i].classifier + j;
CvHidHaarTreeNode* node = classifier->node;
// ayasin - NHM perf optim. Avoid use of costly flaky jcc
__m128d t = _mm_set_sd (node->threshold *variance_norm_factor);
__m128d a = _mm_set_sd (classifier->alpha[0]);
__m128d b = _mm_set_sd (classifier->alpha[1]);
__m128d sum = _mm_set_sd (calc_sum(node->feature.rect[0],p_offset) * node->feature.rect[0].weight + calc_sum(node->feature.rect[1],p_offset) * node->feature.rect[1].weight);
t = _mm_cmpgt_sd (t, sum);
vstage_sum = _mm_add_sd (vstage_sum, _mm_blendv_pd(b, a, t));
}
}else{
for( j = 0; j < cascade->stage_classifier[i].count; j++ ){
CvHidHaarClassifier* classifier=cascade->stage_classifier[i].classifier + j;
CvHidHaarTreeNode* node = classifier->node;
// ayasin - NHM perf optim. Avoid use of costly flaky jcc
__m128d t = _mm_set_sd (node->threshold *variance_norm_factor);
__m128d a = _mm_set_sd (classifier->alpha[0]);
__m128d b = _mm_set_sd (classifier->alpha[1]);
double _sum = calc_sum(node->feature.rect[0],p_offset) * node->feature.rect[0].weight;
_sum += calc_sum(node->feature.rect[1],p_offset) * node->feature.rect[1].weight;
if( node->feature.rect[2].p0 )
_sum += calc_sum(node->feature.rect[2],p_offset) * node->feature.rect[2].weight;
__m128d sum = _mm_set_sd (_sum);
t = _mm_cmpgt_sd (t, sum);
vstage_sum = _mm_add_sd (vstage_sum, _mm_blendv_pd(b, a, t));
}
}
__m128d i_threshold = _mm_set1_pd (cascade->stage_classifier[i].threshold);
if( _mm_comilt_sd (vstage_sum, i_threshold) )
return -i;
}
}
return 1;
这里仅对符合haveSSE2条件的分枝进行展示,按照cascade给出的特征计算特征值并比较threshold,获得特征的分类结果,综合判定该特征的分类结果。比较每一个stump节点特征,每一个cart弱分类特征,累加获得对应stage分类特征,所有分类特征都通过后返回1作为分类结果,所有分类小于0的结果都表示非人脸结果。
Opencv还提供HaarDetectObjects_ScaleCascade_Invoker类的实现,表示对分类器特征进行比例缩放,并对源待检测图片进行扫描分类的方法。
总结,在分类器识别过程中,有两种方法,它们是等价的,一是对待测试图片进行缩小,逐步按指定扫描粗糙度比例缩小图片直到缩小到小于等于检测窗口大小为止,每次变尺度后都进行检测窗口扫描,并记录检测正确的结果(矩形)。另一种是待检测图片不变,对分类器检测窗口进行放大,直到在一个方向上(x,y)大于待检测图片为止,放大粗糙度由指定的比例因子确定,每一次尺度改变,都进行窗口扫描,获得指定位置的分类结果(矩形),记录正确结果窗口位置。
检测矩形的过滤:
最终,通过检测对象类,获得所有正确检测的矩形数组矢量allCandidates,在这个数组的矩形中包含了所有可能的人脸目标矩形。对于图片中的同一个人脸目标,在allCandidates中可能对应一系列的目标矩形,因此需要对所有矩形进行分组,以分组的方式确定图片中目标的个数,同一分组内的矩形表示同一个目标。对allCandidates的分组算法由函数:
groupRectangles (rectList, rweights, std ::max(minNeighbors, 1), GROUP_EPS);
完成。其中rectList为待分组的目标矩形,本例中共有42个矩形元素。该函数调用它的一个重载函数,下面是这个重载函数的实现过程:
void groupRectangles(vector<Rect >& rectList, //矩形列表
int groupThreshold, //矩形误差阈值
double eps, //矩形分种类误差
vector<int>* weights, //种类权重(矩形个数)
vector<double>* levelWeights)
{
if(groupThreshold <= 0 || rectList.empty()){
if(weights){
size_t i, sz = rectList.size();
weights->resize (sz);
for( i = 0; i < sz; i++ )
(*weights)[i] = 1;
}
return;
}
vector<int> labels;
int nclasses=partition(rectList,labels,SimilarRects (eps));//区分矩形种类eps为相似度
//对所属同类矩形进行调整计算,确定最终目标的位置和大小
vector<Rect > rrects(nclasses);
vector<int> rweights(nclasses, 0);
vector<int> rejectLevels(nclasses, 0);
vector<double> rejectWeights(nclasses, DBL_MIN);
int i, j, nlabels = (int)labels.size();
for(i = 0; i < nlabels; i++){//计算每一类矩形的种类矩形的总和值(为计算平均值做准备)
int cls = labels[i];
rrects[cls].x += rectList[i].x;
rrects[cls].y += rectList[i].y;
rrects[cls].width += rectList[i].width;
rrects[cls].height += rectList[i].height;
rweights[cls]++;
}
if (levelWeights && weights && !weights->empty () && !levelWeights->empty()){
for(i = 0; i < nlabels; i++){
int cls = labels[i];
if((*weights)[i] > rejectLevels[cls]){
rejectLevels[cls] = (*weights)[i];
rejectWeights[cls] = (*levelWeights)[i];
}else if(((*weights)[i]==rejectLevels[cls])&&((*levelWeights)[i]> rejectWeights[cls]))
rejectWeights[cls] = (*levelWeights)[i];
}
}
for( i = 0; i < nclasses; i++ ){//计算每一类矩形的平均值
Rect r = rrects[i];
float s = 1.f/rweights[i];
rrects[i] = Rect (saturate_cast<int>(r.x *s), saturate_cast<int>(r.y *s), saturate_cast<int>(r.width *s), saturate_cast<int>(r.height *s));
}
rectList.clear();
if( weights )
weights->clear();
if( levelWeights )
levelWeights->clear();
for( i = 0; i < nclasses; i++ ){//检查矩形是否完整包含(排除被包含的矩形)
Rect r1 = rrects[i];
int n1 = levelWeights ? rejectLevels[i] : rweights[i];
double w1 = rejectWeights[i];
if( n1 <= groupThreshold )
continue;
// 过滤所有包含在大矩形中的小目标矩形
for( j = 0; j < nclasses; j++ ){
int n2 = rweights[j];
if( j == i || n2 <= groupThreshold )
continue;
Rect r2 = rrects[j];
int dx = saturate_cast<int>( r2.width * eps );
int dy = saturate_cast<int>( r2.height * eps );
if( i != j &&
r1.x >= r2.x - dx &&
r1.y >= r2.y - dy &&
r1.x + r1.width <= r2.x + r2.width + dx &&
r1.y + r1.height <= r2.y + r2.height + dy &&
(n2 > std ::max(3, n1) || n1 < 3) )
break;
}
if( j == nclasses ){//合格的矩形被保留
rectList.push_back (r1);
if( weights )
weights->push_back (n1);
if( levelWeights )
levelWeights->push_back (w1);
}
}
}
在这个函数中,首先调用函数partition()划分矩形序列的归属种类,其中使用一个误差值确定两个矩形的相似度。partition()函数的实现过程如下:
// 这个函数划分或设置输入序列元素为多个等价的种类,并返回一个从0开始的标签矢量,表示序列// 中每一个元素所属的种类索引。
// predicate(a,b) 函数返回true 如果两个元素属于相同的种类。
// 这个算法在"算法介绍"中由Cormen, Leiserson and Rivest给出,见"不相交集的数据结构"一章
template<typename _Tp, class _EqPredicate> int
partition( const vector<_Tp>& _vec, vector<int>& labels, _EqPredicate predicate=_EqPredicate())
{
int i, j, N = (int)_vec.size();
const _Tp* vec = &_vec[0];
const int PARENT=0;
const int RANK=1;
vector<int> _nodes(N*2);
int (*nodes)[2] = (int(*)[2])&_nodes[0];
// 首先 O(N) 阶: 建立 N 个单顶点树
for(i = 0; i < N; i++){//初始化树节点
nodes[i][PARENT]=-1;
nodes[i][RANK] = 0;
}
// 主循环O(N^2)阶: 合并关联项
for( i = 0; i < N; i++ ){
int root = i;
// 查找第i个元素的根
while( nodes[root][PARENT] >= 0 )
root = nodes[root][PARENT];
for( j = 0; j < N; j++ ){
if( i == j || !predicate(vec[i], vec[j]))//比较i,j元素
continue;
int root2 = j;//如果同种类
while( nodes[root2][PARENT] >= 0 )//查找第j各元素的根
root2 = nodes[root2][PARENT];
if( root2 != root ){//两个同类元素集有不同的根,需要合并
// 合并两个树,按照根元素的排名rank确定合并后的根,并增加其排名
int rank = nodes[root][RANK], rank2 = nodes[root2][RANK];
if( rank > rank2 )
nodes[root2][PARENT] = root;//排名已经大于
else{
nodes[root][PARENT] = root2;
nodes[root2][RANK] += rank == rank2;//增加排名,等于增1否则0
root = root2;
}
assert ( nodes[root][PARENT] < 0 );
int k = j, parent;
// 改变树node2到根的路径,指向新根
while( (parent = nodes[k][PARENT]) >= 0 ){
nodes[k][PARENT] = root;
k = parent;
}
// 改变树node到根的路径,指向新根
k = i;
while( (parent = nodes[k][PARENT]) >= 0 ){
nodes[k][PARENT] = root;
k = parent;
}
}
}
}
//通过这个循环,将nodes划分成同类树,有多少棵树就有多少个矩形种类
// 最后O(N)阶: 枚举种类(根据nodes的树状结构)
labels.resize (N);
int nclasses = 0;
for( i = 0; i < N; i++ ){
int root = i;
while( nodes[root][PARENT] >= 0 )//找到节点的根节点索引root
root = nodes[root][PARENT];
// 复用rank值作为种类标签
if( nodes[root][RANK] >= 0 )
nodes[root][RANK] = ~nclasses++;//0的非操作=-1,非是按位操作,取反
labels[i] = ~nodes[root][RANK];//注意,-1的非操作=0,不是+1
}
return nclasses;
}
函数predicate(vec[i], vec[j])比较两个矩形,返回true,如果两个矩形相似,否则返回false。比较过程是:
inline bool operator()(const Rect & r1, const Rect & r2) const
{
double delta = eps*(std ::min(r1.width , r2.width ) + std ::min(r1.height , r2.height))*0.5;
return std ::abs(r1.x - r2.x ) <= delta &&
std ::abs(r1.y - r2.y ) <= delta &&
std ::abs(r1.x + r1.width - r2.x - r2.width ) <= delta &&
std ::abs(r1.y + r1.height - r2.y - r2.height ) <= delta;
}
delta取两个矩形的最小宽高和的一半,eps=0.2是建立比较对象时带入的误差值。然后比较两个矩形的错位是否在delta范围内。Partition算法通过归并同类矩形到相同根类将矩形集划分成不同的根类,并进行标号。nclasses返回矩形分类数,labels[]指定了每一个矩形所属的分类标号。根据labels的索引,计算每一类矩形的平均值和权重度(矩形个数),当权重数小于给定的阈值(最小邻域参数)时,说明该识别矩形的目标是干扰目标。然后检查类矩形的包含条件:
r1.x >= r2.x - dx &&
r1.y >= r2.y - dy &&
r1.x + r1.width <= r2.x + r2.width + dx &&
r1.y + r1.height <= r2.y + r2.height + dy &&
(n2 > std ::max(3, n1) || n1 < 3) )
其中:
int dx = saturate_cast<int>( r2.width * eps );
int dy = saturate_cast<int>( r2.height * eps );
如果满足包含条件r1被r2所包含(完全在r2范围内),则放弃该类矩形r1。否则将r1作为一个目标矩形存储到返回序列中。
返回序列中的矩形个数(每个矩形类的平均矩形)表示图片中被识别出的目标数。
七、结语
前面各节讲述了关于采样的生成,关于分类器训练过程及其分类原理,特征及其采样特征值计算,分类器训练算法等各个环节的程序实现过程,其中分类器的训练过程及其算法实现是最复杂的和费时的过程,分类器之所以叫做级联分段 分类器cascadestageclassifier,其本质在于stage,而级联过程则是按照训练集的先后,将各个分段串接在一起的过程(树构造过程)。每一个分段stage都是一个训练集在前面的cascade过滤下,对cascade的一次增强,即对cascade不能正确识别的负采样和能够正确识别的正采样进行增强训练,得到新的stage,连接到cascade上,形成新的增强版的cascade。分类器随着stage的增加,识别性能得到不断强化(每一个分段stage是对提供的采样集进行识别,能够满足给定的误差条件,采样集涵盖的场景范围决定了stage能够处理的场景范围,因此一个stage仅仅是一定场景下的stage。实际应用中,要想适合广泛的场景,就需要给出广泛场景下的采样集)。
识别过程则是对不断缩小的图片或不断放大的检测窗口特征使用检测窗口对图片进行一定粒度的扫描,并将正确的目标检测结果进行记录,归类,过滤等操作,最终确定图片中包含的检测目标。
从使用的harr特征可知,harr特征是对采样窗口进行x,y两个方向平移和缩放的。分类器并不使用所有harr特征,而是根据采样集,挑选出最具代表性的(分类误差最小的)特征,不同的采样集,选择的特征也不尽相同。Harr特征并不是针对人脸设计的特征组,而是可以用于任何目标识别(任何特定目标类)的特征组,你也可以设计关于特定目标类的自有特征组,然后根据特定目标收集采样数据(正负采样数据),训练特定目标类的分类器来识别特定目标。因此可以断定,分类器方式的目标类识别包含如下属性:
设计特征组,满足特定目标的基本特征
收集特定目标的采样集,包含正负采样
逐步训练cascade分类器,形成各个stages
识别包含特定目标的图片或视频帧
其中设计特征组可以直接使用harr特征简化版,并增加一些自有的元素,采样集的建立则需要在实际的工况下采集目标样本,训练过程可以直接使用opencv提供的训练算法框架,识别过程则可以使用opencv提供的程序实现,其中有一些程序可以进一步改写以适应特定目标特征。
对于特定目标的识别,可以将opencv提供的程序进行简化,去掉一些不必要的判断和分枝,这样可以提高程序的简洁性和易读性,减少bug发生的风险。也可以更快地进入到实际检测环节,缩减系统调试时间。
本章所述各节采用的是opencv2.4.9。部分程序实现是在vs2010上完成。
2024.3.4 ccc