標籤:

在 MFC 框架中,有什麼方法能直接將 OpenCV 2.0 庫中 Mat 格式的圖片傳遞到 Picture Control(圖片控制項)顯示?

本人現在在做一個目標檢測的項目,選用了 OpenCV 2.0 庫和 MFC 框架上,在載入顯示圖片的時候,使用的是 Cvvimage 類里的函數 DrawPicToHDC( IplImage *img, UINT ID) ,將讀入的圖片自適應顯示到 Picture Control(圖片控制項)上,但這個類只支持 OpenCV 1.0的 IplImage 圖片格式操作,每次顯示完之後,還需要把 IplImage 轉為 OpenCV 2.0下的 Mat 格式。然後,對 Mat 格式進行處理並檢測, 又要重新將 Mat 轉為 IplImage ,又要再使用 DrawPicToHDC 將 檢測結果傳遞到畫板上 。

請問在 MFC 框架中,有什麼方法能直接將 Mat 格式的圖片傳遞到 Picture Control(圖片控制項)顯示,而不在 IplImage 和 Mat 兩種圖片格式上反覆轉換了?


另外一個方法,也可以把OpenCV的窗口添加到PictureControl裡面,這樣你可以繼續使用OpenCV的setMouseCallback直接對PictureControl進行滑鼠操作。

首先在你的OnInit函數中添加如下

namedWindow("view", WINDOW_AUTOSIZE);
HWND hWnd = (HWND) cvGetWindowHandle("view");
HWND hParent = ::GetParent(hWnd);
::SetParent(hWnd, GetDlgItem(IDC_STATIC_CV)-&>m_hWnd);
::ShowWindow(hParent, SW_HIDE);

上面就是打開一個名為view的OpenCV窗口,並將其放置到一個ID為IDC_STATIC_CV的PictureControl裡面,這個控制項的類型需要是rectangle。當你在需要顯示某圖片的時候,就像在OpenCV裡面顯示一樣就可以了。

imshow("view", mat);


Transformation of OpenCV image to MFC image in MSVC project

@jie wu 的方法更優雅,推薦!


OpenCV之前接觸過,但不是很熟悉。

我覺得最簡單的方法是,可以用Windows API函數StretchDIBits直接在控制項上畫就行了。因為用OpenCV載入圖像後,IplImage這個結構體裡面會包含圖像的信息以及指向圖像緩衝區的指針,StretchDIBits是可以指針指向的圖像緩衝區畫到DC上的。


一直以來,我使用的方法都是shiqiyu在opencvchina上面提供的引入directshow,並且採用cvvimage和cameraDs的方法。這個方法雖然在xp/win7/win8下面都能夠成果使用,但是一直以來我都沒有動機去深入看一看這個方法。這次在知乎上面看到 jie wu 提出的「將Opencv窗口添加到PictureControl」中的方法,感到思路很好,進行了具體實現

http://pan.baidu.com/s/1nuixdhR

具體可以看代碼,我帖一些主要代碼

void CMfcRibbonTemplateView::OnInitialUpdate()

{

CFormView::OnInitialUpdate();

GetParentFrame()-&>RecalcLayout();

ResizeParentToFit();

//根據控制項的大小設置初始幀的大小

CRect rect;

GetDlgItem(IDC_PBSRC) -&>GetClientRect( rect ); // 獲取控制項尺寸位置

m_lframe = Mat::zeros(rect.Height(),rect.Width(),CV_8UC3);

GetDlgItem(IDC_PBSRC) -&>GetClientRect( rect );

m_rframe = Mat::zeros(rect.Height(),rect.Width(),CV_8UC3);

//綁定Mat到Picturebox上去

namedWindow("src",WINDOW_AUTOSIZE);

HWND hWnd = (HWND)cvGetWindowHandle("src");

HWND hParnt = ::GetParent(hWnd);

::SetParent(hWnd,GetDlgItem(IDC_PBSRC)-&>m_hWnd);

::ShowWindow(hParnt,SW_HIDE);

namedWindow("dst",WINDOW_AUTOSIZE);

hWnd = (HWND)cvGetWindowHandle("dst");

hParnt = ::GetParent(hWnd);

::SetParent(hWnd,GetDlgItem(IDC_PBDEST)-&>m_hWnd);

::ShowWindow(hParnt,SW_HIDE);

}

