【人脸检测】“人脸训练代码”项目笔记(3)——代码结构分析

17 篇文章 1 订阅

IplImage中,

加载图片,即Load函数:

先用cvLoadImage()加载图片,然后检查是否加载成功,主要看height和width是否为0,若为0,则加载失败;加载成功后对图片进行SetSize(),即设置图像的大小尺寸,并清除之前数据,再为内部向量和矩阵申请内存,变量初始化,将向量转化为矩阵。

         接着对数据进行赋值,

for(int i=0,ih=img->height,iw=img->width;i<ih;i++) //给矩形图像赋值
{
REAL* pdata = data[i];
unsigned char* pimg = reinterpret_cast<unsigned char*>(img->imageData+img->widthStep*i); 

               //char* imageData指向对齐图像数据的指针。
for(int j=0;j<iw;j++) pdata[j] = pimg[j];
}

       最后释放图片内存,cvReleaseImage(&img)



0、准备工作:生成弱分类器,实质上还是有了位置和大小但没有阈值和方向的特征。(当特征有了阈值和方向,即thresh和parity时,便就成了简单的分类器)

Haar特征

void WriteSimpleClassifiers(char *filename)

WriteSimpleClassifiers(".\\data\\classifiers.txt")

这里只是遍历获得特征的各个点,即x1,x2,x3,x4,y1,y2,y3,y4,而阈值以及误差初始化为0.0。

在后面SingleFeatureClassifier单个特征分类器训练的时候,也就是设置一个分类器的过程。

WriteToFile(f)写入到文件classifiers.txt中,阈值和方向,即thresh和parity初始化为0。在初始化时FaceDetectInit的ClassifierInit()中,将classifiers.txt中的数据,即弱分类器,读取到缓冲区,便于在后面训练时FaceDetectTrain的SingleFeatureClassifier(labels,classifiers[i],table[i])中设置一个分类器的时候使用,在训练的时候主要是设置阈值和方向,即thresh和parity。



一、人脸样本的初始化部分

FaceDetectInit()


首先向系统申请动态内存,开辟一个存放Cascade的空间,加载之前的训练好的分类器到缓冲区,即级联分类器数据。cascade = new CascadeClassifier; 向系统堆区申请动态内存,开辟一个存放CascadeClassifier类型的存储空间,返回该内存块的首地址,这里同时也调用了构造函数。注意这里的Cascade是一个全局变量。LoadDefaultCascade()----ReadFromFile()(这里用三次嵌套)

读取文件中的数据之前,先开辟内存,然后将文本文件的数据读入到缓冲区内。

注意:其实这里也只是创建了cascade.txt文件夹而已,真正的使用还是在训练的时候。


totalcount=trainsetInit()

int TrainSetInit(char *posDir, char *negDir, int *nFaceCount, IntImage **trainSet)

totalcount = TrainSetInit(positive_dir, negative_dir, &facecount, &trainset)

输入:存放正样本图片的文件路径;存放负样本原始图片的文件路径

输出:函数右边以指针的方式获得正样本图片的数目facecount训练样本集trainset,函数左边获得所有样本的总数totalcount

FuncGetPicName() 遍历文件,获得文件下面的目录,也就是图片名称,同时也能获得图片的数目。

利用到了Load();

读入正样本以及负样本

a.标签;  b.setsize()由24*24到25*25;c.data数据(循环读取数据);d. .clear()。

CalculateVarianceAndIntegralImageInPlace(void)

求解的是小图的样本方差和积分图,


totalfeatures = ClassifiersInit()

初始化分类器

int ClassifiersInit(char *filename, SimpleClassifier **scs)

totalfeatures = ClassifiersInit(classifier_filename, &classifiers)

声明ifstream f;f.open打开文件“classifiers.txt”;进行开辟内存,ReadFromFile(f)读取文件classifiers.txt中的弱分类器到缓冲区中;f.close关闭文件。

