将OpenCV输出的图像嵌入显示到子窗口控件中(支持Windows、Linux)

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

 

你可能感兴趣的:(OpenCV)