利用MFC和Opencv读取摄像头

1 前言

本人小白,想利用MFC实现摄像头的开关、截图、录像、手动亮度和对比对调整以及控件自适应变化等基本功能,程序基本上都是参考网上的教程和博客,但中间遇到过摄像头打不开、opencv造成内存泄露、摄像头运行一段时间后会自动关闭等各种小问题,所以想借此记录下自己调试的过程,也希望能有所帮助。

本文主要是利用MFC中的OnTimer函数定时截取摄像头所获图像,再对图像进行亮度和对比度调整,然后利用Opencv里CvvImage类中的DrawToHDC函数将所获图像投射到MFC的Picture控件上,以此实现摄像头的读取。

2 版本

开发环境:VS2015 社区版

Opencv: 3.4.16

3 Opencv下载

直接百度搜索Opencv进入官网,点击Library即可选择Opencv版本进行下载

这里建议下载3.4.16以下版本。好像因为CvvImage类中使用的IplImage格式的图像比较落后,高版本中都被淘汰了,导致后续导入CvvImage类出现部分函数cvLoadImage等函数无法使用等情况。

此外CvvImage类在2.和3.版本中已经找不到了,所以需要自己导入。好像1.版本中有,但本人未进行尝试。所以本人下载Opencv3.4.16 Windows版本

后续直接按照相应安装程序安装即可

4 创建MFC项目

4.1 新建MFC空项目

选择基于对话框模式

 根据自己需求选择最大化、最小化等功能,点击完成即可。后续可以在属性栏里修改。

生成后即可出现如图界面,相应的源文件和头文件文件可以在右侧解决方案资源管理器中进行查看,如图所示的控件编辑界面可以在资源视图中查看。

4.2 Opencv配置

在项目栏中最下方的属性页中进行配置。

 VC++目录-->包含目录-->编辑,添加如图所示两个路径

  VC++目录-->库目录-->编辑,添加如图所示路径

 在链接器-->输入--->附加依赖项中添加opencv_world3416d.lib。这里3416是版本,d是Debug模式,根据自己需求进行添加。

该库的位置在前述下载的Opencv中,路径如下如所示。Opencv中只有x64,如需在x86其他模式下运行需要另外上网进行配置;vc14是VS2015,如果是VS2017则选择vc15。

 其他版本的VS参考其他网上其他教程。比如VS2013

(65条消息) 如何利用MFC及Opencv读入摄像头并显示画面_小菜鸟IT修行路-CSDN博客_mfc opencv 摄像头icon-default.png?t=M0H8https://blog.csdn.net/qq_32171677/article/details/61190719

4.3 导入CvvImage类

这个网上资源较多,直接搜索即可,应该不会出错,比如Opencv2.2版本以上CvvImage类的使用 - 我自逍遥笑 - 博客园 (cnblogs.com)icon-default.png?t=M0H8https://www.cnblogs.com/zwh0214/p/6061880.html

(65条消息) CvvImage类_学习、思考、总结-CSDN博客_cvvimageicon-default.png?t=M0H8https://blog.csdn.net/szlcw1/article/details/21296523以防万一,我把自己的也上传了

(65条消息) CvvImage类_m0_48322633的博客-CSDN博客icon-default.png?t=M0H8https://blog.csdn.net/m0_48322633/article/details/122618120在相应位置添加相应源文件和头文件即可

最后解决方案应该如下图所示。 

并最后在OpenCarmerDlg.cpp(自己创建的MFC项目)中加上头文件

#include <opencv2\opencv.hpp>
#include "CvvImage.h"

添加上相应命名空间。我这应该是小白做法。

using namespace cv;
using namespace std;

5 编辑MFC项目

5.1 添加控件

在右侧工具箱选项中拖动添加Botton、Edit Control、Picture Control和Slider Control 控件,其中相应命名和形状格式可以在相应控件中自行修改。

5.2 添加变量和自定义函数

 打开类向导

 在消息该栏中添加WM_TIMER和WM_SIZE事件。前者是定时器,用来定时截取摄像头图像和实时将调节后的图像投射到Picture控件上;后者是用于调节各控件在缩放中的自适应变化。

 在成员函数中给Slider control控件中添加相关变量。一个用于调节亮度,一个用于调节对比度。讲道理,在控件较多时应该要修改各控件的ID,以便于区分和调试,有时候在修改控件ID后,在程序中不能及时识别,可以关掉程序重新打开即可。