这里Classifiers.txt中的数据(选取的特征总数以及分类器的参数(阈值、方向符、类型以及位置坐标:thresh、parity、type、x1、x2......y3、y4)),需要注意的是,这里的数据来源于上面的WriteSimpleClassifiers()函数写入到文件中的未确定方向和阈值但有位置和大小的特征模版(未设置好的分类器)。

这里的未设置好的分类器将在训练部分进行设置,即弱分类器的训练过程,也就是设置一个分类器的过程。


bootstrap_filenames = BootstrapInit()

char** BootstrapInit(char *bootstrapDir, int *fileNum, int **fileUsed)

bootstrap_filenames = BootstrapInit(boostrap_dir, &bootstrap_size, &fileused)

输入:boostrap文件,也就是存放遍历产生负样本的原始图片的文件

输出:原始图片的数目;图片使用的情况(使用赋值为1,未使用赋值为0),这里只是通过memset初始化为0;文件中图片的名称

获取图片的名称以及图片的数目;

首先,利用FuncGetPicName()遍历bootstrap文件,获取图片的名称filenames以及图片的数目;

接着,为filenames分配内存空间,遵循“先外部内存,后内存内存”;

然后,利用strcpy()拷贝文件名;

最后,利用samples.clear()清空内存。

返回值为:filenames文件中的图片名称的数组

切记:最后要释放内存临时空间


提示显示训练是从哪一层开始的。


其他变量数据的初始化(先申请内存,注意申请内存的先后顺序并且注意检查指针的有效性,最后初始化)

weight[]代表样本的权重,这里是初始化正负样本的权重;

table[][]代表为每个特征分配存放所有样本特征值的内存;

feature[]代表每个样本的的特征值;

labels[]代表每个样本的标签;


最后利用ReadRangeFile()从“cascade.txt.range”读取mean_min、mean_max、sq_max、sq_min、var_max、var_min。

其实此时只是创建“cascade.txt.range”文件,还有读取到mean_min、mean_max、sq_max、sq_min、var_max、var_min,这些值是在人脸样本训练部分的第一层训练,利用WriteRangeFile()函数才写入到文件cascade.txt.range中的。


二、人脸样本的训练部分

FaceDetectTrain()

(一)获取样本

当层数为1时;即( starting_node==1 )

WritreRangeFile()

mean_min、mean_max、sq_max、sq_min、var_max、var_min写入文件(ofstream f; f.open(filename); ....f.close())

其中:mean_min、mean_max、sq_max、sq_min、var_max、var_min是所有正样本的中的最大最小mean,sq以及var。最后将其缩放0.1,即最小mean、sq以及var分别乘以0.9,最大mean、sq以及var分别乘以1.1.


当层数大于1,且小于预设的层数时:即(1 < starting_node <= max_nodes)

BoostingInputFiles(const bool discard)

if(i!=1 && i<=max_nodes)
{
ofstream f;
if(!BoostingInputFiles(true))
{
printf("All bootstrapping file used! Training finished!\n");
return;
}
f.open(FileUsage_log_filename);
for(int j=0;j<max_files;j++) f<<fileused[j]<<" ";
f.close();
}



1.LoadDefaultCascade()

加载分类器的原因在于,下面在遍历bootstrap文件中的原始图片生成负样本的时候,需要以上训练好的分类器来判别,若是能通过以上训练好的所有层的强分类器,则说明这个小图片被选作负样本。


2.ApplyOriginalSizeForInputBoosting(const char *filename,int &pointer)

cascade->ApplyOriginalSizeForInputBoosting(bootstrap_filenames[index],pointer)

 此部分好好研究一番,写的非常好,值得研读,同时这里还用到了双线性插值的算法进行缩图

从bootstrap文件中的原始图片中获取负样本;

首先,利用Load()加载原始图片,利用宽和高来判断是否加载成功;

接着,求解的是原始大图的平方像素和积分图:

CalcSquareAndIntegral(IntImage& square, IntImage& image) const

然后,遍历读取的一副原始图片,获得小图的负样本,具体如下:

          求解小图的方差以及积分图,然后通过以上的分类器,若能通过就视为负样本;

