要将人脸检测算法移植到DM6467,我们使用OpenCV现有的源码作为基础。首先,需要在PC上用C语言实现人脸检测的程序编写,然后移植OpenCV到DM6467,接下来再修改代码直至程序运行无误。
在OpenCV安装包中已经提供了使用Haar特征、AdaBoost算法和级联分类器来检测人脸的算法源码,并且提供了经过精心训练好的级联分类器。分类器数据使用xml格式文件存储,位于$(OPENCV1.0)\ data\haarcascades\,该目录下有用于人脸、上半身和全身等特征的级联分类器,我们使用的是人脸特征haarcascade_frontalface_alt.xml。对于人脸特征也还可以使用该目录下的haarcascade_frontalface_alt2.xml,效果也不错。
由于在嵌入式系统中一般使用C语言,所以我们需要使用C语言编写实现人脸检测的程序。鉴于OpenCV中提供了易用的API,程序编写比较简单,代码量也不大,具体源码如下所示。
#include "cv.h" #include "highgui.h" #include void main() { IplImage *img = 0, *img_small = 0; //原始图像和缩放后的图像 int i, scale = 2; //scale控制缩放比例 CvPoint point1, point2; //检测到的人脸矩形的两个点 CvRect* rect; //检测到的人脸的矩形 CvSize scale_size; //缩放后的图像大小 CvScalar color = {0, 255, 0}; //矩形框颜色 CvMemStorage* storage = cvCreateMemStorage(0); //分配存储空间 CvHaarClassifierCascade* cascade = (CvHaarClassifierCascade*)cvLoad( "haarcascade_frontalface_alt.xml", 0, 0, 0 ); //加载分类器 img = cvLoadImage( "lena.jpg", 0); //加载图像 scale_size = cvSize(img->width / scale, img->height / scale); //计算放缩后的图像比例 img_small = cvCreateImage(scale_size, IPL_DEPTH_8U, 1); //创建放缩后的图像 cvResize(img, img_small, 1); //放缩图像 cvClearMemStorage( storage ); //清零存储空间 if( cascade ) { CvSeq* faces = cvHaarDetectObjects( img_small, cascade, storage, 1.1, 2, 0 , cvSize(30, 30) );//检测人脸 for( i = 0; i < (faces ? faces->total : 0); i++ ) //画出人脸矩形框 { rect = (CvRect*)cvGetSeqElem( faces, i ); point1 = cvPoint(rect->x, rect->y); point2 = cvPoint(rect->x + rect->width, rect->y + rect->height); cvRectangle(img_small, point1, point2, color, 2, 8, 0); } } cvResize(img_small, img, 1); //缩放图像回原来的大小
cvNamedWindow( "result", CV_WINDOW_AUTOSIZE ); //显示执行过人脸检测算法后的图像 cvShowImage( "result", img ); cvWaitKey(0); cvDestroyWindow( "result"); cvReleaseImage( &img_small ); cvReleaseImage(&img); } |
程序运行后的效果见图 8。另外,经测试得到PC机上运行人脸检测算法时,对于一张512X512的只有一张人脸的图像,算法执行时间大约700ms,放缩图像至1/4即256X256时,检测时间大约200ms,如果放缩至128X128,检测时间大约为40ms。可以看出,图像大小对人脸检测算法的运行时间有非常大的影响,几乎成正比关系。所以,当我们移植人脸检测算法到资源有限的嵌入式平台时,可以将图像缩小以降低计算量,保证实时性。
图 8 PC机上实现人脸检测的效果图1
OpenCV的人脸检测算法不止能检测一幅图像中的单张人脸,还能检测多人脸,测试效果如图 9所示。
图 9 PC机上实现人脸检测的效果图2
要移植人脸检测算法到DM6467,需要先将OpenCV的基本数据结构、关键的一些宏定义、重要数据结构的初始化及相关操作、一些最基本的图像处理操作(如画矩形框、圆形)等移植到DM6467上,然后再逐步移植各个高层算法,包括人脸检测算法。
由于嵌入式平台中一般使用C语言进行编程,而OpenCV中包含部分C++格式的代码,在移植的时候这部分代码在编译器中无法识别,所以需要删除掉C++格式的代码或者编写对应的C语言实现。鉴于网上已有的EMCV是一个不错的OpenCV嵌入式版本,我们可以在其基础上进行修改和添加。
在移植OpenCV到DM6467上时,需要考虑一些细节问题,其中很关键的一个问题是如何实现ARM端的无缝调用各个OpenCV算法。如果将每种不同的算法分别封装成一个codec,那么工作量很大并且有许多重复性工作。对于这个问题,TI提供了C6Accel这个codec,集成了DSPLIB、IMGLIB和VLIB等库,功能强大,效率极高,使用方便,并且很关键的是扩展性良好。所以,我们是在C6Accel的基础上移植OpenCV的。对于C6Accel的介绍请参考原来的文档《使用C6Accel进行Sobel处理》。
C6Accel包含两个版本,1.x版本对应C64x和C64x+的DSP,2.x版本对应C674x的DSP,这两类DSP的区别主要就是是否具有硬件浮点计算单元,C64x系列是不带硬件浮点单元的。我们的移植是在1.x版本的基础上进行的,参考了2.x版本的源码。在C6Accel 2.x版本中,DSP端的OpenCV算法是封装成了库文件的,无法修改,使用不方便,所以我们需要自己修改EMCV源码以适应DSP平台。对于具体的移植过程请参考原来的文档《移植OpenCV到ARM并集成到C6Accel》。
在完成了OpenCV基本数据结构的移植之后,还需要移植一些高层图像处理函数,包括cvResize、cvIntegral和cvCanny,这几个函数是cvHaarDetectObjects函数中所需要调用的。其中cvCanny函数可以通过参数设置来选择是否执行(默认是不执行)。由于cvCanny算法比较复杂,并且大量使用了C++语法,移植难度较大,所以我们在移植人脸检测算法时默认不使用canny滤波。另外,为了提高人脸检测算法的识别率,可以使用cvEqualizeHist函数增强图像对比度,然后再进行人脸检测。不过为了简单起见,暂时也还没有移植该函数。经测试发现,AdaBoost人脸检测算法具有很高的检测率,即使不使用cvEqualizeHist也可以在绝大部分情况下成功检测到人脸。并且,不使用cvEqualizeHist也可以降低算法复杂度,减轻DSP的负担。
OpenCV的源码模块清晰,函数之间耦合程度低,移植比较方便。在移植人脸检测算法时,对于算法部分的代码修改很少,主要包括两部分:一是删除其中一些使用IPP的代码,如果不删除编译会出问题。至于如何正确地删除IPP相关代码请参考之前的文档《移植OpenCV到嵌入式平台的注意事项》。二是如何处理其中的CvType haar_type这个结构体,下面详细讲解这个问题。
CvType haar_type( CV_TYPE_NAME_HAAR, icvIsHaarClassifier, (CvReleaseFunc)cvReleaseHaarClassifierCascade, icvReadHaarClassifier, icvWriteHaarClassifier, icvCloneHaarClassifier ); |
haar_type该结构体是与级联分类器文件相关的,用于判断某xml文件是否是haar级联分类器、如何读取xml文件中的数据到cascade结构体以及其他一些复制、写和释放操作。该结构体相当于是一个注册函数,将cvhaar.c文件中的一些函数注册到系统,经系统统一调用。也就是说由于xml文件可能包含各种OpenCV中所使用的数据,每种数据格式需要对应的函数来读取其中的数据。当使用cvLoad检测到xml文件是haar级联分类器文件时,OpenCV就调用cvhaar.c文件中通过CvType注册的对应的读取函数来读xml文件中的数据到cascade结构体。
由于使用cvLoad函数加载分类器xml文件的代码中包含了部分C++代码和语法,并且很难修改成C语言版,所以我干脆放弃使用cvLoad来加载级联分类器,而是在PC上将级联分类器存储为自定义格式的文件,然后再自己编写函数来读取文件并将数据传递给cascade结构体。这样做既减轻了移植的工作量,也降低了算法复杂度。至于级联分类器具体的处理办法见下一节内容。
另外,为了进一步降低DSP负担,我们还可以通过仔细阅读代码然后删掉一些无用的片段。暂时只删掉了一段对cascade数据进行有效性检查的代码,如下所示(红色部分为最后删掉了的)。这部分代码包括四层循环,相当复杂,如果我们保证了传入的cascade数据无误,那么删除掉这部分代码可以减少很多计算量。
for( i = 0; i < cascade->count; i++ ) { CvHaarStageClassifier* stage_classifier = cascade->stage_classifier + i; if( !stage_classifier->classifier || stage_classifier->count <= 0 ) { sprintf( errorstr, "header of the stage classifier #%d is invalid " "(has null pointers or non-positive classfier count)", i ); CV_ERROR( CV_StsError, errorstr ); } max_count = MAX( max_count, stage_classifier->count ); total_classifiers += stage_classifier->count; for( j = 0; j < stage_classifier->count; j++ ) { CvHaarClassifier* classifier = stage_classifier->classifier + j; total_nodes += classifier->count; for( l = 0; l < classifier->count; l++ ) { for( k = 0; k < CV_HAAR_FEATURE_MAX; k++ ) { if( classifier->haar_feature[l].rect[k].r.width ) { CvRect r = classifier->haar_feature[l].rect[k].r; int tilted = classifier->haar_feature[l].tilted; has_tilted_features |= tilted != 0; if( r.width < 0 || r.height < 0 || r.y < 0 || r.x + r.width > orig_window_size.width || (!tilted && (r.x < 0 || r.y + r.height > orig_window_size.height)) || tilted && (r.x - r.height < 0 || r.y + r.width + r.height > orig_window_size.height))) { sprintf( errorstr, "rectangle #%d of the classifier #%d of " "the stage classifier #%d is not inside " "the reference (original) cascade window", k, j, i ); CV_ERROR( CV_StsNullPtr, errorstr ); } } } } } } |
除了以上部分的修改,已经完成的另外一个大修改是将hidcascade的创建放在了初始化阶段、加载分类器数据之后。icvCreateHidHaarClassifierCascade函数用于创建hidcascade结构体并初始化,原来的代码中这部分是放在cvHaarDetectObjects函数中的,我现在把它提出来放到读取分类器文件之后,也即交给ARM处理,DSP专注于运算。
if( !cascade->hid_cascade ) CV_CALL( icvCreateHidHaarClassifierCascade(cascade) ); |
OpenCV中将级联分类器数据存储为xml文件,读取时非常复杂,函数层层嵌套,并且还需要很多递归操作。为了降低复杂度,我将haar分类器数据按最简单的格式存储,只包含纯的数据,不含任何其他冗余信息。存储的顺序就是按照cascade结构体中个成员的定义顺序来存储的,具体的存储代码如下所示。
int SaveCascade(CvHaarClassifierCascade *cascade) { FILE *haar ; int i, j, k, m, tempi; double tempd; float tempf; if((haar = fopen("haar_feature.bin", "wb")) == NULL) return -1; tempi = cascade->flags;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->count;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->orig_window_size.width;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->orig_window_size.height;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->real_window_size.width;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->real_window_size.height;fwrite(&tempi, sizeof(int), 1, haar); tempd = cascade->scale;fwrite(&tempd, sizeof(double), 1, haar);
for(i = 0; i < cascade->count; i ++) { tempi = cascade->stage_classifier[i].count;fwrite(&tempi, sizeof(int), 1, haar); tempf = cascade->stage_classifier[i].threshold;fwrite(&tempf, sizeof(float), 1, haar);
for (j = 0; j < cascade->stage_classifier[i].count; j ++) { tempi = cascade->stage_classifier[i].classifier[j].count;fwrite(&tempi, sizeof(int), 1, haar); printf("cascade->stage_classifier[%d].classifier[%d].count=%d\n", i, j, cascade->stage_classifier[i].classifier[j].count);
for(k = 0; k < cascade->stage_classifier[i].classifier[j].count; k ++) { tempi = cascade->stage_classifier[i].classifier[j].haar_feature[k].tilted;fwrite(&tempi, sizeof(int), 1, haar);
for(m = 0; m < 3; m ++) { tempi = cascade->stage_classifier[i].classifier[j].haar_feature[k].rect[m].r.x;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->stage_classifier[i].classifier[j].haar_feature[k].rect[m].r.y;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->stage_classifier[i].classifier[j].haar_feature[k].rect[m].r.width;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->stage_classifier[i].classifier[j].haar_feature[k].rect[m].r.height;fwrite(&tempi, sizeof(int), 1, haar); tempf = cascade->stage_classifier[i].classifier[j].haar_feature[k].rect[m].weight;fwrite(&tempf, sizeof(float), 1, haar); } } tempf = *(cascade->stage_classifier[i].classifier[j].threshold);fwrite(&tempf, sizeof(float), 1, haar); tempf = *(cascade->stage_classifier[i].classifier[j].threshold + 1);fwrite(&tempf, sizeof(float), 1, haar); tempi = *(cascade->stage_classifier[i].classifier[j].left);fwrite(&tempi, sizeof(int), 1, haar); tempi = *(cascade->stage_classifier[i].classifier[j].left + 1);fwrite(&tempi, sizeof(int), 1, haar); tempi = *(cascade->stage_classifier[i].classifier[j].right);fwrite(&tempi, sizeof(int), 1, haar); tempi = *(cascade->stage_classifier[i].classifier[j].right + 1);fwrite(&tempi, sizeof(int), 1, haar); tempf = *(cascade->stage_classifier[i].classifier[j].alpha);fwrite(&tempf, sizeof(float), 1, haar); tempf = *(cascade->stage_classifier[i].classifier[j].alpha + 1);fwrite(&tempf, sizeof(float), 1, haar); } tempi = cascade->stage_classifier[i].next;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->stage_classifier[i].child;fwrite(&tempi, sizeof(int), 1, haar); tempi = cascade->stage_classifier[i].parent;fwrite(&tempi, sizeof(int), 1, haar); } fclose(haar); return 0; } |
文件的读取与存储是对应操作,只需要按照以上顺序读取到cascade结构体中即可。重点需要考虑cascade结构体中包含的许多指针,这些指针在读取数据时需要根据实际情况来分配存储空间,例如:
cascade->stage_classifier = (CvHaarStageClassifier *)malloc(cascade->count * sizeof(CvHaarStageClassifier)); cascade->stage_classifier[i].classifier = (CvHaarClassifier *)malloc((cascade->stage_classifier[i].count) * sizeof(CvHaarClassifier)); cascade->stage_classifier[i].classifier[j].threshold = (float *)malloc(2 * sizeof(float)); |
在上一节中加载cascade时使用的内存分配方法经验证在DM6467上会出错,编译没有问题,但运行程序时出现如图 10所示的错误。
可以看出,由于cascade结构体要占用大量琐碎的内存空间,CMEM在分配内存时超过一定量就会出现错误,提示内存不足。这是因为CMEM模块主要是用来分配大块的物理连续内存,对于这种大量的琐碎内存分配能力不足,结果导致内存碎片过多,进而内存分配失败。
图 10 内存分配错误
为了解决这个问题,我们需要将cascade结构体所占用的内存空间大小计算出来,然后分配一整块连续内存空间给cascade。在查看icvCreateHidHaarClassifierCascade函数源码时意外地发现该函数中对于hidcascade的内存就是连续分配的,所以我们可以参考其方案来为cascade分配连续内存。最终的内存分配代码如下所示:
#define STAGE_CLASSIFIER_COUNT 22 #define CLASSIFIER_COUNT 2135 int classifier_count[22] = {3,16,21,39,33,44,50,51,56,71,80,103,111,102,135,137,140,160,177,182,211,213}; Memory_AllocParams cvMemParams = {Memory_CONTIGHEAP, Memory_NONCACHED, Memory_DEFAULTALIGNMENT, 0}; CvHaarClassifierCascade * LoadCascade() { FILE *haar = NULL; int i, j, k, m, tempi, total_size; double tempd; float tempf; void * ptr; CvHaarClassifierCascade *cascade = NULL;
total_size = sizeof(CvHaarClassifierCascade) + STAGE_CLASSIFIER_COUNT * sizeof(CvHaarStageClassifier) + CLASSIFIER_COUNT * (sizeof(CvHaarClassifier) + sizeof(CvHaarFeature) + 4 * sizeof(int) + 4 * sizeof(float)); cascade = (CvHaarClassifierCascade *)Memory_alloc(total_size, &cvMemParams);//malloc(total_size); cascade->stage_classifier = (CvHaarStageClassifier *)(cascade + 1); …… } |
其中,STAGE_CLASSIFIER_COUNT、CLASSIFIER_COUNT和classifier_count[22]的值都是通过在存储cascade数据到文件时计算出来的。由于我们现在只使用一个固定的人脸分类器文件,所以可以将这个参数设置为固定值,如果要使代码能够读取其他文件,那么可以在存储cascade时将这几个参数的值存储在文件的最开始部分,在读取时就可以快速确定这些参数的值了。
DM6467是ARM+DSP双核架构,ARM端的Linux使用虚拟地址,DSP端的DSP/BIOS使用物理地址,ARM端的指针不能直接传递给DSP使用,因此,在将数据从ARM端传递到DSP端时需要进行虚拟地址和物理地址之间的转换。
对于虚拟地址和物理地址之间的转换,TI提供了CMEM模块可以很方便的进行地址转换。如果在分配内存时我们直接使用malloc分配,那么是无法获取物理地址的。所以,如果某段数据需要传递到DSP端,那么必须使用CMEM模块进行内存分配。
C6Accel使用了CodecEngine中的iUniversal,对于提供的一帧视频数据是提供了地址转换操作的,但是在进行人脸检测时,需要传递级联分类器cascade结构体,该结构体中包含一些指针变量,这些指针就需要自己在ARM端动手进行地址转换。CMEM模块已经提供了易用的API进行地址转换,具体的转换代码如下所示:
void traverse_and_translate_cascade(CvHaarClassifierCascade *cascade ) { CvHaarStageClassifier *stage; CvHaarClassifier *classifier; CvHaarFeature *feature; int stage_count, classifier_count, feature_count; unsigned int new_thresh, new_left, new_right, new_alpha; int i, j, k; stage_count = cascade->count; for (i = 0; i < stage_count; i++) { stage = cascade->stage_classifier + i; classifier_count = stage->count; for (j = 0; j < classifier_count; j++) { classifier = stage->classifier + j; feature_count = classifier->count; for (k = 0; k < feature_count; k++) { feature = classifier->haar_feature + k; } classifier->haar_feature = classifier->haar_feature ? (void *)Memory_getBufferPhysicalAddress(classifier->haar_feature, sizeof(CvHaarFeature),NULL) : NULL; classifier->threshold = classifier->threshold ? (void *)Memory_getBufferPhysicalAddress(classifier->threshold,sizeof(float),NULL) : NULL; classifier->left = classifier->left ? (void *)Memory_getBufferPhysicalAddress(classifier->left,sizeof(int),NULL) : NULL; classifier->right = classifier->right ? (void *)Memory_getBufferPhysicalAddress(classifier->right,sizeof(int),NULL) : NULL; classifier->alpha = classifier->alpha ? (void *)Memory_getBufferPhysicalAddress(classifier->alpha,sizeof(float),NULL) : NULL; Memory_cacheWbInv( (void *)classifier, sizeof(CvHaarClassifier)); }
stage->classifier = stage->classifier ? (void *)Memory_getBufferPhysicalAddress(stage->classifier,sizeof(CvHaarClassifier),NULL) : NULL; Memory_cacheWbInv( (void *)stage,sizeof(CvHaarStageClassifier)); } traverse_and_translate_hid_cascade(cascade->hid_cascade, fp); cascade->stage_classifier = cascade->stage_classifier ? (void *)Memory_getBufferPhysicalAddress(cascade->stage_classifier,sizeof(CvHaarStageClassifier),NULL) : NULL; cascade->hid_cascade = cascade->hid_cascade ? (void *)Memory_getBufferPhysicalAddress(cascade->hid_cascade,sizeof(CvHidHaarClassifierCascade),NULL) : NULL; (unsigned int)cascade->hid_cascade); Memory_cacheWbInvAll(); fclose(fp); } |
为了规范DSP上算法的编写,降低集成和移植的难度,TI提出了xDAIS算法标准,并在其基础上针对多媒体应用而对xDAIS进行扩展,成为xDM标准。符合xDM标准的算法可以无缝集成到Codec Engine框架中,很方便地供ARM端调用。xDAIS算法标准规定算法不能自己分配内存,必须是向系统申请,所有的内存都由系统统一分配、管理和释放。
在OpenCV中,大量的图像处理算法都有各种各样的内存需求,如果对所有的内存需求都需要向系统申请,那么移植OpenCV到DSP就几乎是不可能的事情了,要修改的代码太多甚至可能根本无法修改。所以,我们需要考虑如何使得一个DSP端的codec能够自己分配内存。
经过多方查找,终于在一个博客中发现了相关信息。C语言中分配内存一般我们使用malloc函数,malloc函数申请的内存默认是定位到动态存储区的堆区(Heap)中,在TI的code generation tool编译器中需要使用伪代码将用malloc分配的内存重定位到堆区。如果没有指定malloc分配内存所放的位置,那么就会内存分配失败。其实,在codec中不能分配内存就是因为默认时没有指定malloc分配内存所放置的位置。这么来看,要使得DSP端的codec使用malloc就是很容易的了。
在Codec Server中,mmemap.tci文件用于分配各个段的具体起始物理地址和段大小,server.tcf文件用于配置与内存相关的一些参数,也包括代码重定位的配置。
修改在$(C6ACCEL_INSTALL_DIR)/c6accel/soc/packages/ti/c6accel_unitserver/dm6467/中的server.tcf文件,定为malloc到heap区(添加以下红色代码)。
/* =========================================================================== * MEM : Global * =========================================================================== */ prog.module("MEM").BIOSOBJSEG = bios.DDRALGHEAP; prog.module("MEM").MALLOCSEG = bios.DDRALGHEAP; |
在移植人脸算法到DM6467上的过程中,可以使用一些小技巧,以减轻工作量、加快移植速度,提高算法效率等。具体来说,我在移植过程中收获了以下一些经验:
1, 人脸检测算法比较复杂,要移植到嵌入式平台,必须对算法的执行过程有清晰的了解。要达到该要求,大家直接想到的肯定是单步调试。但是由于一般在PC上编写OpenCV程序时都是使用了库,无法单步查看源码,所以,我们可以把OpenCV中cv和cxcore部分的源码全部拿出来和应用程序一起组成一个VS2010工程,然后再单步运行程序,就可以查看算法每一步是如何执行。了解了算法的细节之后移植才会更容易。
2, 由于移植OpenCV源码移植到嵌入式平台时需要做很多修改,修改过程中出现很多错误是难免的事,那么代码的调试工作就是一件很烦人的事了。为了降低调试的复杂度,强烈建议将算法在PC上编写好之后再移植。VS2010拥有强大的调试能力,可以很容易发现代码中的错误和bug。
3, 在调试程序时一定要多做备份。很可能原来好的代码经过多次修改之后出现错误再改回去就很困难了,所以,备份程序时非常重要的。每次程序做了大修改或者每次程序调试成功之后都需要保存一份备份文件。同时,还可以使用虚拟机的snapshot功能很方便地备份整个虚拟机。在使用snapshot时注意它可以使用分支备份,这是很好一个功能。另外,为了减小备份的文件大小,建议每次都讲虚拟机关闭或者中止之后再备份。
4, 在编程序之前一定要想好该怎么编,不要没有思路就开始瞎编,这很可能导致在没有弄清楚原理时编写的错误代码在后期很难发现。要知道其实编写程序是很快的,但要知道怎么编这才更重要。
5, 在将级联分类器数据存储到文件时,如果fwrite中使用”wt”参数(文本文件)会导致最后的文件大小比实际数据大一点点,进而导致读取数据时出错。我没有发现这是什么原因,初步估计是与文件分页相关。后来将”wt”参数修改为”wb”就正确了。
最终程序运行效果如图 11和图 12所示。
图 11 人脸检测效果图1
图 12 人脸检测效果图2
可以看出,程序对于单张人脸和多人脸都能成功识别,并且经测试发现,当人脸倾斜角度不是很大时也完全可以识别,这说明AdaBoost人脸检测算法鲁棒性极好。
在显示器输出人脸检测结果的同时,串口终端输出的一些程序运行信息如图 13所示。
图 13 串口终端输出信息
从图 13中可以看出,AdaBoost人脸检测算法计算量很大,DSP一直接近满负荷,ARM端CPU占有率较低,因为在等待DSP完成计算返回结果。由于这人脸检测算法移植到DM6467的工作是初步完成,还没有对代码进行任何优化,现在已经能够达到2帧每秒的速度也还算是不错了。
从上一节可以看出,当前人脸检测在DM6467上运行的效果还很一般,速度只有2帧每秒,完全无法达到实时性的要求,所以待解决的问题主要就是如何提高算法效率,提高实时性,具体来说还有以下一些工作可以做:
1, 仔细阅读人脸检测算法源码,删除无用部分,减小代码量以及降低算法复杂度。
2, 对输入图像设置人脸检测的感兴趣区域,减小计算窗口,这可以大大降低人脸检测算法的运算时间。
3, 由于视频中连续两帧之间的关联性很大,所以可以根据上一帧检测到人脸的位置来大致判断下一帧中人脸所在的位置。
4, 图像积分算法可以交由VLIB来处理,由于VLIB是使用C和汇编混合编程,算法经过优化,效率极高,所以可以先用VLIB进行积分再由OpenCV检测人脸。
5, 可以在人脸检测之前将图像放缩至1/4或者更小,缩短人脸检测的时间。
6, 可以平衡ARM和DSP端的负载分配,当前情况下ARM端比较闲而DSP端一直满载运行,所以可以再将一部分计算交给ARM来处理,例如resize。
7, 可以动态调整cvHaarDetectObjects函数的参数。
8, 增加图像直方图均衡化的功能(cvEqualizeHist),增强图像对比度,提高人脸检测算法的识别率。
9, 在人脸检测算法中将一些复杂代码进行优化,包括线性汇编、循环展开、内联函数和宏定义等,也还可以使用DSPLIB、IMGLIB等库提供的函数。
10, 考虑编译器优化,通过设置编译参数提高流水线效率。
11, 使用cache,将图像分块放置到cache中计算,可以大大缩短计算时间。
12, 使用EDMA进行内存搬运,大大降低CPU负担,使CPU专注于运算。
13, 考虑如何优化指针对齐,提高从内存中读取数据的速度。
14, 考虑如何减少内存碎片,尽量使用连续内存。
在完成人脸检测之后,如果时间充足,那么可以再拿一段时间来进行代码优化,做到每秒10帧以上,基本达到实时性要求。
既然我们现在已经能够检测人脸,根据Haar-AdaBoost算法,我们也可以检测其他刚性物体,这只需要一个训练过程,虽然训练过程比较麻烦。