void CMfcRibbonTemplateView::OnSize(UINT nType, int cx, int cy)

{

CFormView::OnSize(nType, cx, cy);

CWnd* pwndsrc = GetDlgItem(IDC_PBSRC);

CWnd* pwnddst = GetDlgItem(IDC_PBDEST);

//計算出長寬,這裡的長寬是按照比例的,圖像居中顯示

int iblank = 15; //邊界空餘

int iwidth = cx/2-iblank*2;

int iheight =(int)(iwidth*0.75);

if (pwndsrc-&>GetSafeHwnd() pwnddst-&>GetSafeHwnd()){

pwndsrc-&>MoveWindow(iblank,(cy-iheight)*0.4,iwidth,iheight);

pwnddst-&>MoveWindow(cx/2+iblank,(cy-iheight)*0.4,iwidth,iheight);

}

}

void CMfcRibbonTemplateView::showimage(Mat src, UINT ID)

{

if (src.empty())

return;

CRect rect;

Mat dst = src.clone();

GetDlgItem(ID) -&>GetClientRect( rect ); // 獲取控制項尺寸位置

if (dst.channels() == 1)

cvtColor(dst, dst, CV_GRAY2BGR);

resize(dst,dst,Size(rect.Width(),rect.Height()));

imshow("src",dst);

}

總體感到jie wu 提出的方法,對於解決比較簡單的問題,的確是不錯的(我記得halcon生成能夠被csharp調用的代碼的時候,好像採用的就是類似的方法)。但是它本身存在以下問題:

1、在窗體初始化的時候會有一個黑框彈出來,應該是nameWindow的效果;

2、在沒有圖片的時候,會自動將Picturebox的背景繪製成為灰色,而且好像不好控制;

3、僅僅是imshow還不能完成全部的調用,比如控制項的大小可能還會變化,那麼就需要用showimage重新進行封裝。

如果加上這些7788的東西,那麼最後調用起來,也是比較複雜的。

但是jie wu的方法有一個天生的優點,就是可以調用high gui的callback機制,這個是非常強的東西,能夠省不少麻煩事情。

這裡只是我粗淺的認識,若有不對之處,歡迎批評指正。


轉一個自己四年前寫的,雖然描述很多,但使用會很簡單。

原文地址(論壇改版過,導致有些是亂碼)

http://www.opencv.org.cn/forum.php?mod=viewthreadtid=15224page=1#pid111336

自從開始學C++的OpenCV,就想把問題都用一個簡單的類來解決。原來使用VC6.0使用的是StretchDIBits這類函數來畫圖,一旦畫新的圖像或者顯示多張點陣圖,勢必遇到多次對環境的調色板重新載入。如今都已經VS2010,MFC下的圖像顯示功能有所加強。使用MFC下的CImage類(貌似VC2005開始支持)可以很輕鬆的和快速的在MFC下顯示圖像;(到底是不是很快速還需要更多的考證,但把已經存在的Mat矩陣lena圖像顯示出來,使用getTimeGet函數測得,貌似低於1ms)。由於網上太多雜亂的內容和很多不夠詳細的講解,讓我嘗試了一天,才把彩色和灰度圖像正確顯示到界面上。

我這裡使用C++的OpenCV,如果你使用c語言的OpenCV一樣可以實現出來。

1.讀入Mat矩陣(cvMat一樣),Mat img=imread("*.*");//cvLoadImage

確保轉換前矩陣中的數據都是uchar(0~255)類型(不是的話量化到此區間),這樣才能顯示。(初學者,包括我經常忘了此事)