最后,进行缩图,一直遍历达到预定的负样本数目为止。

缩图用的是双线性插值。具体见另一篇双线性插值的博文:http://blog.csdn.net/smf0504/article/details/51304007

一个完整的双线性插值算法可描述如下:

(1)通过原始图像和比例因子得到新图像的大小,并创建新图像。

(2)由新图像的某个像素(x,y)映射到原始图像(x’,y’)处。

(3)对x’,y’取整得到(xx,yy)并得到(xx,yy)、(xx+1,yy)、(xx,yy+1)和(xx+1,yy+1)的值。

(4)利用双线性插值得到像素点(x,y)的值并写回新图像。

(5)重复步骤(2)直到新图像的所有像素写完。

这段代码要见Resize()函数,不错的代码。

注意:乘法的效率要比除法的效率要高,所以可以先求倒数,再乘以倒数已达到除以一个数的目的;

        移位的效率也比除法的效率要高很多。


3.获得负样本之后,将所有的样本trainset[i]中将表示积分图的data转换为像素的data。原因在于:下面所有的样本记录下来,也就是小的图片。具体见步骤4。

//此部分代码将data[][]代表的积分图转化为代表像素
for(i=0;i<totalcount;i++)
{
int k,t;
memcpy(im.buf,trainset[i].buf,(sx+1)*(sy+1)*sizeof(im.buf[0]));
for(k=0;k<=sy;k++) trainset[i].data[0][k] = 0;
for(k=0;k<=sx;k++) trainset[i].data[k][0] = 0;
for(k=1;k<=sx;k++)
for(t=1;t<=sy;t++)
trainset[i].data[k][t] = im.data[k][t]-im.data[k-1][t]-im.data[k][t-1]+im.data[k-1][t-1];
}

4.利用of.open(trainset_filename,ios_base::out|ios_base::binary)打开文件夹,写入总样本数目totalcount以及每个样本的大小(sx*sy)以及数据(标签label以及表示像素的data),这里是通过申请一个临时动态向量内存空间。原因在于,样本是(sx+1)*(sy+1),而数据存取的是sx*sy。

of.open(trainset_filename,ios_base::out | ios_base::binary);
of<<totalcount<<endl;
unsigned char* writebuf;
writebuf = new unsigned char[sx*sy]; assert(writebuf!=NULL);
for(i=0;i<totalcount;i++)
{
of<<trainset[i].label<<endl;
of<<sx<<" "<<sy<<endl;
for(int k=0;k<sx;k++)
for(int t=0;t<sy;t++)
writebuf[k*sy+t] = (unsigned char)((int)trainset[i].data[k+1][t+1]);
of.write((char*)writebuf,sx*sy);
of<<endl;
}
delete[] writebuf; writebuf=NULL;
of.close();

注意:临时申请的内存空间,最后需要释放,释放后指针变量指向NULL,如同变量赋值为零一样。

5.将所有的样本trainset[i]中的data,由表示像素转化为表示积分图。

6.负样本判别,是否为应用的图片块:

for(i=facecount;i<totalcount;i++) 
{
if(cascade->ApplyImagePatch(trainset[i])==0)
; //printf("Something is wrong?");
}

cascade->ApplyImagePatch()(CascadeClassifier::ApplyImagePatch(const IntImage& im) const)----ac[i].ApplyImagePatch()(AdaBoostClassifier::ApplyImagePatch(const IntImage& im) const)----GetValue(im)(AdaBoostClassifier::GetValue(const IntImage& im) const)----alphas[i]*scs[i].Apply(im)(SimpleClassifier::Apply(const IntImage& im) const)----GetOneFeature(im)(SimpleClassifier::GetOneFeature(const IntImage& im) const)----CalcOneHaarFeature(data, 0, this)


FileUseage_log_filename即FileUsedage_log.txt记录了哪些bootstrap中的图片被使用了,也是通过ofstream f ; f.open以及f.close记录到文件中。


(二)进行训练:

条件:没有达到预设的最大层数时

