基于opencv和mfc的摄像头采集代码(GOMFCTemplate2)

Posted GreenOpen专注图像处理

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于opencv和mfc的摄像头采集代码(GOMFCTemplate2)相关的知识,希望对你有一定的参考价值。

 
      编写带界面的图像处理程序,选择opencv+mfc是一种很好的选择;在读取摄像头数据方面,网上的方法很多,其中shiqiyu的camerads的方法是较好的。
      基于现有资料,通过在实际项目中的积累,我总结出来一套结合opencv和mfc的摄像头采集框架。具有以下特点:
      1、基于directshow,兼容性好,速度快。到目前为止,无论是工业相机还是普通相机,没发现不兼容的;
      2、摄像头部分通过线程读取,保证界面的运行流畅;
      3、框架经过多次打磨,已经比较稳定,不会出现异常错误;代码简洁明了,方便复用。
 
代码解析 框架为对话框模式代码生成,加入CameraDS类和CvvImage类。
      
CameraDS是shiqiyu编写的,主要完成directshow的引入,提供了以下函数。能够获得目前相机总数,读取相机名称,打开相机以及获得当前帧的数据等
 
//打开摄像头,nCamID指定打开哪个摄像头,取值可以为0,1,2,...
//bDisplayProperties指示是否自动弹出摄像头属性页
//nWidth和nHeight设置的摄像头的宽和高,如果摄像头不支持所设定的宽度和高度,则返回false
boolCCameraDS::OpenCamera(int nCamID,bool bDisplayProperties=true,int nWidth=320,int nHeight=240);
//关闭摄像头,析构函数会自动调用这个函数
voidCloseCamera();
//返回摄像头的数目
//可以不用创建CCameraDS实例,采用int c=CCameraDS::CameraCount();得到结果。
staticintCameraCount();
//根据摄像头的编号返回摄像头的名字
//nCamID: 摄像头编号
//sName: 用于存放摄像头名字的数组
//nBufferSize: sName的大小
//可以不用创建CCameraDS实例,采用CCameraDS::CameraName();得到结果。
staticintCCameraDS::CameraName(int nCamID,char* sName,int nBufferSize);
//返回图像宽度
intGetWidth(){return m_nWidth;}
//返回图像高度
intGetHeight(){return m_nHeight;}
//抓取一帧,返回的IplImage不可手动释放!
//返回图像数据的为RGB模式的Top-down(第一个字节为左上角像素),即IplImage::origin=0(IPL_ORIGIN_TL)
IplImage*QueryFrame();
voidDisplayPinProperties(void);

 