在OpenCarmerDlg.h中添加相应自定义函数。在private还是public中添加应该都是可以的,这里需要在该头文件中添加#include <opencv2\opencv.hpp>

afx_msg void ChangeSize(UINT nID, int x, int y);
afx_msg void MatToHDC(cv::Mat mat, UINT nType);
inline cv::Mat Adjust_conAndBri(const cv::Mat Img_src, float ContrastValue, float BrightVlue);

 在OpenCarmerDlg.cpp中添加全局变量。部分变量是在开始调试过程中添加,后续并没有使用,可以根据情况自行删除。其中亮度和对比度的最大最小设置不一样是为了实现不同精度下的调控;CDC、HDC、CWnd是用来获取相应控件的权柄;OriPos是用来存储各控件初始化状态的位置信息,20可以自行根据控件数量进行修改。


#define MaxBrightValue 255
#define MinBrightValue -255

#define MaxContrastValue 1000
#define MinContrastValue 0

CRect m_rect;//存储整个框架的位置信息
IplImage* Image_temp;
CRect rect;//存储临时框架位置信息
CDC *pDC;
HDC hDC;
CWnd *pwnd;
int OriPos[20][5];
Mat g_srcImage;//实时读取帧图像
Mat g_dstImage;//实时调节图像亮度和饱和度
Mat Img_capture;//用于存储所借截图像
Mat Img_close;//用于存储关闭摄像头时最后帧图像
VideoCapture capture;//打开摄像头
//CvVideoWriter *writer = 0;
VideoWriter writer;
bool RecordVideo(FALSE);
//参考https://blog.csdn.net/qq_32171677/article/details/61190719
//实现摄像头的开关,并通过一帧帧图像截取显示到控件矩形框中,并实现所有控件随窗口的自适应变化

float BrightValue;
float ContrastValue;

5.3 功能实现

在OnInitDialog()添加初始化代码。主要实现遍历各控件记录其上下左右边界的初始化位置信息,为后续自适应缩放控件大小做准备;设置亮度初始位置为0,对比度初始化位置为1(后续使用该对比值ContrastValue的0.01倍作为实际调节对比度)。

	
	GetClientRect(&m_rect);//获取整个窗口的位置信息

	HWND hwndchild = ::GetWindow(m_hWnd, GW_CHILD);
	CRect rect_original;
	int k = 0;
	int ID;
	while (hwndchild) {//将各控件的初始矩形框位置及ID储存至数组中,避免多次变换导致位置变化产生误差(long int在多次非整除中产生误差)
		if (k >= 20) {
			MessageBox(L"控件初始化遍历出错!");
			return FALSE;
		}
		ID = ::GetDlgCtrlID(hwndchild);
		GetDlgItem(ID)->GetWindowRect(rect_original);
		ScreenToClient(rect_original);
		OriPos[k][0] = ID;
		OriPos[k][1] = rect_original.left;
		OriPos[k][2] = rect_original.right;
		OriPos[k][3] = rect_original.top;
		OriPos[k][4] = rect_original.bottom;
		++k;
		hwndchild = ::GetWindow(hwndchild, GW_HWNDNEXT);
	}
	
	Slider_bright.SetRange(MinBrightValue,MaxBrightValue);
	Slider_bright.SetTicFreq(1);
	Slider_bright.SetPos(0);

	Slider_Contrast.SetRange(MinContrastValue, MaxContrastValue);
	Slider_Contrast.SetTicFreq(1);
	Slider_Contrast.SetPos(100);

打开摄像头

void COpenCarmerDlg::On_OpenCarmer()
{
	// TODO: Add your command handler code here
	if (capture.isOpened()) {
		MessageBox(L"摄像头已打开");
		return;
	}
	capture.open(0);
	SetTimer(1, 25, NULL); //定时器,1为定时器编号,25为间隔时间,ms(最好设置成摄像头帧率)
}

定时器。将摄像头的截取图像传入g_srcImage,在经过亮度和对比度调节映射到IDC_STATIC的Picture Control控件中。

void COpenCarmerDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	capture.read(g_srcImage);//读取每一帧图像
	g_dstImage = Adjust_conAndBri(g_srcImage, ContrastValue*0.01, BrightValue);
	if (RecordVideo) {
		writer << g_dstImage;
	}
	MatToHDC(g_dstImage, IDC_STATIC);//将每一帧图像传至控件中
	CDialogEx::OnTimer(nIDEvent);
}