OneRound(round)    round为第几次迭代训练

cascade->OneRound(round)

1.确定当前层弱分类器的个数

第一层为预设定好的个数;其他层为当前层预定数目的0.4+上一层分类器个数的0.6.

输出第几节点,使用的弱分类器的数目

当ada.TrainLDS(numofSc,true,goal_method)以及TrainAdaBoost(numOfSc,false,method)时,每次只训练一层;

当ada.TrainLDS(numofSc,false,goal_method)以及TrainAdaBoost(numOfSc,true,method)时,从头训练到尾。

2.TrainLDS(int numOfSc,const bool update,const int method)

ada.TrainLDS(numofSc,true,goal_method)

goal_method代表的是采用哪种方式确定集合的阈值,本代码选择的是第三种方法

updata代表的是是否执行AppendAdaBoostClassifier()和 Cascade2DataCpp(".//data//data.cpp"),也就是说是否添加当前强分类器以及是否将cascade.txt数据编写为data.cpp。换言之更新cascade.txt中的数据以及编写c++代码。updata为true是更新,且同时也决定了这是单层训练,原因在于Cascade2DataCpp(".//data//data.cpp")里有一个exit(0),生成.cpp文件之后,就退出了程序。若要想一直训练到底,只需要把updata改为false。将TrainAdaBoost(numOfSc,false,method)中updata的值由false改为true即可。

TrainAdaBoost(int numOfSc,const bool update,const int method)

TrainAdaBoost(numOfSc,false,method)

updata代表是否执行AppendAdaBoostClassifier(),也就是说是否添加当前强分类器,换言之更新cascade.txt中的数据。updata为false为不更新。若是不想编写c++代码。也就是说不执行Cascade2DataCpp(".//data//data.cpp"),可以将updata改为true,将ada.TrainLDS(numofSc,true,goal_method)中的updata改为false。

此部分的训练对照AdaBoost算法进行实现,但是里面的参数的确定方式不一样。

(1)利用malloc()为所有特征分配内存空间,但不初始化;

(2)InitToGivenSize(1000):为当前层上的弱分类器以及对应的权重数组开内存,并且将个数以及阈值初始化为0;

       注意:首先利用Clear()清空内存,再初始化

(3)FillTheTable(int* const row,const SimpleClassifier& sc):利用for循环,利用函数FillTheTable(table[i],classifiers[i])(这里的classifier[i]其实就是特征,即没有确定阈值和方向符),为每个特征计算所有训练样本的特征值CalcOneHaarFeature(data, 0, &sc),接着进行方差归一化,最后将方差归一化的特征值以升序的方式进行排序QuickSort(features,row,0,totalcount-1);

注意:计算特征值的时候,将特征值除以了方差,而方差归一化是为了减小不同关照的影响。

           QuickSort()此函数需进行好好的琢磨一番

(4)InitializeWeights();初始化样本权重,注意正负样本不一样。

(5)为当前所有样本的结果分配内存malloc(),并初始化为0.0。

(6)选择最优的分类器(在所有特征的分类器中选择误差最小的那个分类器作为最优的分类器):第三种方法,即新的Adaboost训练算法

        条件:当小于本层设定的分类器数目时

 首先:AddOneSimpleClassifier(unsigned char*used,int round)

       选择最优的分类器的过程;

1.初始化最小误差,以及最小索引值;

2.将所有样本的标签赋予label[];

3.NormalizeWeight()归一化权重;

4.选择最优弱分类器(特征不重复)

SingleFeatureClassifier(const int* const labels,SimpleClassifier& sc,const int* const indexes)

SingleFeatureClassifier(labels,classifiers[i],table[i])

训练一个弱分类器,即设置一个分类器的过程;实质就是为特征选择最佳阈值的过程,即使得分类误差最小的特征值

labels是个数组,是所有样本的标签,classifiers[i]代表这个特征的未设定阈值的分类器,table[i]代表这个特征,且实际是个二维数组,第二维代表的是在这个特征下的所有样本的特征值。