CvvImage类是Opencv自己提供的,这里使用它的主要目的是讲mat对象画到mfc的控件中去
CvvImage cimg;
IplImage cpy = dst;
cimg.CopyOf(&cpy );// 复制图片
cimg.DrawToHDC( hDC,&rect );// 将图片绘制到显示控件的指定区域内
在GOMfcTemplate2Dlg中是主要代码,分为以下几个部分。这块的东西主要是我自己总结的。
1、摄像头显示循环,是单独的线程
//摄像头显示循环,所有关于采集的操作是通过主线程传递控制变量到采集线程,而后由采集线程完成的
DWORD WINAPI CaptureThread(LPVOID lpParameter)
{
    CGOMfcTemplate2Dlg* pDlg = (CGOMfcTemplate2Dlg*)lpParameter;
    double t_start = (double)cv::getTickCount(); //开始时间
    //#pragma omp parallel for
    while (true)
    {
        if (pDlg->b_closeCam)//退出循环
            break;
        double t  = ((double)cv::getTickCount() - t_start) / getTickFrequency();
        if (t <= 0.042)//fps =24,主动降低速度
        {
            Sleep(10);
            continue;
        }
        else
        {
            t_start = (double)cv::getTickCount();
        }
        //从directX中获得当前图像并显示出来
        IplImage* queryframe  = pDlg->cameraDs.QueryFrame();
        //在2.0版本中可以强转,在3.0中需要使用函数
        Mat camframe = cvarrToMat(queryframe);
        pDlg->showImage(camframe, IDC_CAM); //显示原始图像
        if (pDlg->b_takeApic)
        {
            CString strName;
            pDlg->GetDlgItemText(IDC_EDIT1, strName);    
            CT2CA pszName(strName);
            std::string m_NameStd(pszName);
 
            SYSTEMTIME sysTime;
            GetLocalTime(&sysTime); //得到系统时间
            
            char cbuf[255];
            sprintf(cbuf, "/%d-%d-%d-%d-%d-%d.jpg", sysTime.wYear,sysTime.wMonth,sysTime.wDay, sysTime.wHour, sysTime.wMinute, sysTime.wSecond);//"2010-09-21"
            m_NameStd = m_NameStd  + cbuf;
            imwrite(m_NameStd, camframe);
            pDlg->b_takeApic = false;
            CString cstr;
            cstr = cbuf;
            pDlg->m_statusBar.SetWindowText(_T("保存图片:")+ cstr);
        }
    }
    return 0;
}
这个线程函数,在创建的时候读取主Dlg的指针为参数,这样能够进行线程间通信。它主要完成两项工作,一个是通过camerads的QueryFrame函数读取当前的图像并传递给主线程;一个是判断b_closeCam和b_taleApic两个控制变量是否为true并进行相关操作。
目前的线程间通信采用的变量共享的方式,由于在摄像头线程中是写变量,在主线程中是读变量,一般不会冲突。但是如果摄像头很多或者实时性非常高,还是应该采用postmessage的方式通信。
线程部分,这里标红的是核心代码,它在采集结果不带来明显变化的基础上带来了这样的优化:
设备 去除标红 添加标红
Logitech(640X480)  35%、9.2  10.5% 9.2
HDCammer(1920X1080) 46%、44 23% 42.1
作为比对,我使用了Logitech官方自带的软件进行对比,它使用与HDCammer上是 19.6%/24.9MB,已经非常接近了,考虑到OpenCV自己带来的消耗,我认为可以接受的。
2、initdialog中,对界面控件进行初始化
m_nCamCount =CCameraDS::CameraCount();//摄像头总数
//获得摄像头数目
char camera_name[1024];
char istr[25];
for(int i=0; i < m_nCamCount; i++)
{
int retval =CCameraDS::CameraName(i, camera_name,sizeof(camera_name));
sprintf_s(istr," # %d", i);
strcat_s(camera_name,istr );
CString camstr = camera_name;
if(retval >0)
m_CBNCamList.AddString(camstr);
else
AfxMessageBox(_T("不能获取摄像头的名称"));
}
//初始化显示控件
CRect rect;
GetDlgItem(IDC_CAM)->GetClientRect(&rect);
m_mainframe =Mat::zeros(rect.Height(),rect.Width(),CV_8UC3);
GetDlgItem(IDC_PIC)->GetClientRect(&rect);
m_takepic =Mat::zeros(rect.Height(),rect.Width(),CV_8UC3);
return TRUE;// 除非将焦点设置到控件,否则返回 TRUE
包括填写combolist控件,为两个用于显示的static控件生成对应大小的mat变量等。
3、打开摄像头,主要就是根据选择的摄像头名称,创建摄像头线程
voidCGOMfcTemplate2Dlg::OnBnClickedBtnOpencam()
{
if(m_nCamCount>=1)//开视频捕获线程
{
HANDLE hThread = NULL;
DWORD dwThreadID =0;
OnBnClickedBtnClosecam();//首先关闭现有摄像头
bool bret = cameraDs.OpenCamera(m_iCamNum,false,640,480);//尝试打开摄像头
if(bret)
{
b_closeCam =false;
hThread =CreateThread(NULL,0,CaptureThread,this,0,&dwThreadID);
}
}
else
{
AfxMessageBox(_T("请确认至少有摄像头连上了"));
}
}
稍作修改,可以用于多摄像头,这个是完全没有问题并且做过实际项目的。
4、关闭摄像头
voidCGOMfcTemplate2Dlg::OnBnClickedBtnClosecam()
{
//尝试关闭摄像头
b_closeCam =true;
Sleep(100);
cameraDs.CloseCamera();
}
传递控制变量到摄像头线程,并且调用camerads的closecamera函数关闭摄像头;
5、采集图片
voidCGOMfcTemplate2Dlg::OnBnClickedBtnTakepic()
{
b_takeApic =true;
Sleep(100);
if(m_mainframe.rows >0)
{
showImage(m_mainframe,IDC_PIC);
}
}
传递控制变量到摄像头线程,并且显示图片到控件。
6、显示图像函数,为了方便地讲mat对象显示到mfc的控件上,编写图像实现函数
void CGOMfcTemplate2Dlg::showImage(Mat src, UINT ID)
{
    if (src.empty())
        return;
    CRect rect;
    GetDlgItem(ID) ->GetClientRect( &rect );    // 在哪里进行显示?
    CDC* pDC = GetDlgItem( ID ) ->GetDC();   
    HDC hDC = pDC ->GetSafeHdc();                // 获取显示控件(位置)的 HDC(设备句柄)
    
 
    BITMAPINFO bmi = { 0 };                     //生成bitmap
    bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
    bmi.bmiHeader.biCompression = BI_RGB;
    bmi.bmiHeader.biWidth = src.cols;
    bmi.bmiHeader.biHeight = src.rows * -1;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 24;
 
    if (rect.Width() > src.cols)
    {
        SetStretchBltMode(
            hDC,           // handle to device context
            HALFTONE);
    }
    else
    {
        SetStretchBltMode(
            hDC, // handle to device context
            COLORONCOLOR);
    }
 
    ::StretchDIBits(hDC, 0, 0,
        rect.Width(), rect.Height(), 0, 0, src.cols,src.rows, 
        src.data,&bmi, DIB_RGB_COLORS, SRCCOPY);//显示在界面上
 
    ReleaseDC( pDC );
 
}

 

目前这种模式,不但不需要引入CVVimage,而且内存不会溢出。
7.摄像头参数配置
cameraDs.DisplayFilterProperties();
      
 
 
代码位置 https://github.com/jsxyhelu/GOMfcTemplate2

 [p.s]2016年10月7日 经过对directshow的简单学习,主要参考了ampcap(vs2012+win7可运行版本 https://git.coding.net/jsxyhelu/AMCa_win7vs2012.git ),解决了摄像头的属性操作问题,同时也想办法解决了分辨率设置问题。在usb摄像头上和工业摄像头上测试都没有问题。

 [p.s]我自己在新装的机器上测试框架,不会出现需要direct头文件的情况,但是可能由于操作系统版本不一样,有一些操作系统还是需要头文件的。链接:https://pan.baidu.com/s/1qXNgzOo   direct 为完整版本下载。

以上是关于基于opencv和mfc的摄像头采集代码(GOMFCTemplate2)的主要内容,如果未能解决你的问题,请参考以下文章

为啥用opencv捕获摄像头视频流时,突然显示框内就变灰了呢

TX1使用OpenCV3.1实时采集视频图像

openCV采集摄像头图片并保存的方法

移植opencv到开发板,摄像头在开发板6410上的采集使用

opencv采集图像使用的摄像头CCD和CMOS有区别吗?

如何从视觉采集卡访问数据到 opencv?