关闭摄像头。关闭摄像头会将最后一帧图像储存到Img_close中,此时Picture中会显示最后一帧的图像,当此时缩放界面时,控件会刷新,此时在后续程序中会再次利用MatToHDC函数将最后帧图像再次显示到控件上。

void COpenCarmerDlg::On_CloseCarmer()
{
	// TODO: 在此添加控件通知处理程序代码
	
	if (!capture.isOpened()) {
		MessageBox(L"请打开摄像头!!");
		return;
	}
	Img_close = g_dstImage.clone();//读取关闭摄像头时的最后帧图像
	capture.release();//关闭摄像头
	KillTimer(1);//关闭计时器
}

图像捕捉。

void COpenCarmerDlg::OnBnClickedButton3()//图像捕捉
{
	// TODO: 在此添加控件通知处理程序代码
	if (!capture.isOpened()) {
		MessageBox(L"请打开摄像头!!否则无法捕捉");
		return;
	}
	Img_capture = g_dstImage.clone();//将读取帧存入全局Mat中
	MatToHDC(Img_capture, IDC_STATIC1);
}

实现控件自适应缩放。每次界面缩放都会调用该函数,通过遍历控件获取相应ID,调用并传给ChangeSize函数实现自适应缩放。

void COpenCarmerDlg::OnSize(UINT nType, int cx, int cy)
{
	CDialogEx::OnSize(nType, cx, cy);

	// TODO: 在此处添加消息处理程序代码
	if (nType != SIZE_MINIMIZED) {//排除最小化
		int ID_temp;
		HWND hwndChild = ::GetWindow(m_hWnd, GW_CHILD);//遍历所有控件
		while (hwndChild) {
			ID_temp = ::GetDlgCtrlID(hwndChild);//获取相应控件ID
			ChangeSize(ID_temp, cx, cy);//修改控件位置和大小
			hwndChild = ::GetWindow(hwndChild, GW_HWNDNEXT);//查找下一个控件
		}
	}
}

定义ChangeSize函数。将控件现位置信息与最初位置信息进行比较(部分参考文章并不是与最初位置进行比较,而是与上次变化位置进行比较,由于位置信息只能存储为int型,在进行乘除时会导致精度丢失,导致多次缩放后出现错误),后续判断是对不同情况下的讨论,目的是为了保证截取图像能随控件大小变化而自动变化,即使在摄像头关闭的状态下,仍保证最后帧图像和截取图像仍能显示在控件上。

void COpenCarmerDlg::ChangeSize(UINT nID, int x, int y) 
{
	CWnd* pWnd1;
	pWnd1 = GetDlgItem(nID);
	//CString msg;//用于调试,在必要位置弹出信息框
	//msg.Format(L"%d", nID);
	//MessageBox(msg);
	if (pWnd1 != NULL) {
		CRect rect_temp;
		pWnd1->GetWindowRect(&rect_temp);
		ScreenToClient(&rect_temp);
		int k = 0;
		for (int i = 0; OriPos[i][0] != nID; ++i) {
			if (i >= 20) {
				MessageBox(L"控件遍历出错");
				return;
			}
			++k;
		}
        rect_temp.left  =  (OriPos[k][1] * x) / m_rect.Width();//控件自适应变化
		rect_temp.right =  (OriPos[k][2] * x) / m_rect.Width();
		rect_temp.top   =  (OriPos[k][3] * y) / m_rect.Height();
		rect_temp.bottom = (OriPos[k][4] * y) / m_rect.Height();
		pWnd1->MoveWindow(rect_temp); //伸缩控件
		
		if (Img_close.empty() && nID == IDC_STATIC && !capture.isOpened()) {//初始化时自适应,刷新主显示框,避免主显示框出现重叠
			InvalidateRect(&rect_temp, TRUE);
		}
		if (!Img_close.empty()  && !capture.isOpened()) {//摄像头关闭时保持最后帧能够自适应变化
			MatToHDC(Img_close, IDC_STATIC);
		}
		if (Img_capture.empty() && nID == IDC_STATIC1) {//未截图时刷截图框,避免截图框出现重叠
			InvalidateRect(&rect_temp, TRUE);
		}
		if (!Img_capture.empty()) {//保持捕获帧能够随截图框大小自适应变化
			MatToHDC(Img_capture, IDC_STATIC1);
		}
	}
}

