使用OpenCV人脸识别程序建立人脸目标识别分类器xml的说明
2024-5-11 ccc
首先准备正采样
使用opencv提供的采样文件:
trainingfaces_24-24.vec
其中包含16520个人脸对象来源为1652个人脸目标,每个目标生成10个不同的视角变形。
其次准备负采样,负采样可以是任何不包含人脸目标的图片,满足如下要求:
- 可以是任意大小的图片,最好统一尺寸。
- 图片中不应包含任何人脸图像
- 尽量包含各类背景场景,风景、蓝天白云、城市高楼,远山森林等。
形成info格式的背景文件。
注意,目标图片和背景图片作为训练分类器的样本来源称之为样本空间,指明训练过程中的采样是来自于指定的样本空间,而在训练过程中指定的采样数,npos和nneg则是自样本空间中选择出合适的样本个数(称之为采样数),程序使用指定个数的采样对分类器进行训练,每训练一个stage都使用样本空间中一个采样集。
级联分类器的stage数(级联数)确定了分类器的分类误差,样本空间确定分类目标种类和背景类型,npos和nneg确定stage使用的采样集大小。
在opencv中有一个限制,npos + nneg不能大于32767,因为在opencv中定义CV_16SC1为16位整数类型,而在idxcache中数据类型定义为CV_IDX_MAT_TYPE = CV_16SC1。如果npos+nneg大于32767,则在读采样的过程中开辟的索引缓冲区溢出,因此需要对缓冲区数据类型进行修改,是之为CV_IDX_MAT_TYPE = CV_32SC1。
将在_cvhaartraining.h中定义的CV_IDX_MAT_TYPE修改为:
#define CV_IDX_MAT_TYPE CV_32SC1
使采样总计数可以超过32767,达到32位整数。
一般情况下,npos和nneg仅仅是一个stage计算时所需要的采样数,越大计算的stage越慢,并且需要的内存也越大,因此没必要选择过大的npos和nneg。
准备好采样图片后运行opencv的建立xml程序,其中stage数应该为适当值,这要根据样本空间的情况和flasealarmrate值做出选择,比如,默认的stagefalsealarmrate=0.5,则如果选择stages = 7,分类器cascade的falsealarmrate = exp(0.5, 7),即cascade的falsealarmrate是stage的falsealarmrate的7次方。默认的stages = 14,脱标报警(falsealarmrate)的误差应该为0.5的14次方(0.0000610)。选择适当的stages参数可以使分类器获得更高的精确度,识别率更高,但是训练过程也就更加漫长。
在开始训练一个stage之前,首先需要从样本空间中读取指定个数npos的正采样和nneg个负采样。正采样按照vec文件指定的顺序读取,经过已存在的(已装入的)cascade验证为可以正确识别的采样图片,负采样则根据info文件提供的负样本空间图片顺序装入经过变换后的图片。图片变换的原则是移动抠图窗口,逐步读取图片,缩放源图后再移动窗口抠图,变换原图后再移动窗口抠图。所有装入的负采样都必须经过已存在的cascade检测,只有那些被检测为人脸的负采样才能作为训练stage的负采样(注意,此时的负采样是错误识别的负采样)。负采样读取程序如下(lbp和harr):
bool CvCascadeImageReader::NegReader::nextImg()
{
Point _offset = Point(0,0);//设置偏移初值为(0,0)
size_t count = imgFilenames.size();
for( size_t i = 0; i < count; i++ ){
src = imread( imgFilenames[last++], 0 );//last为读文件的序号
if( src.empty() )
continue;
round += last / count;//累加一个小于1的数,
round = round % (winSize.width * winSize.height);//窗口面积大小的余数
last %= count;//当last = count 时,返回last = 0
_offset.x = std::min( (int)round % winSize.width, src.cols - winSize.width );
_offset.y = std::min( (int)round / winSize.width, src.rows - winSize.height );
if( !src.empty() && src.type() == CV_8UC1 && _offset.x >= 0 && _offset.y >= 0 )
break;
}//计算当前last值下的抠图偏移值,注意round是一个累加值,last归0时round不一定归0,
//因此读入的同一个last序号图片,抠图位置也不一定相同,因此负采样的获得也不相同。
if( src.empty() )
return false; // no appropriate image
point = offset = _offset;
scale = max( ((float)winSize.width + point.x) / ((float)src.cols),
((float)winSize.height + point.y) / ((float)src.rows) );
//计算源图到被抠图像的缩放尺度
Size sz( (int)(scale*src.cols + 0.5F), (int)(scale*src.rows + 0.5F) );
resize( src, img, sz );//缩放图像
return true;
}
//抠图过程
bool CvCascadeImageReader::NegReader::get( Mat& _img )
{
CV_Assert( !_img.empty() );
CV_Assert( _img.type() == CV_8UC1 );
CV_Assert( _img.cols == winSize.width );
CV_Assert( _img.rows == winSize.height );
if( img.empty() )
if ( !nextImg() )
return false;
//指定抠图区域在point(x,y)位置和窗口winSize范围
Mat mat( winSize.height, winSize.width, CV_8UC1,
(void*)(img.data + point.y * img.step + point.x * img.elemSize()), img.step );
mat.copyTo(_img);//将指定范围图像拷贝到_img。
//准备下一次读取负采样的参数
if( (int)( point.x + (1.0F + stepFactor ) * winSize.width ) < img.cols )
point.x += (int)(stepFactor * winSize.width);//改变point.x位置
else{
point.x = offset.x;//point.x位置返回到抠图初始位置
if( (int)( point.y + (1.0F + stepFactor ) * winSize.height ) < img.rows )
point.y += (int)(stepFactor * winSize.height);//改变point.y位置
else{
point.y = offset.y;//初始化抠图point.y位置
scale *= scaleFactor;//缩放scale,其中scaleFactor = 1.4145常量(根号2)
if( scale <= 1.0F )
resize( src, img, Size( (int)(scale*src.cols), (int)(scale*src.rows) ) );
else{
if ( !nextImg() )
return false;
}
}
}
return true;
}
nextImg()函数读入指定last序号的采样图片到src,并根据当前的last计算round,然后获得offset和scale,并根据scale把scr缩放到img。注意,在nextImg()中,每一个读文件,都计算一个scale,是根据src的大小进行计算的,在get()中,则是根据scaleFactor进行进一步缩放,直到不能再进一步缩放为止,才读入新图片。而offset的偏移过程则是根据stepFactor = 0.5进行point(x,y)移动抠图,直到不能进一步移动了,才进行下一步的缩放(一般为缩小)。因此在准备背景图片时,只需要选择不同场景的图片即可,不需要对图片进行剪裁和缩放处理操作,因为读背景图片的操作可以自己处理剪裁和缩放。
注意,这里给出的仅仅是opencv背景图片采集的一种算法,你也可以自己建立一套背景图片采集算法,因为这套算法会产生大量的冗余图片,因此也增加了大量的冗余计算,如果使用采集算法排除掉冗余性,则可以极大地提高训练效率。
注意,无论给出的负样本多少,在最后,程序都将花费大量的时间进行负样本的装入,因为在给出的负样本图片中能够满足要求的负采样越来越靠后了,因此需要大量的重复计算。
关于训练过程
下面是使用相同样本空间和参数进行的两种特征类型(lbp/harr)的boost级联GAB类型的分类器的训练过程,设置的样本参数为:
正样本数:2000
负样本数:1000
正样本空间:16132x24x24
负样本空间:8446x36x36
训练过程中,程序每次从正样本空间中读出满足要求的2000个正样本,从负样本空间中读出满足要求的1000个负样本,组成训练采样集,训练一个stage分类器。
分类器的识别效率如下:
对于stages=5
Harr lbp
对于stages=7
Harr lbp
对于stage=9
Harr lbp
对于stage=11
Harr lbp
对于stage=15
Harr lbp
对于stage=18
Harr lbp
对于stage=19
Harr lbp
从上面图片的识别过程来看,harr特征明显低于lbp特征,但是需要注意,在训练过程中harr的每一个stagen分类器都是在stage数达到之后终止的训练过程,也就是说stage数达到后,训练误差(特别是falsealarmrate误差)并没有达到要求的值。而lbp则是在stage数达到后训练误差(特别是falsealarmrate误差)也达到了要求的值。
对于harr的第stage=18和stage=19这两个分类器,几乎没有分别,这说明给定的采样空间中(特别是负采样空间)已经耗尽了所有的可用场景属性,再也不能提供更精确的识别特征了,如果不增加负样本空间,继续的训练没有意义。
关于训练参数的意义
上述训练是在样本空间中选定2000个正样本(顺序读取)和1000个负样本(顺序读取并缩放、位移)进行stage训练,得出的各个不同stage级联分类器的过程。其中使用相同的增强类型GAB和误差设定。如果所有误差设定不变,而改变选定样本参数,会怎样呢,下面是扩大选定样本的数量(不改变样本空间),然后看一下结果:
正采样数:4000
负采样数:2000
正样本空间:16132x24x24
负样本空间:8446x36x36
Harr4000_5 harr2000_5
Lbp4000_5 lbp2000_5
等比例改变采样数(4000/2000、2000/1000),在相同的样本空间下,效果不大。没有明显的识别率提升。
增加负采样的数量,如下参数:
正采样数:2000
负采样数:4000
正样本空间:16132x24x24
负样本空间:8446x36x36
同样是stages=5,结果如下:
harr2000_4000_5 harr4000_2000_5
增加负采样,有助于排除背景干扰。
lbp_2000_4000_5 lbp_4000_2000_5
增加负采样,有助于排除背景干扰,但是效果不很明显。
我们注意到在训练xml的过程中,样本空间中被使用的采样是顺序装入的,对于正采样,排除当前cascade不能正确识别的样本,顺序装入样本空间中后续采样,对于负采样则排除正确识别的采样,从样本空间中继续装入那些不能正确识别的负采样(注意,负采样的正确识别是说,非目标被识别成非目标,非目标被识别成目标则说明这个负采样是不能被正确识别的)。随着stages的训练深入,负采样的正确识别率升高,因此,在负样本空间中装入的负采样绝大部分都能够被正确识别,得到指定的训练用负采样的几率会随着stages的深入而降低,程序需要花费大量的时间在样本空间中检测负采样,以获得参数指定的负采样个数。
是不是在开始训练的时就增加负采样的数量,就可能在较少的stage下就能够获得比较满意的xml。下面继续增加负采样参数,如下参数:
正采样数:2000
负采样数:8000
正样本空间:16132x24x24
负样本空间:8446x36x36
harr_2000_8000_5 harr_2000_4000_5
lbp_2000_8000_5 lbp_2000_4000_5
无论是harr特征还是lbp特征,在相同的负样本空间中增加负采样数都不能够显著地排除背景干扰。
要想排除背景干扰,必需增加stages数,因为stages的深入,能够显著第排除负采样中重复的采样,使得样本空间中大量的重复采样被排除在训练过程之外,这还可以增加训练速度。
正样本是表示检测目标类型的样本,负样本是表示目标所处环境的样本。在训练过程中初始设置为空cascade分类器,此时,读入的正采样默认为是完全可正确识别的采样,而读入的负采样则是完全不可识别的采样(错误识别)。从而建立一个在这些样本上的一个stage(由多个cart组成),然后以该stage级联成一个cascade(只有一个stage),重新对样本空间进行读取并检测,再获得一组参数指定的正负采样,其中正采样是通过所获得的cascade进行正确识别的采样,负采样则是通过cascade错误识别的采样。由于cascade的过滤,之前的一些采样被过滤掉(消耗掉),样本空间中的后续样本被读出,然后对新的采样集进行stage计算。以上叙述说明了样本空间、采样集和stage数的关系,使用一定的样本空间(vec和info)并指定采样集参数(正采样数、负采样数),和适当的stages数,能够快速训练出理想的分类器cascade。
注意,负采样是通过cascade过滤形成的,较大的负采样可能会增加采样集的空间占有率,加大计算量,较多的stages数可能会耗尽采样空间,在读入负采样时增加读入时间。因此,选择适当的训练参数进行分类器训练时一个关键技术。
一般而言,正采样的重复数比较少,因此正采样数的选择比较容易,比如选择正样本空间的70%—80%。负样本空间的重复较多,因此选择适当的负采样数能减少空间占用和加快训练过程,并增加排除样本的重复率。下面是按此原则进行的harr和lbp两种特征的训练过程。
正采样数:12000
负采样数:4000
正样本空间:16132x24x24
负样本空间:8446x36x36
级联数:Stages = 10
harr_12000_4000_10 lbp_12000_4000_10
减少采样数,增加stages数,结果如下:
正采样数:1000
负采样数:500
正样本空间:16132x24x24
负样本空间:8446x36x36
级联数:Stages = 18
harr_1000_500_18 lbp_1000_500_18
harr_1000_500_18_1 lbp_1000_500_18_1
h_1000_500_18_peolpe l_1000_500_18_people
对于使用一定特征而言,采样数和stage的的关系可以从falsealarmrate的给定值计算出来。比如stage=10,falsealarmrate=0.5,则无论指定什么样的采样数,对于特定样本空间其最终的分类器误差必定是小于Exp(falsealarmrate,10),即为falsealarmrate的10次方。而正采样数则表明分类器可检测的目标范围(比如,采样空间有16000个样本,分类器仅适用头2000个就完成了训练,则分类器对于样本空间中2000样本之后的样本种类就可能会识别出错),负采样也是如此。训练过程消耗掉的样本是分类器可以正确检测的样本。
使用正确的采样数和stage级联数,在训练中消耗掉样本空间中的所有采样,是训练参数的最佳选择。
如果样本空间中的所有样本都已经消耗掉,也就是在最后一个stage级联的计算中采样读入的是样本空间最后的样本(每一个stage级联,逐渐增加正采样数和逐渐减少负采样数),可以加快训练过程并不损失分类器检测精度和检测种类范围。
Harr_2000_1000_18 harr_1000_500_18
左侧分类器识别显然没有右侧的好,它们都使用了stage=18参数,只是负采样数不一样,一个是2000/1000,一个是1000/500。
Harr_2000_1000_18_1 harr_1000_500_18_1
Harr_2000_1000_18_2 harr_1000_500_18_2
以上harr特征的训练中可以看出采样数的参数的作用,训练一个stage时,可以根据当前使用的样本空间指定不同的采样数,使得读入采样的耗散数达到最佳(用脱标报警的stage次方检验),逐级改变参数,一级一级训练(逐级增加增采样,减少负采样),如下:
1、5000/5000/3(harr)
2、6000/4000/5(harr)
3、3000/2000/10(harr)
4、2000/1000/15(harr)
4、1000/500/18(harr)
从上述识别效果可以看出,对于stages小于15的cascade分类器,npos和nneg的数量变化不明显,对于stages大于15的cascade分类器,减少npos和nneg会减少采样读入时间,减少stage计算量,识别效果也比较明显增加。
因为选择的背景样本空间有限,所以最终的cascade效果不是很好,如果增加背景info的图片数和种类,可能效果会比较满意。
增加背景info的样本空间
所有上面的info样本空间都是基于300张图片生成的36x36的8000多张背景情况下的训练过程。下面改变背景info样本空间如下:
- 直接使用300张源图
- 训练参数:harr1000/100/10
POS: 1000 1037 0.964320
NEG: 100 0.00484355//负采样读入样本数 = 20646
在训练的样本空间中读入了1037个正样本,获得1000个可用的正采样。在背景样本空间中读入了20646个负样本,获得100个可用的负采样。
2.训练参数:harr1000/100/15
POS: 1000 1063 0.940734
NEG: 100 0.000780969//负采样读入样本数 = 128046
3.训练参数:harr1000/100/18
POS: 1000 1086 0.920810
NEG: 100 0.000707439//负采样读入样本数 = 141354
4.训练参数:harr1000/100/20
POS: 1000 1096 0.912409
NEG: 100 0.000489222//负采样读入样本数 = 204406
5.训练参数:harr1000/100/30
POS: 1000 1143 0.874891
NEG: 100 5.17787e-005 = 0.0000517787//负采样读入样本数 = 1931296
6.训练参数:harr1000/100/40
POS: 1000 1196 0.836120
NEG: 100 1.20734e-006 = 0.00000120734//负采样读入样本数 = 82826709
从上面的识别过程来看,当stages大于20后,效果就已经很明显了。当背景的消耗达到了千万级别,stages = 35 则完全达到了识别要求。
因为背景图片样本只给出了一般形式的图片,并没有考虑全面的场景需求,可能在一些复杂的背景环境下仍有错误地背景出现,这就需要添加新背景重新训练。
以上训练过程也说明,样本空间给定后,采样参数npos和nneg影响训练计算时间,而stages数则影响负样本的过滤时间,当stages大于15后,如果nneg过大,则负样本的过滤过程将会消绝大部分训练的计算时间。
- 增加源图到600张
在将负样本空间增加到600张图片后,由于采样参数仍然是100,因此stage训练过程中对负样本的消耗没有太大的改变,也就是说负样本的排除率反而下降了,因此识别的效率也下降了。
- 训练参数:harr1000/100/10
POS: 1000 1037 0.964320
NEG: 100 0.00484355//负采样读入样本数 = 20646
2.训练参数:harr1000/100/15
POS: 1000 1066 0.938086
NEG: 100 0.00100953//负采样读入样本数 = 99055
3.训练参数:harr1000/100/18
POS: 1000 1090 0.917431
NEG: 100 0.000841446//负采样读入样本数 = 118843
4.训练参数:harr1000/100/20
POS: 1000 1099 0.909918
NEG: 100 0.000404029//负采样读入样本数 = 247512
5.训练参数:harr1000/100/30
POS: 1000 1145 0.873362
NEG: 100 5.40441e-005 = 0.0000540441//负采样读入样本数 = 1850481
6.训练参数:harr1000/100/40
POS: 1000 1206 0.829187
NEG: 100 6.78488e-006 = 0.00000678488负采样读入样本数 = 14749262
说明,以上训练过程,只是将背景样本空间(负样本空间)进行扩展,增加了300张背景图片,在相同的stages、npos、nneg参数的情况下,训练出来的cascade分类器效果就有了一定的差异,也就是在训练过程中,负样本空间对负采样的排除过程有一定的影响。在一定的样本空间下,npos和nneg的选择与stages一定要有一个匹配,这样才能充分利用当前样本空间提高cascade的识别精度。
一般的harr特征下stage应该在15左右,此时的npos和nneg应根据机器的容量进行选择,特别是nneg的选择,应该估算:
当stages = 15时,cascade的识别误差为exp(0.5,15) = 0.0000305176。此时如果设nneg为1000,则要达到这个误差精度,需要读入负样本的次数应该为32767976。此时提供的样本空间通过上一级stages系列组成的cascade过滤对负样本空间的耗散与提供的样本空间容量相关。样本空间中采样图片的读取由harr框架程序确定,不同的算法有不同的效果。
在当前的例子中如果想要在600个背景图片提供的负样本空间中达到满意的效果,一是可以提高nneg的数量,或者进一步提高stages的数值,比如npos = 1000,nneg=1000,stages=40。或则npos=1000,nneg=100,stages=50,都能提高cascade的识别精度。
最终结论,对于一定的样本空间当指定的样本耗散达到空间的70%后,样本空间所提供的识别目标种类和背景类型能够正常识别。负样本空间的排除过程与负样本读入算法(抠取算法)有关,不同的算法有快速排除背景的能力,本例中使用的是opencv中提供的算法,稍有改动(将随机过程改为顺序过程),因此背景图片的排列顺序可能会影响到负采样的过滤过程。样本空间的耗散过程与npos/nneg/stages相关,同时也与样本空间的大小相关,特别是负样本空间,要在一定的stages下达到想要的耗散比率,需要适当调节nneg。可以使用一个函数测算nneg和npos,使之在不同的stage下读入不同的npos和nneg,以获得相同的耗散率。
cascade分类器的训练过程实际上是一个负样本的排除过程。根据贝叶斯的条件概率原理,多个stages的级联分类,将负样本逐级过滤,最终得出分类结果。这个结果是概率意义上的,不可能达到100%,但是在一般识别情况下,使用一定的参数训练出来的cascade分类器能够满足目标检测的初步需求,即“目标肯定包含在检测结果当中”。然后可以利用其它环境条件将非目标结果进一步排除,减少复杂运算过程。这种分类器可用于流水线上的产品定位等特殊目标、痕迹检测。
使用opencv的目标检测及分类器cascade框架,可以训练出识别范围特殊的分类器,对特定目标进行视频动态检测,定位,和监控,比如车牌定位。
下面是OpenCV的xml文件haarcascade_frontalface_alt.xml的检测结果与cascade_src_l1000_100_30.xml和_decinfoimgs12_src_h1000_100_30.xml检测结果的比较。
Opencv_0 lbp_0 harr_0
Opencv_1 lbp_1 harr_1
Opencv_2
Lbp_2
Harr_2
Opencv_3
Lbp_3
Harr_3
Opencv_4
Lbp_4
Harr_4
注意,其中的opencv分类器时20x20的,而lbp和harr都是24x24的。