1 人臉檢測算法在DM6467上移植的步驟
要将人臉檢測算法移植到DM6467,我們使用OpenCV現有的源碼作為基礎。首先,需要在PC上用C語言實作人臉檢測的程式編寫,然後移植OpenCV到DM6467,接下來再修改代碼直至程式運作無誤。
1.1 PC上用C語言實作人臉檢測
在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 <stdio.h> 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
1.2 OpenCV基礎架構在DM6467上的移植
要移植人臉檢測算法到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》。
1.3 OpenCV人臉檢測算法相關代碼的移植
在完成了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) ); |
2 移植中的難點與注意事項
2.1 級聯分類器的加載
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)); |
2.2 連續記憶體
在上一節中加載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時将這幾個參數的值存儲在檔案的最開始部分,在讀取時就可以快速确定這些參數的值了。
2.3 虛拟位址和實體位址之間的轉換
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); } |
2.4 Codec中使用malloc
為了規範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區(添加以下紅色代碼)。
prog.module("MEM").BIOSOBJSEG = bios.DDRALGHEAP; prog.module("MEM").MALLOCSEG = bios.DDRALGHEAP; |
2.5 一些技巧和經驗
在移植人臉算法到DM6467上的過程中,可以使用一些小技巧,以減輕工作量、加快移植速度,提高算法效率等。具體來說,我在移植過程中收獲了以下一些經驗:
1, 人臉檢測算法比較複雜,要移植到嵌入式平台,必須對算法的執行過程有清晰的了解。要達到該要求,大家直接想到的肯定是單步調試。但是由于一般在PC上編寫OpenCV程式時都是使用了庫,無法單步檢視源碼,是以,我們可以把OpenCV中cv和cxcore部分的源碼全部拿出來和應用程式一起組成一個VS2010工程,然後再單步運作程式,就可以檢視算法每一步是如何執行。了解了算法的細節之後移植才會更容易。
2, 由于移植OpenCV源碼移植到嵌入式平台時需要做很多修改,修改過程中出現很多錯誤是難免的事,那麼代碼的調試工作就是一件很煩人的事了。為了降低調試的複雜度,強烈建議将算法在PC上編寫好之後再移植。VS2010擁有強大的調試能力,可以很容易發現代碼中的錯誤和bug。
3, 在調試程式時一定要多做備份。很可能原來好的代碼經過多次修改之後出現錯誤再改回去就很困難了,是以,備份程式時非常重要的。每次程式做了大修改或者每次程式調試成功之後都需要儲存一份備份檔案。同時,還可以使用虛拟機的snapshot功能很友善地備份整個虛拟機。在使用snapshot時注意它可以使用分支備份,這是很好一個功能。另外,為了減小備份的檔案大小,建議每次都講虛拟機關閉或者中止之後再備份。
4, 在程式設計式之前一定要想好該怎麼編,不要沒有思路就開始瞎編,這很可能導緻在沒有弄清楚原理時編寫的錯誤代碼在後期很難發現。要知道其實編寫程式是很快的,但要知道怎麼編這才更重要。
5, 在将級聯分類器資料存儲到檔案時,如果fwrite中使用”wt”參數(文本檔案)會導緻最後的檔案大小比實際資料大一點點,進而導緻讀取資料時出錯。我沒有發現這是什麼原因,初步估計是與檔案分頁相關。後來将”wt”參數修改為”wb”就正确了。
3 移植結果分析以及總結和展望
3.1 移植結果分析
最終程式運作效果如圖 11和圖 12所示。
圖 11 人臉檢測效果圖1
圖 12 人臉檢測效果圖2
可以看出,程式對于單張人臉和多人臉都能成功識别,并且經測試發現,當人臉傾斜角度不是很大時也完全可以識别,這說明AdaBoost人臉檢測算法魯棒性極好。
在顯示器輸出人臉檢測結果的同時,序列槽終端輸出的一些程式運作資訊如圖 13所示。
圖 13 序列槽終端輸出資訊
從圖 13中可以看出,AdaBoost人臉檢測算法計算量很大,DSP一直接近滿負荷,ARM端CPU占有率較低,因為在等待DSP完成計算傳回結果。由于這人臉檢測算法移植到DM6467的工作是初步完成,還沒有對代碼進行任何優化,現在已經能夠達到2幀每秒的速度也還算是不錯了。
3.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, 考慮如何減少記憶體碎片,盡量使用連續記憶體。
3.3 下一步工作展望
在完成人臉檢測之後,如果時間充足,那麼可以再拿一段時間來進行代碼優化,做到每秒10幀以上,基本達到實時性要求。
既然我們現在已經能夠檢測人臉,根據Haar-AdaBoost算法,我們也可以檢測其他剛性物體,這隻需要一個訓練過程,雖然訓練過程比較麻煩。