自定义MatToHDC函数。将图像映射到控件上,这是需要利用CvvImage类中的DrawToHDC函数,而该类只能用于IplImage类图像,所以将Mat类型进行转化;而之所以采用Mat类型是因为这种类型的图像是Opencv更加推崇的,而且VideoCapture所借图像也是这种类型,而且相关可参考的文章较多。至于为什么没用cvCapture,我也忘了,好像是打不开摄像头之类的,这里cvCapture参考的是(65条消息) 如何利用MFC及Opencv读入摄像头并显示画面_小菜鸟IT修行路-CSDN博客_mfc opencv 摄像头icon-default.png?t=M0H8https://blog.csdn.net/qq_32171677/article/details/61190719

inline void COpenCarmerDlg::MatToHDC(Mat mat, UINT nType)
{
	CvvImage m_CvvImage;

	Image_temp = &cvIplImage(mat);//将mat类型转变为lplImage类
	//frame = &cvIplImage(g_srcImage);//常用的&IplImage(const Mat img)不能使用
    //如要使用参考https://blog.csdn.net/qq_43348528/article/details/104051519

	pDC = GetDlgItem(nType)->GetDC();//GetDlgItem(IDC_PIC_STATIC)意思为获取显示控件的句柄(句柄就是指针),获取显示控件的DC
	GetDlgItem(nType)->GetClientRect(&rect);//获取控件的矩形图形框的位置
	hDC = pDC->GetSafeHdc();//获取显示控件的句柄

	m_CvvImage.CopyOf(Image_temp, 1); //复制该帧图像   
	m_CvvImage.DrawToHDC(hDC, &rect); //显示到设备的矩形框内

	m_CvvImage.Destroy();//销毁copyof所产生的新空间
	m_CvvImage.~CvvImage();//注销CvvImage类
	ReleaseDC(pDC);
}

图像调节函数。其调节原理可以直接百度,比较简单。我这里就是矩阵的运算。

(66条消息) Opencv改变图像亮度和对比度以及优化_一个想要改变世界的IT slave的专栏-CSDN博客_opencv 亮度变化icon-default.png?t=M0H8https://blog.csdn.net/u013139259/article/details/52145377若要实现自动调节,可以参考以下文章

OpenCV图像增强算法实现(直方图均衡化、拉普拉斯、Log、Gamma) - mydddfly - 博客园 (cnblogs.com)icon-default.png?t=M0H8https://www.cnblogs.com/jukan/p/7815722.html(66条消息) 【图像增强】自适应亮度对比度调节算法C++_Anida_qin的博客-CSDN博客_自适应亮度算法icon-default.png?t=M0H8https://blog.csdn.net/qq_20095389/article/details/83658878

cv::Mat Adjust_conAndBri(const cv::Mat Img_src, float ContrastValue, float BrightVlue)
{
	Mat Img_blank = Mat::zeros(Img_src.size(), Img_src.type());
	Mat Img_adjust(Img_src.size(), Img_src.type(), Scalar::all(1));
	//Mat	Img_adjust = Mat::zeros(Img_src.size(), Img_src.type());
	cv::addWeighted(Img_src, ContrastValue, Img_adjust, BrightVlue, 0, Img_adjust);
	return Img_adjust;
}

亮度调节和显示。

void COpenCarmerDlg::OnNMCustomdrawSlider1(NMHDR *pNMHDR, LRESULT *pResult)
{
	LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR);
	// TODO: 在此添加控件通知处理程序代码
	*pResult = 0;
	CString msg;
	BrightValue = Slider_bright.GetPos();
	msg.Format(L"%.2f", BrightValue);
	GetDlgItem(IDC_EDITBRIGHT)->SetWindowText(msg);
}

对比度调节和显示。

void COpenCarmerDlg::OnNMCustomdrawSlider2(NMHDR *pNMHDR, LRESULT *pResult)
{
	LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR);
	// TODO: 在此添加控件通知处理程序代码
	*pResult = 0;
	CString msg;
	ContrastValue = Slider_Contrast.GetPos();
	msg.Format(L"%.2f", ContrastValue/100);
	GetDlgItem(IDC_EDIT2)->SetWindowText(msg);
}

图像保存和视频保存功能相关的网上文章质量参差不齐,后续整理好后再发。

  • 4
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值