OpenCV學習筆記(9)利用MFC的Picture控制元件顯示影象+播放視訊和捕獲攝像頭畫面
P.S.:後面又添加了播放視訊和捕獲攝像頭畫面的功能,其中播放視訊的功能只有 'Play' 和 'Stop',不能實現暫停,《A step-by-step guide to the use of Microsoft Visual C++ and the Intel OpenCV library》 中好像有關於多執行緒防止任務衝突(Mutex)的內容,深入學習和了解那一部分後再嘗試做暫停的功能和其它更復雜的應用。
下面是新增功能後的專案檔案(可直接執行):
在《OpenCV教程-基礎篇》的2.8節中,所建立的MFC影象顯示是直接放在對話方塊面板的左上角的,感覺不大美觀;《MFC中快速應用OpenCV
一、建立MFC
首先建立一個MFC對話方塊應用程式(Dialog-based Application)如下:
在VS2005和2008裡,我們可以用一個 Solution 來組合幾個 Project (每個 Project 基本上只包含一個 Program),當我們要構建一個多Program的應用時(例如一個客戶端程式加一個伺服器應用程式),利用 Solution 可以將這些 Projects 組合起來、並且共享檔案和函式庫。通常需要為Solution建立一個主路徑,其中包含了所有Projects的路徑。不過在這篇文章裡,我們只構建一個簡單的Project,所以在建立MFC的New Project對話方塊裡,不用勾選“Create directory for solution”這個選項。
點選OK -- Next進入下一步,在這裡我們建立一個Dialog-based Application,大部分選項按預設設定就行,不過最下面的“Use Unicode libraries”最好去掉。如果勾選了這個選項,程式程式碼就會使用16bit的Unicode字符集來編碼,但是很多函式雖然使用 char* (ASCII stings) 型別字元,而將字串從 Unicode 轉換到 ASCII 是非常麻煩的。使用 Unicode 在編譯時可能會遇到下列錯誤:
cannot convert parameter 1 from 'CString' to 'const char *' cannot convert from'const char [11]' to 'LPCWSTR'
這意味著在Unicode和Multi-byte字串的轉換中出現了問題。在上一篇學習筆記中,就提到“成員函式LoadBMP其輸入引數型別應為 const char*”,那應該只是一個治標的方法,這裡的去掉“Use Unicode libraries”選項,才是治本之道。
往後的幾步設定,可以根據自己的需要來操作,我的設定如下:
二、編寫程式碼
在Resource View面板->mymfc(工程名稱)->mymfc.rc->Dialog雙擊IDD_MYMFC_DIALOG,可以看到一個初始的GUI介面,往裡面新增兩個 Button 和 一個 Picture 控制元件,如下:
選中單個控制元件、右擊選擇屬性(Properties),可以看到控制元件的ID號,這個號可以自行編輯,例如 Picture 控制元件的 ID 號我設定為 IDC_ShowImg,這個 ID 號在後面的影象顯示函式中要用到。
首先在專案屬性中載入lib檔案:選單Project -> Properties -> Configuration Properties -> Linker –> Input -> additional dependencies 中加入 cxcore200.lib cv200.lib highgui200.lib。
然後在 mymfc.h 的 #include "resource.h" 下加入如下程式碼:
#include "cv.h" #include "highgui.h"#define IMAGE_WIDTH 256#define IMAGE_HEIGHT 256#define IMAGE_CHANNELS 3
在 Class View 面板右擊 CmymfcDlg,選擇 Add –> Add Variable,新增一個 IplImage* 型別的變數 TheImage;再點選 CmymfcDlg,在下面視窗的列表中雙擊 OnInitDialog,在“// TODO: Add extra initialization here”下面新增 TheImage 的初始化程式碼:
CvSize ImgSize; ImgSize.height = IMAGE_HEIGHT; ImgSize.width = IMAGE_WIDTH; TheImage = cvCreateImage( ImgSize, IPL_DEPTH_8U, IMAGE_CHANNELS );
然後雙擊 OnPaint,在 if(IsIconic())…的 else 裡新增以下程式碼,用來重繪視窗:
CDialog::OnPaint(); // 重繪對話方塊 CDialog::UpdateWindow(); // 更新windows視窗,如果無這步呼叫,圖片顯示還會出現問題 ShowImage( TheImage, IDC_ShowImg ); // 重繪圖片函式
接著在 CmymfcApp 下面的成員列表中雙擊 InitInstance,在兩個“// TODO: Place code here to handle when the dialog is…”下面新增:
cvReleaseImage( &dlg.TheImage );
即按下“OK”或“Cancel”時,釋放TheImage佔用的記憶體。
接下來就是寫讀取和處理圖片的功能函數了。
回到 mymfc 的 GUI 編輯介面中,右擊按鈕 ReadImg,選擇 Add Event Handler,建立按鈕點選的訊息響應程式:
控制代碼名設定為 OnBnClickedReadimg,主要的響應操作包括 彈出對話方塊選擇圖片檔案、讀入圖片檔案、對圖片統一縮放至256*256的大小、顯示影象,程式碼如下:
// TODO: Add your control notification handler code here CFileDialog dlg( TRUE, _T("*.bmp"), NULL, OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_HIDEREADONLY, _T("image files (*.bmp; *.jpg) |*.bmp; *.jpg | All Files (*.*) |*.*||"), NULL ); // 選項圖片的約定 dlg.m_ofn.lpstrTitle = _T("Open Image"); // 開啟檔案對話方塊的標題名if( dlg.DoModal() != IDOK ) // 判斷是否獲得圖片return; CString mPath = dlg.GetPathName(); // 獲取圖片路徑 IplImage* ipl = cvLoadImage( mPath, 1 ); // 讀取圖片、快取到一個區域性變數 ipl 中if( !ipl ) // 判斷是否成功載入圖片return; if( TheImage ) // 對上一幅顯示的圖片資料清零 cvZero( TheImage ); ResizeImage( ipl ); // 對讀入的圖片進行縮放,使其寬或高最大值者剛好等於 256,再複製到 TheImage 中 ShowImage( TheImage, IDC_ShowImg ); // 呼叫顯示圖片函式 cvReleaseImage( &ipl ); // 釋放 ipl 佔用的記憶體
其中包含了兩個新的成員函式 ResizeImage 和 ShowImage,前者的作用是對讀入的不同大小的影象進行縮放,再通過設定 ROI 的方式將影象存入 256*256 的 TheImage 中;後者是將影象 TheImage 顯示到圖片顯示控制元件 IDC_ShouImg 視窗的正中部位。為了實現這兩個功能,首先在 Class View 面板右擊 CmymfcDlg,選擇 Add –> Add Function,建立兩個函式:void ShowImage( IplImage* img, UINT ID ) 和 void ResizeImage(IplImage* img)。以下是這兩個函式的實現程式碼:
void CmymfcDlg::ResizeImage(IplImage* img) { // 讀取圖片的寬和高int w = img->width; int h = img->height; // 找出寬和高中的較大值者int max = (w > h)? w: h; // 計算將圖片縮放到TheImage區域所需的比例因子float scale = (float) ( (float) max /256.0f ); // 縮放後圖片的寬和高int nw = (int)( w/scale ); int nh = (int)( h/scale ); // 為了將縮放後的圖片存入 TheImage 的正中部位,需計算圖片在 TheImage 左上角的期望座標值int tlx = (nw > nh)?0: (int)(256-nw)/2; int tly = (nw > nh)? (int)(256-nh)/2: 0; // 設定 TheImage 的 ROI 區域,用來存入圖片 img cvSetImageROI( TheImage, cvRect( tlx, tly, nw, nh) ); // 對圖片 img 進行縮放,並存入到 TheImage 中 cvResize( img, TheImage ); // 重置 TheImage 的 ROI 準備讀入下一幅圖片 cvResetImageROI( TheImage ); } void CmymfcDlg::ShowImage( IplImage* img, UINT ID ) // ID 是Picture Control控制元件的ID號{ CDC* pDC = GetDlgItem( ID ) ->GetDC(); // 獲得顯示控制元件的 DC HDC hDC = pDC ->GetSafeHdc(); // 獲取 HDC(裝置控制代碼) 來進行繪圖操作 CRect rect; GetDlgItem(ID) ->GetClientRect( &rect ); int rw = rect.right - rect.left; // 求出圖片控制元件的寬和高int rh = rect.bottom - rect.top; int iw = img->width; // 讀取圖片的寬和高int ih = img->height; int tx = (int)(rw - iw)/2; // 使圖片的顯示位置正好在控制元件的正中int ty = (int)(rh - ih)/2; SetRect( rect, tx, ty, tx+iw, ty+ih ); CvvImage cimg; cimg.CopyOf( img ); // 複製圖片 cimg.DrawToHDC( hDC, &rect ); // 將圖片繪製到顯示控制元件的指定區域內 ReleaseDC( pDC ); }
函式 ResizeImage 是參考了學習筆記(5)中單視窗顯示多幅影象的函式 cvShowMultiImages 修改而成的,函式 ShowImage 則是參考了帖子《OpenCV如何把影象顯示到MFC的picture控制元件上》的程式碼,另外下面幾個帖子也可以參考:
最後是要對讀入的影象做簡單的Canny邊緣處理,為此,建立一個按鈕 EdgeDetect,相應的響應程式碼如下:
void CmymfcDlg::OnBnClickedEdgedetect() { // TODO: Add your control notification handler code here IplImage *gray =0, *edge =0; gray = cvCreateImage( cvSize(IMAGE_WIDTH, IMAGE_HEIGHT), IPL_DEPTH_8U, 1 ); edge = cvCreateImage( cvSize(IMAGE_WIDTH, IMAGE_HEIGHT), IPL_DEPTH_8U, 1 ); cvCvtColor( TheImage, gray, CV_BGR2GRAY ); cvCanny( gray, edge, 30, 100, 3 ); cvCvtColor( edge, TheImage, CV_GRAY2BGR ); ShowImage( TheImage, IDC_ShowImg ); // 呼叫顯示圖片函式 cvReleaseImage( &gray ); cvReleaseImage( &edge ); }
這裡主要是參考了《OpenCV教程-基礎篇》P33的程式碼,不過並沒有像書中那樣建立新的 MyIplClass 類來進行影象的讀取和處理操作。我覺得這篇文章中直接對 TheImage 進行處理可能不大妥當,按照教程那樣新建一個類來處理才是比較穩妥吧。這裡涉及到函式返回物件例項的過程、深拷貝和淺拷貝、拷貝構造、動態記憶體的使用、opencv相關介面的程式碼等方面,以後還要進一步深入學習和理解!
最後我們還可以在Resource View面板->mymfc(工程名稱)->mymfc.rc->Version中修改程式的產品版本、名稱等資訊。
來看看程式生成後的效果: