OpenCV用来做视频处理很方便,能用窗口显示处理后的图像,但是它默认显示图像的窗口是弹出式的,而我们很多情况下需要将图像显示到自己软件的窗口控件中。这应该怎么做呢?上网搜过一些方案,也试了一下,最后自己优化了一下,把其中几种比较靠谱的方法分享给大家。
第一种,使用Cvvimage类 + GDI方式显示图像。
CvvImage类有个函数DrawPicToHDC (IplImage *img, UINT ID),可以把OpenCV输出的类型为IplImage的图像内容显示到指定ID控件中,其实原理很简单,CVVImage类内部会创建一个RGB图像,当调用DrawPicToHDC的时候会将输入的img指针指向的图像的数据拷贝到CVVImage内部创建的图像缓存中,并且采用GDI的函数(SetDIBitsToDevice、StretchBlt)显示图像到目标控件的DC中,DrawPicToHDC实现函数如下:
void CImageDetectionDlg::DrawPicToHDC(IplImage *img, UINT ID)
{
CDC *pDC = (CDC *)GetDlgItem(ID)->GetDC();
HDC hDC = pDC->GetSafeHdc();
CRect rect;
GetDlgItem(ID)->GetClientRect(&rect);
m_cimg.CopyOf(img, img->nChannels);
m_cimg.DrawToHDC(hDC, &rect);
ReleaseDC(pDC);
}
但是,现在OpenCV3.0以上版本已经丢弃了这个类,需要开发者自己去添加。怎么去调用上面这个函数呢?给个例子:
//用GID方法显示
frmImg = &IplImage(frame);
DrawPicToHDC(frmImg, IDC_VIDEO_STATIC);
其中frame是一个CMat变量,而frmImg是iplImage指针类型,上面第一句代码的作用是将: Mat -》IplImage ,然后第二句代码是将frmImg指向的IplImage的图像拷贝到另外一个IplImage图像中,最后将图像显示到窗口的HDC。
第二种,使用SDL。因为SDL库是跨平台的,理论上支持Linux、Windows等多种系统。我用的是Windows上编译好的SDL版本。注意旧的SDL库是不支持内嵌窗口到程序中,请下载较新的版本。用SDL显示图像到控件窗口中只需要调用几个API,简单可以归纳为以下几个步骤:
1. 定义几个变量。
SDL_Window* m_pScreen;
SDL_Renderer* m_pRenderer;
SDL_Texture* m_pbmp;
2. 根据传入的窗口句柄和原图分辨率初始化SDL的窗口和渲染者对象,注意:这个函数创建的纹理(Texture)是YV12格式的,如果采集的图像是RGB,需要在外部将RGB转成YV12再传图像进来。
bool CSdl::initialize(HWND presentWnd, int nSrcWidth, int nSrcHeight)
{
m_PresentWnd = presentWnd;
m_nSrcWidth = nSrcWidth;
m_nSrcHeight = nSrcHeight;
// Allocate a place to put our YUV image on that screen
m_pScreen = SDL_CreateWindowFrom(presentWnd);
m_pRenderer = SDL_CreateRenderer(m_pScreen, -1, SDL_RENDERER_ACCELERATED);
m_pbmp = SDL_CreateTexture(m_pRenderer,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
nSrcWidth,
nSrcHeight);
return (m_pRenderer && m_pbmp) ? true : false;
}
3. 渲染图像。传入的是Y,U,V三个平面的首地址以及每个分量的Linesize(即Pitch大小)或者叫行字节宽度。
void CSdl::Draw(uint8_t *data[NUM_DATA_POINTERS], int linesize[NUM_DATA_POINTERS])
{
SDL_Rect rect = { 0 };
SDL_Rect rectDst = { 0 };
CGRect rcClient;
GetClientGRect(m_PresentWnd, rcClient);
rectDst.x = 0;
rectDst.y = 0;
rectDst.w = rcClient.Width();
rectDst.h = rcClient.Height();
if (m_pbmp)
{
rect.x = 0;
rect.y = 0;
rect.w = m_nSrcWidth;
rect.h = m_nSrcHeight;
SDL_UpdateYUVTexture(m_pbmp, &rect,
data[0], linesize[0],
data[1], linesize[1],
data[2], linesize[2]);
SDL_RenderClear(m_pRenderer);
int nRet = SDL_RenderCopy(m_pRenderer, m_pbmp, &rect, &rectDst);
if(nRet != 0)
{
TRACE("SDL_RenderCopy return: %d \n", nRet);
m_nErrorCount++;
if(m_nErrorCount < 3)
{
SDL_DestroyRenderer(m_pRenderer);
SDL_DestroyTexture(m_pbmp);/*释放内存*/
m_pRenderer = SDL_CreateRenderer(m_pScreen, -1, SDL_RENDERER_ACCELERATED);
m_pbmp = SDL_CreateTexture(m_pRenderer,
SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
m_nSrcWidth,
m_nSrcHeight);
return;
}
}
else
{
m_nErrorCount = 0;
}
SDL_RenderPresent(m_pRenderer);
}
}
4. 渲染完毕,销毁对象。
void CSdl::release()
{
SDL_RenderClear(m_pRenderer);
SDL_DestroyRenderer(m_pRenderer);
SDL_DestroyWindow(m_pScreen);
SDL_DestroyTexture(m_pbmp);/*释放内存*/
if(m_PresentWnd)
{
::ShowWindow(m_PresentWnd, SW_SHOWNORMAL);
}
}
上面为什么销毁对象后还要调用::ShowWindow(m_PresentWnd, SW_SHOWNORMAL)把视频窗口显示出来?默认视频窗口控件不是显示的吗?其实这是因为SDL的一个卑劣行为,它的对象被销毁后会隐藏掉原来的显示图像的窗口(为什么这样,我也搞不明白),反正需要调用ShowWindow把窗口还原出来。
上面只是说了这个SDL渲染类的几个成员函数的实现,但具体怎么调用它们去显示图像呢?下面给个例子:
//将该函数放到一个线程里调用
int CImageDetectionDlg::PlayVideoFile(string video_path)
{
Mat frame;
Mat result;
Mat dstImage, yuvImage;
IplImage *frmImg;
VideoCapture capture(video_path);
if (!capture.isOpened())
{
OutputDebugString(_T("OpenFile Failed. \n"));
return -1;
}
int w = capture.get(CV_CAP_PROP_FRAME_WIDTH);
int h = capture.get(CV_CAP_PROP_FRAME_HEIGHT);
if (m_pSDLWin != NULL)
{
delete m_pSDLWin;
m_pSDLWin = NULL;
}
if (m_pSDLWin == NULL)
{
m_pSDLWin = new CSdl();
m_pSDLWin->initialize(m_VideoWin.GetSafeHwnd(), w, h);
}
uint8_t *data[NUM_DATA_POINTERS] = { 0 };
int linesize[NUM_DATA_POINTERS] = { 0 };
while(!m_bEndPlaying)
{
capture >> frame;
if (frame.empty())
break;
// 将图像由BGR转换为YUV420
cvtColor(frame, yuvImage, /*CV_BGR2YUV*/CV_BGR2YUV_I420);
data[0] = yuvImage.data;
data[1] = yuvImage.data + w*h;
data[2] = yuvImage.data + w*h + w*h/4;
data[3] = 0;
linesize[0] = yuvImage.step;
linesize[1] = yuvImage.step/2;
linesize[2] = yuvImage.step/2;
linesize[3] = 0;
m_pSDLWin->Draw(data, linesize);
Sleep(20);
}
if (m_pSDLWin)
{
m_pSDLWin->release();
delete m_pSDLWin;
m_pSDLWin = NULL;
}
return 0;
}
注意到上面函数调用了OpenCV的cvtColor函数将解码输出的图像(默认为BGR格式)转为YUV420的图像,这里要看清楚转换的图像格式类型,是CV_BGR2YUV_I420,而不是CV_BGR2YUV。
第三种方法,使用QT渲染图像。这种方法也支持跨平台,详细介绍请看这个博主的文章:https://blog.csdn.net/qingyang8513/article/details/80378491?utm_source=app
前面两种方法我都写了一个例子,请到这里下载:
https://download.csdn.net/download/zhoubotong2012/11985588