5.将选定好的阈值、误差以及对应的特征以及分类器作为最优的分类器。

6.根据选择的最优弱分类器的误差调整样本权重以及设定分类器的权重,正确的样本的权重减小,错误的不变;

7.将最优的分类器(位置信息,即x1,x2,x3.....)添加到当前层的强分类器中,分类器的权重,强分类器的阈值的调动,强分类器中最优弱分类器个数的调动。


然后:累计每个样本通过的分类器权重之和

最后:到最后一次循环时,也就是说最后一个分类器的选取时,进行检查是否达到预设定的目标;

IsReachTheGoal(REAL *curresult,const int method,bool discard)

IsReachTheGoal(curresult,method,discard)

curresult[]记录的是每个样本通过的所有分类器的权重的和。

循环facecount次,寻找到curresult的最小值mincurresult.

通过nth_element()函数将90%的负样本和正样本分为一类,也就是让正样本全部通过,90%的负样本通过。

负样本的阈值为:thresh_negative = curresult_bak[nth]+0.0001f

正样本的阈值为:thresh_positive = minCurresult*0.9f

选择正负样本阈值小的那个作为阈值thresh2,同时也是强分类器的阈值。

计算伪正样本在正样本中的概率,伪负样本在负样本中的概率:

void CalcFnFp(REAL *curresult, float *f_fn, float *f_fp, REAL th, int faceNum, int totalNum)

CalcFnFp(curresult_bak, &f_fn, &f_fp, thresh2, facecount, totalcount)

每个级联节点的假阳性目标(伪正样本在正样本中的比例)和假阴性目标(伪负样本在负样本中的比例)的设定值0.00051f和0.4f,一旦达到了,就退出当前层。且把thresh2赋值作为强分类器的阈值。

注意:当添加分类器个数超过预定的次数,且还没有达到设定的目标,那么就放弃这样的阈值选择方式,重新设定阈值方式。也就说让通过的负样本数目发生变动。

(7)添加强分类器,即更新级联分类器。

void AppendAdaBoostClassifier(const AdaBoostClassifier& ada)

 AppendAdaBoostClassifier(*this)

首先,将上面训练好的各层分类器,从文件中读入缓冲区;

然后,添加当前层的强分类器;

最后,将之前的强分类器以及当前的强分类器一同从缓冲区写入到文件中。

做法如下:

ifstream f;ofstream of; 

f.open打开文件;从文件中读取到缓冲区ReadFromFile(f);f.close关闭文件。

添加当前层强分类器

of.open打开文件;从缓冲区写入到文件中WriteToFile(of);of.close关闭文件。

注意:当是第一层的训练的时候,f.open只是创建文件cascade.txt文件;然后在关闭;添加第一层训练好的强分类器,也就是当前层强分类器;of.open打开文件cascade.txt文件,将存放在缓冲区的强分类器写入到文件中WriteToFile(of;of.close关闭

(8)free()释放申请的内存,即为所有特征申请的数组内存,为所有样本申请的存放通过当前层的所有分类器阈值之和的内存。

AppendAdaBoostClassifier(*this)

Cascade2DataCpp(".//data//data.cpp")

     ConverOldData2NewData(".\\data\\cascade_Ada.txt", filename)

fiename代表的是“.\\data\\data.cpp”

将级联分类器转化为数据代码


3.result = BoostingInputFiles(false);

4. f.open(FileUsage_log_filename);
for(i=0;i<max_files;i++) f<<fileused[i]<<" ";
f.close();



以下为全局变量:

Cascade:

totalcount:样本的总数目,且在TrainSetInit的左输出值

table:int **;为每个特征所分配存放所有样本的特征值的内存空间。

weights:float * 样本的权重,初始化的样本权重。

features[]:float *;所有样本的特征值

labels:int *;所有样本的标签;

bootstrap_level:


训练样本:带有标签的样本。获取的样本需要对其label以及data赋值。

定义了vector<type> M;用完之后,要对向量数组进行释放,M.clear()。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值