2.根據矩陣大小創建(CImage::Create)新的的CImage類

CImage CI;

int w=img.cols;//寬

int h=img.rows;//高

int chinnels=img.channels();//通道數

CI.Destroy();//創建前,最好使用它,防止重複創建,程序崩潰

CI.Create(w,h,8*chinnels);

3.下來就是對CI進行賦值了,這裡是最核心的地方,分二類討論

(1)如果是1個通道的圖像(灰度圖像)

CImage中內置了調色板,我們要對他進行賦值:

RGBQUAD* ColorTable;

int MaxColors=256;

//這裡可以通過CI.GetMaxColorTableEntries()得到大小(如果你是CI.Load讀入圖像的話)

ColorTable = new RGBQUAD[MaxColors];

CI.GetColorTable(0,MaxColors,ColorTable);//這裡是取得指針

for (int i=0; i {

ColorTable.rgbBlue = (BYTE)i;

//BYTE和uchar一回事,但MFC中都用它

ColorTable.rgbGreen = (BYTE)i;

ColorTable.rgbRed = (BYTE)i;

}

CI.SetColorTable(0,MaxColors,ColorTable);

delete []ColorTable;

然後就是數據拷貝了(這裡的矩陣表示方法,根據需要(cvMat or Mat)修改):

if(chinnels==1)

{//灰度圖像

uchar *pS;

uchar *pImg=(uchar *)CI.GetBits();

int step=CI.GetPitch();

for(int i=0;i {

pS=img.ptr(i);

for(int j=0;j {

*(pImg+i*step+j)=pS[j];

}

}

}

(2)如果是3個通道(彩色圖像)

沒有調色板,直接賦值

if(chinnels==3)

{//彩色圖像

uchar *pS;

uchar *pImg=(uchar *)CI.GetBits();//得到CImage數據區地址

int step=CI.GetPitch();

//這個是一行像素站的存儲空間w*3,並且結果是4的倍數(這個不用關注,到底是不是4的倍數有待考證)

for(int i=0;i {

pS=img.ptr(i);

for(int j=0;j {

for(int k=0;k *(pImg+i*step+j*3+k)=pS[j*3+k];

//注意到這裡的step不用乘以3

}

}

}

4.至此已經構建好CImage,下來就是顯示它。我們可以直接在對話框、單文檔等地方顯示他,還可以使用CPictureCtrl空間顯示他。下面給出幾個顯示方法:

//顯示前,這裡有個問題,等會討論

(1)放在一個按鈕響應或者函數中

//這裡的m_Pic是一個CPictureCtrl的control,其他控制項等也一樣

//CStatic m_Pic;

//DDX_Control(pDX, IDC_STATIC_Img, m_Pic);

CWnd * pCWnd = CWnd::FromHandle(m_Pic.GetSafeHwnd());

//通過變數得到dc比較複雜,但很好用

CPaintDC dc(pCWnd);//如果這個不能使用就換成CClientDC 。。。。

Invalidate(false);

SetStretchBltMode(dc.m_hDC,COLORONCOLOR);

//這個需要百度看看為什麼這樣設置

CI.StretchBlt(dc.m_hDC,rect,SRCCOPY);

//這裡顯示大小rect(CRect類型)也由自己定義,這個函數有許多重載函數

//圖像顯示的大小和效果,在你能顯示出來後,可以慢慢考慮

這裡的控制項的dc還可以由下面方式取得

CPaintDC dc(GetDlgItem(IDC_STATIC_Img));//IDC_STATIC_Img是空間的ID

(2)直接顯示(下面就寫得簡單點,少的部分自己加)

CDC *pDC=GetDC();

Invalidate(false);

CI.StretchBlt(pDC-&>m_hDC,rect,SRCCOPY);

或者

CPaintDC dc(this);

CI.Draw(dc.m_hDC,0,0);//這個以某個dc(可以是窗口)的(0,0)為起點

5.問題

前面提到一個問題,現在討論下,就是使用StretchBlt可以對圖像進行拉伸顯示。一般的圖像大小和你顯示的控制項或者區域大小是不一樣的,這時使用它顯示可以拉伸它到合適的大小。(注意選擇合適參數)。但我們還可以直接使用opencv自帶的函數對圖像進行resize,可以同樣達到類似的效果。目前我嘗試的結果的是使用opencv經過形變後的顯示效果比MFC下的StretchBlt拉伸好看,也不知道這二種的效率如何。

6.小結

操作數據和演算法實現時都是用opencv來實現,當顯示時,構造CImage顯示到windows系統的窗口上。不得不說,MFC,特別是高版本的MFC,學習起來相當困難。一方面,它本身做的十分複雜,需要很長時間熟悉它才能按照你的想法實現想要的東西。另一方面,缺乏好書來指導,比如說CImage類的用法,我是通過多種途徑和以前的積累才實現了最基礎的使用方法。雖然最後給出的代碼不多,但讓代碼不出錯,弄明白它的簡單機制卻花費很長時間。到底CImage類+CPictureCtrl是否適合用來顯示圖像,特別是對實時圖像處理的顯示,還需要進一步考證。本人對MFC了解有限,都是邊寫程序邊學,有些機制也不是很明白。。歡迎大家討論和完善。


首先感謝 @Vinjn張靜的回答.

有一個 匿名用戶 給出的代碼也比較簡潔.

除了直接使用opencv的對話框句柄之外.

其它方法的原理都是把Mat的data加個MFC的數據頭,然後顯示出來.

但是如果MFC的控制項框比Mat圖片小的時候,顯示會出錯.圖片會錯開.

這點已經有人注意到了,並給出了解決方案, @小明

採用的方法是手動4位元組對齊.不過代碼格式沒排版,比較亂.

在網上搜了一下,大家為了解決這個圖片錯位的問題一般手動4位元組對齊.

/********我給出的改進方案*********/

cvtColor(matImg,matImg,CV_BGR2BGRA); // matImg是你要顯示的Mat圖片

下面是詳細解釋,看懂了上面的代碼就不用往下看了:

CV_BGR2RGBA,也就是把BGR轉成RGBA格式,如此一來1個像素就是4個位元組了.

為啥一定要4個位元組呢?

Windows中顯示圖像存在一個4位元組對齊的問題,也就是每一行的位元組數必須是4的倍數.

而Mat的數據是連續存儲的.一般Mat的數據格式為BGR,也就是一個像素3個位元組,假設我的圖片一行有5個像素,那一行就是15個位元組,這不符合MFC的數據對齊方式,

如果我們直接把Mat的data加個數據頭再顯示出來就可能會出錯.

手動4位元組對齊,就是計算每行的位元組是不是4的倍數,不是的話,在後面補0

但是我們把圖片轉成RGBA之後,一個像素就是4個位元組,不管你一行幾個像素,一直都是對齊的.

支持輸入 單通道灰度 / 3通道BGR / 4通道BGRA.

如果想傳入RGB,把case 3那裡改一下,改成CV_RGB2BGRA.

其實也沒必要寫在函數裡面,只要在調用該函數之前自己先用cvtColor轉成BGRA再輸入就可以了.

關於一個像素多少位元組的計算那裡可以刪掉,直接寫 8*通道數 就行了.

當時是因為有種奇葩的格式是BGR555和BGR565,一個像素兩個位元組.

img.channel()獲得的屬性竟然是2個通道, WTF??.

原本就是為了支持傳入這玩意,不過轉換這事還是交給程序員比較好.

CV_BGR5552BGRA , CV_BGR5652BGRA

// 要顯示的圖 控制項的ID
void CXXXDlg::DrawMat(cv::Mat img, UINT nID)
{
cv::Mat imgTmp;
CRect rect;
GetDlgItem(nID)-&>GetClientRect(rect); // 獲取控制項大小
cv::resize(img, imgTmp, cv::Size(rect.Width(), rect.Height()));// 縮放Mat並備份
// 轉一下格式 ,這段可以放外面,
switch (imgTmp.channels())
{
case 1:
cv::cvtColor(imgTmp, imgTmp, CV_GRAY2BGRA); // GRAY單通道
break;
case 3:
cv::cvtColor(imgTmp, imgTmp, CV_BGR2BGRA); // BGR三通道
break;
default:
break;
}
int pixelBytes = imgTmp.channels()*(imgTmp.depth() + 1); // 計算一個像素多少個位元組
// 製作bitmapinfo(數據頭)
BITMAPINFO bitInfo;
bitInfo.bmiHeader.biBitCount = 8 * pixelBytes;
bitInfo.bmiHeader.biWidth = imgTmp.cols;
bitInfo.bmiHeader.biHeight = -imgTmp.rows;
bitInfo.bmiHeader.biPlanes = 1;
bitInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitInfo.bmiHeader.biCompression = BI_RGB;
bitInfo.bmiHeader.biClrImportant = 0;
bitInfo.bmiHeader.biClrUsed = 0;
bitInfo.bmiHeader.biSizeImage = 0;
bitInfo.bmiHeader.biXPelsPerMeter = 0;
bitInfo.bmiHeader.biYPelsPerMeter = 0;
// Mat.data + bitmap數據頭 -&> MFC
CDC *pDC = GetDlgItem(nID)-&>GetDC();
::StretchDIBits(
pDC-&>GetSafeHdc(),
0, 0, rect.Width(), rect.Height(),
0, 0, rect.Width(), rect.Height(),
imgTmp.data,
bitInfo,
DIB_RGB_COLORS,
SRCCOPY
);
ReleaseDC(pDC);
}

我突然想到 resize那裡會複製一份data嗎? 我不希望修改原始數據,明天測.睡覺.

更新,沒時間,自測吧。


void CxxxDlg::DrawcvMat(cv::Mat m_cvImg, UINT ID)//顯示mat在picture控制項中
{
cv::Mat img;
CRect rect;
GetDlgItem(ID)-&>GetClientRect(rect);
cv::Rect dst(rect.left,rect.top,rect.right,rect.bottom);
cv::resize(m_cvImg,img,cv::Size(rect.Width(),rect.Height()));

unsigned int m_buffer[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD)*256];
BITMAPINFO* m_bmi = (BITMAPINFO*) m_buffer;
BITMAPINFOHEADER* m_bmih = (m_bmi-&>bmiHeader);
memset(m_bmih, 0, sizeof(*m_bmih));
m_bmih-&>biSize = sizeof(BITMAPINFOHEADER);
m_bmih-&>biWidth = img.cols;
m_bmih-&>biHeight = -img.rows; // 在自下而上的點陣圖中 高度為負
m_bmih-&>biPlanes = 1;
m_bmih-&>biCompression = BI_RGB;
m_bmih-&>biBitCount = 8 * img.channels();

CDC *pDC = GetDlgItem(ID)-&>GetDC();
::StretchDIBits(
pDC-&>GetSafeHdc(),
0, 0, rect.Width(), rect.Height(),
0, 0, rect.Width(), rect.Height(),
img.data,
(BITMAPINFO*) m_bmi,
DIB_RGB_COLORS,
SRCCOPY
);
ReleaseDC(pDC);
}

最近在寫 MFC(初學),善用搜索引擎


這個問題最後怎麼解決的?最後源碼是?


推薦閱讀:

關於opencv中對齊圖片的問題?
做增強現實AR,高通sdk與opencv有什麼區別。各有什麼利弊?
有什麼好的機器視覺相關網站推薦呢?
opencv Mat類型的轉換問題?
在圖像處理中用Mat比IplImage 除了不用自己管理內存,Mat有其他的優勢沒?請熟悉OpenCV和圖像處理的大牛指點

TAG:OpenCV |