程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++開發人臉性別識別教程(14)——灰度圖像識別BUG處理

C++開發人臉性別識別教程(14)——灰度圖像識別BUG處理

編輯:關於C++

  在這篇博客中,我們對目前程序中一個隱藏很深的BUG進行處理,這個BUG導致程序目前有一部分邏輯出現錯誤(雖然沒有表現出來)。

  一、觸發BUG

  1、准備觸發樣本

  為了復現這個隱藏的BUG,需要實現准備兩張測試樣本,一張是彩色圖(三通道圖),一張是灰度圖(單通道圖):

\

  臨時讀入這兩個圖像,驗證其屬性:

\

  注意此時程序能夠正常讀取這兩個圖片,不會崩潰。

  2、修改代碼,觸發BUG

  解析來我們修改“圖片文件夾”按鈕對應事件響應函數中圖像讀取的代碼,這裡由於這是為了復現BUG,只修改一個模式下的讀取函數即可,假設我們修改“圖片文件”模式下的讀取函數:

\

  我們這裡就是修改了cvLoadImage()的在讀取圖片時的標志位,“-1”代表原始讀取方式,即在讀取時不會改變圖像的任何屬性(如通道數,位數等),這種更改看上去是很合理的,因為我們應該保證加載到的圖像就是圖像本身,在加載圖像的過程中對圖像進行預處理本身是不合理的,因為我們會編寫專門的圖像預處理代碼。

  此時,在“圖片文件”模式下讀取彩色圖像進行性別識別,一切正常。然後我們在“圖片文件”模式下讀取灰度圖(單通道圖),程序崩潰了:

\

  接下來開始追蹤定位這個BUG。

  二、定位第一個BUG

  出現這個錯誤之後,首先判斷是否在圖像加載的過程中出了問題。在cvLoadImage()函數處添加一個斷點,F5調試運行:

\

  程序運行到斷點處後,按下F10,運行斷點對應的程序語句,發現圖片加載正常:

\

  繼續按F10,運行下面的人臉檢測函數(detect_and_draw),程序崩潰,因此可以確定BUG出在detect_and_draw函數中。

  detect_and_draw函數中,最復雜的操作莫過於是人臉檢測函數cvHaarDetectObjects了,因此這裡出現BUG的可能性也非常大,在這裡設置一個斷點,F5運行,程序崩潰,說明BUG在這條語句之前。

  此時我們有理由懷疑是直方圖均衡函數cvEqualizeHist()出了問題,在這個語句前面打一個斷點,F5運行,程序依然崩潰,說明前面還有錯誤。

  此時detect_and_draw()函數還剩三行代碼需要檢測,不妨將斷點設在函數開始的位置,F5運行,程序正常進入函數體:

\

  按F10單步運行,意外發現程序准備執行cvCvtColor()語句,這條語句是對圖像進行灰度化處理,但很顯然我們選擇的圖片本身就是灰度圖,也就是單通道圖,對灰度圖進行灰度化,程序自然崩潰,第一個BUG算是找到了:

\

  至於這個BUG出現的原因,想必大家此時都已經看出來了,由於我之前粗心,將if的判斷條件寫成了賦值語句if (img->nChannels = 3),導致這個條件永遠為真,以至於不管是否為彩色圖,都會執行if內的語句,程序崩潰,氣人的是當輸入圖像本身就是彩色圖時,這個BUG並不會顯現,隱藏很深。

  三、定位第二個BUG

  修改if條件為“if (img->nChannels == 3)”,運行程序,選擇灰度圖像進行性別識別,程序再次崩潰。

  通過之前的方法我們很快確定BUG依然出在detect_and_draw函數中,同樣我們先檢查其中的cvHaarDetectObjects()函數,經過“設置斷點—>F5調試—>F10單步運行”的調試方式,很快確定這個函數能夠正常運行:

\

  接下來我們有理由推測是性別識別函數GenderRecognition()出現問題,在這裡打上斷點,F5運行,程序崩潰,說明問題在前面。

  此時我們能夠確定BUG出在cvHaarDetectObjects()函數和GenderRecognition()函數之間,這部分包含兩段代碼,一段代碼是用來統計人臉檢測結果中面積最大的矩形標號,另一段代碼是用來繪制矩形檢測結果。將斷點設在“if(objects->total > 0)”處,F5調試運行程序,程序能夠正常命中斷點,說明前面的代碼都沒問題。

  F10單步運行斷點之後的代碼,在這裡又發現一處if (img->nChannels = 3)的錯誤,趕緊將其改正。

  再次F5運行程序,命中斷點,F10單步運行,發現程序居然順利通過GenderRecognition()函數,似乎程序BUG已經消失,繼續F10單步運行,程序在“cvReleaseImage(&gray);”語句處發生錯誤,很明顯句代碼有問題。

  四、終極BUG的發現

  首先來解決“cvReleaseImage(&gray);”這句代碼的BUG。這是IplImage結構體所特有的釋放內存的語句,IplImage類型的變量在生命周期結束之後需要通過這句代碼來顯示的釋放掉內存,而OpenCv2.x中的Mat類型則不需要這部操作(封裝了智能指針),所以從這點來講Mat類還是要優於IplImage結構體的。如果這句代碼出現問題,很大一部分原因就是待釋放的對象不存在(或者說是已經釋放過),我們在這行代碼處設置一個斷點,F5運行程序,果然,變量gray已經被清空:

\

  那我們是在什麼時候不經意間把這個IplImage變量釋放掉的呢?最值得懷疑的就是上面的那句“cvReleaseImage(&faceImage);”代碼,很可能是這句代碼在釋放faceImage變量時連同gray變量也一起釋放掉了,我們將斷點放在這行代碼上,驗證我們的猜想,從下圖中可以看到,在執行這句代碼之前,faceImage和gray兩個變量對應的圖像均不為空:

\

  程序運行到這裡細心的朋友應該能夠發現問題了,那就是faceImage所存儲的圖片居然和img變量所對應的圖片是一樣的!這顯然是不合道理的,因為理論上faceImage中保存的應該是分割出來的人臉圖像,然後把這個圖像送入GenderRecognition(faceImage)函數中進行性別識別,可現在的情況卻是faceImage保存了原始圖片,並且是經過直方圖均衡化的原始圖片,也就意味著此時進行性別識別的是全部圖像而非人臉部分,這種情況下的性別識別毫無道理可言,這就是所謂的終極BUG。

  五、修改

  首先,我們分析這個BUG出現的原因,理論情況下是通過檢測到的人臉矩形,在原圖(img)的基礎上進行人臉區域分割,將分割得到的人臉賦值給faceImage變量,很明顯這部分代碼現在出了問題:

\

  為什麼會出問題呢?仔細分析這段代碼,不難看出,如果當前圖片是彩色圖片,則“img->nChannels == 3”條件為真,執行cvCvtColor()函數,且我們已經為img圖像變量指定了ROI區域,因此在灰度化操作中會只對ROI區域的圖像進行操作,因此這步灰度化操作也間接的完成了ROI區域(也就是人臉區域)的分割;而目前的圖片是灰度圖片,“img->nChannels == 3”條件為假,執行“faceImage = img;”語句,問題是這種直接用等號連接的賦值語句屬於“弱拷貝”,也就是只拷貝指針指向地址,但兩個變量仍共享一段內存區域,通俗的將就是“faceImage”和“img”這兩個變量本質上都代表了同一個內存中的圖像,這也就解釋了為什麼我們在釋放完faceImage之後,連同img、gray這兩個變量也一起釋放掉了,因為它們三個之間都是通過這種“弱拷貝”的關系聯系在了一起,都代表著同一圖像。

  更嚴重的問題就是在進行“弱拷貝”的過程中,ROI區域是無效的,因此在這種情況下人臉分割失敗。

  很明顯,消除這種BUG的最有效的方法就是將“弱拷貝”替換為深度拷貝,即兩個變量代表兩個圖像,指向兩個不同的內存地址,這樣就不會因為位置的連帶關系而引發莫名的BUG。IplImage對應的深度拷貝函數有兩個:void cvCopy( const CvArr* src, CvArr* dst, const CvArr* mask=NULL )和IplImage* cvCloneImage( const IplImage* image )。其中cvCopy在拷貝過程中只拷貝設置的ROI區域,正好符合我們的需求,因此在這裡采用這個函數來替換之前的“淺拷貝”:

\

  以及:

\

  此時再次運行程序,發現faceImage中正常保存的分割後的人臉圖像:

\

  至此,這個重大BUG修改完成。

  六、總結

  在這篇博文中講述了一個發現BUG、定位BUG、分析BUG、解決BUG的完整過程,希望對大家有所幫助。在下一篇博客中,開始引入攝像頭。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved