DirectShow视频采集和OpenCV

首先说下OpenCV和DirectShow:

OpenCV是Intel开源计算机视觉库。它由一系列 C 函数和少量 C++ 类构成,实现了图像处理和计算机视觉方面的很多通用算法。

DirectShow是微软公司提供的一套在Windows平台上进行流媒体处理的开发包,与DirectX开发包一起发布。DirectShow多媒体流的捕捉和回放提供了强有力的支持。运用DirectShow,我们可以很方便地从支持WDM驱动模型的采集卡上捕获数据,并且进行相应的后期处理乃至存储到文件中。它广泛地支持各种媒体格式,包括AsfMpegAviDvMp3Wave等等,使得多媒体数据的回放变得轻而易举。另外,DirectShow还集成了DirectX其它部分(比如DirectDrawDirectSound)的技术,直接支持DVD的播放,视频的非线性编辑,以及与数字摄像机的数据交换。更值得一提的是,DirectShow提供的是一种开放式的开发环境,我们可以根据自己的需要定制自己的组件。

opencv的cvCaptureFromCAM 使用的是vfw,采用消息机制,也可以进行视频采集,但是速度较慢,测试发现fps只有 9-12左右,太慢了;发现经过使用directshow后速度提升到60帧/s.而且VFW支持的视频格式相对比较少。所以通常情况下都要先用DirectShow的WDM驱动的采集卡采集到视频后,使用cvInitImageHeader()或者cvCreateImage()函数创建iplimage的句柄,然后交给OpenCV进行处理。cvInitImageHeader( IplImage* image, CvSize size, int depth,int channels, int origin CV_DEFAULT(0),int align CV_DEFAULT(4))需要的参数比较‍多,而( CvSize size, int depth, int channels )需要的参数比较少,根据情况自己选择吧。

‍一、安装DirectShowvisual studio 2005

首先我们安装DirectShow SDK,它有许多版本,作者使用的是2003年发布的dx90bsdk.exe,安装在D盘的DXSDK下。软件下载地址为http://download.microsoft.com/download/b/6/a/b6ab32f3-39e8-4096-9445-d38e6675de85/dx90bsdk.exe  http://www.microsoft.com/en-us/download/details.aspx?id=17919

然后安装好visual studio 2005。安装完以后我们将进行开发环境的配置。

二、开发环境配置

开发环境的配置主要有两个工作要做:一是在使用Directshow SDK开发自己的程序时需要的DirectShow的有关静态库的配置,二是visual C++开发环境的配置。

1生成DirectShow SDK开发库

使用DirectShow SDK开发用户自己的程序需要几个静态链接库:quartz.libstrmbasd.libSTRMBASE.libstrmiids.lib。中间两个lib需要用户自己编译生成,而其他两个微软已经提供。下表列出了使用DirectShow SDK开发程序所有要使用的库。

 库名

 功能说明

 Strmiids.lib

定义了DirectShow标准的输出类标识(CLSID)和接口标识(IID

Strmbasd.lib

流媒体开发用到的库,DebugDebug_Unicode版本

Strmbase.lib

流媒体开发用到的库,ReleaseRelease_Unicode版本

Quartz.lib

定义了导出函数AMGetErrorText

Winmm.lib

使用Windows多媒体编程用到的库

 

基于VC++2005开发软件使用DirectShow SDK,首先需要用户编译DirectShow自带的源代码工程baseclasses,以生成DirectShow SDK的不同版本的库。同时由于DirectShow SDK是早期的VC开发软件,所以使用VC++2005编译DirectShow SDK会出现很多编译问题。下面列出了详细的编译过程和问题分析、解决方法。

 

1.1 编译工程baseclasses工程

启动VS2005,选择“文件”→“打开”→“项目/解决方案”命令,在弹出的对话框中打开“BaseClasses”项目。

打开“baseclasses.sln”项目。如果VS2005有提问,则默认同意或确定。现在就开始编译该项目。按“F7”快捷键可以编译生成项目。初次编译VS2005会报很多错误或者警告,有的需要我们手工修改程序,或者修改VS2005环境配置或编译选项;有的是一类问题,解决方法也有很多种。具体解决方法请参考路锦正的Visual C++音频/视频处理技术及工程实践》第225-229页。

 

1.2 Visual C++开发环境配置

有了DirectShow SDK库,用户就可以使用这些库来开发自己的程序了。为了能让VC++自动搜寻到SDK库和头文件,还需要对VC++的开发环境进行配置。添加库或路径的时候,根据你的要求添加DebugReleaseDebug_UnicodeRelease_Unicode版本的库所在路径。下面假定添加非Unicode版本的库或路径。

首先确定VC2005是否已经包含了库和头文件所在的路径,因为在安装VC2005时,它会自动添加该目录。如果没有,则需要用户手工添加。

1.     更改添加的include内容:

D:\DXSDK\Include

D:\DXSDK\Samples\C++\DirectShow\BaseClasses

D:\DXSDK\Samples\C++\Common\Include

添加过程如下。选择“工具”→“选项”命令,在“项目和解决方案下”选择“VC++目录”,在下拉框中选择“包含文件”选项,将上面的三个Include内容添加进去。

2.     更改添加lib路径

要添加的lib内容:

D:\DXSDK\Lib

D:\DXSDK\ Samples\C++\DirectShow\BaseClasses\Debug

D:\DXSDK\ Samples\C++\DirectShow\BaseClasses\Debug_Unicode

D:\DXSDK\ Samples\C++\DirectShow\BaseClasses\Release

D:\DXSDK\ Samples\C++\DirectShow\BaseClasses\Release_Unicode

添加过程和Include内容相似,只是在下拉框中选择“库文件”选项。

3.     添加链接库支持

上面的设置是在VC2005的开发环境的目录(Directories)中,添加用户在开发中可能用到的库或头文件“路径”,需要明确的事文件夹,而不是具体的文件。所以,要使用相关的库支持,还要用户明确地把要使用的库包含、添加到开发环境中。

基于DirectShow SDK开发流媒体应用程序,一般需要链接strmiids.libquartz.lib,前者定义了DirectShow标准的类标识符CLSID和接口标识IID,后者定义了导出函数AMGetErrorText(如果应用程序中没有使用这个函数,也可以不链接这个库)

在编译生成DirectShowBaseClassesstrmbasd.libSTRMBASE.lib时,由于该工程是生成库而不是应用程序,所以在编译该项目时VC++2005没有“链接器”选项。但是在开发其他应用可执行程序时,需要添加DirectShow SDK库的支持。添加路径:项目→属性→配置属性→链接器→输入→附加依赖项,输入strmiids.lib quartz.lib,库名之间用空格分开。另外,在程序中使用DirectShow SDK类或接口的代码程序中,还要添加#include

在添加链接库时,除了以上配置VC的开发环境外,也可以在源程序文件开头部分,直接语句编程引入#pragma comment(lib,”strmiids.lib”)

如果程序中没有使用dshow.h,而是包含了stream.h,则库文件需要链接strmbased.libwinmm.lib,在源程序文件开头添加:

#pragma comment(lib,”strmbasd.lib”)

#pragma comment(lib,”winmm.lib”)

#include

不过,编译器会报出以下的错误。

error C2146:语法错误为缺少“;”(在标识符“m_pString”的前面)。

问题定位在wxdebug.h(329)中。经分析得知,由于某种原因,编译器认为PTCHAR没有定义,那用户可以在类外定义:typedef WCHAR *PTCHAR; 再编译项目。

 

三、开发过程

DirectShow SDK的视频采集经典技术是使用ICaptureGraphBuilder2标准接口,利用其方法RenderStream自动建立、连接滤波器链表。RenderStream方法在预览、捕获视频时引脚的类型分为PIN_CATEGORY_PREVIEWPIN_CATEGORY_CAPTURE,媒体类型均为MEDIATYPE_Video。此实例要完成的目的有两个:一是实时预览采集的视频数据;二是在预览图像的同时,实时地把捕获数据保存到文件中。首先我们使用GraphEdit模拟实现该过程。

1

1视频捕获类CCaptureClass的实现

详细讲述CCaptureClass类的成员变量和其他成员方法的实现,剖析其完成视频采集、保存的技术过程。

1)定义CCaptureClass 

 class CCaptureClass
{
public:
CCaptureClass();          //
类构造器
virtual ~CCaptureClass(); //
类析构器

  

int EnumDevices(HWND hList);                     //枚举视频设备
void SaveGraph(TCHAR *wFileName);            //
保存滤波器链表
void ConfigCameraPin(HWND hwndParent);       //
配置摄像头的视频格式  

void ConfigCameraFilter(HWND hwndParent);     //配置摄像头的图像参数

BOOL Pause();             //暂停

BOOL Play();              //播放

BOOL Stop();              //停止

HRESULT CaptureImages(CString inFileName);    //捕获保存视频

BOOL CaptureBitmap(const char* outFile);        //捕获图片

  

HRESULT PreviewImages(int iDeviceID, HWND hWnd); //采集预览视频
private:
HWND       m_hWnd;          //
视频显示窗口的句柄
IGraphBuilder    *m_pGB;          //
滤波器链表管理器
ICaptureGraphBuilder2  *m_pCapture;  //
增强型捕获滤波器链表管理器
IBaseFilter     *m_pBF;   //
捕获滤波器

IBaseFilter     *pNull;   //渲染滤波器                         IBasicVideo       *pBasicVideo;//视频基本接口   

IBaseFilter        *pGrabberF;//采样滤波器

ISampleGrabber     *pGrabber;//采样滤波器接口
IMediaControl    *m_pMC;   //
媒体控制接口    

IMediaEventEx     *pEvent;  //媒体事件接口
IVideoWindow     *m_pVW;   //
视频显示窗口接口
IBaseFilter     *pMux;   //
写文件滤波器
protected:
bool BindFilter(int deviceId, IBaseFilter **pFilter);   
//
把指定的设备滤波器捆绑到链表中
void ResizeVideoWindow();               //
更改视频显示窗口
HRESULT SetupVideoWindow();             //
设置视频显示窗口的特性
HRESULT InitCaptureGraphBuilder();     //
创建滤波器链表
管理器,查询其各种控制接口


上述代码是类CCaptureClass的成员变量和成员函数,成员变量包括DirectShow 开发流媒体播放应用程序需要的各种接口指针变量。成员函数实现创建滤波器链表管理器、配置视频采集格式、配置图像参数和保存滤波器链表等。

在类的构造器和析构器中完成对

 *定义的资源释放操作宏*/
#ifndef srelease
#define srelease(x) 

if ( NULL != x )
{
x->Release( );
x = NULL;
}
#endif
/*
类构造函数实现*/
CCaptureClass::CCaptureClass()
{
CoInitialize(NULL);  //COM 
库初始化
m_hWnd = NULL;    //
视频显示窗口的句柄
m_pVW = NULL;    //
视频窗口接口指针清空
m_pMC = NULL;    //
媒体控制接口指针清空
m_pGB = NULL;   //
滤波器链表管理器接口指针清空
m_pBF = NULL;     //
捕获滤波器接口指针清空    

pBasicVideo = NULL;//基类视频接口指针清空

pGrabberF = NULL;  //采样滤波器接口指针清空

pNull = NULL;      //渲染滤波器接口清空

pGrabber = NULL;   //

pEvent = NULL;     //媒体事件接口指针清空
m_pCapture = NULL;   //
增强型捕获滤波器链表管理器接口指针清空
}
/*
析构函数实现*/
CCaptureClass::~CCaptureClass()
{
if (m_pMC)  m_pMC->Stop();   //
首先停止媒体
if (m_pVW) {
m_pVW->put_Visible(OAFALSE); //
视频窗口不可见
m_pVW->put_Owner(NULL);  //
视频窗口的父窗口清空
}
srelease(m_pCapture);       //
释放增强型捕获滤波器链表管理器接口srelease(pBasicVideo);

srelease(pGrabber);

srelease(pGrabberF);

srelease(pNull);

srelease(pEvent);
srelease(m_pMC);       //
释放媒体控制接口
srelease(m_pGB);       //
释放滤波器链表管理器接口
srelease(m_pBF);      //
释放捕获滤波器接口
CoUninitialize();     //
卸载COM


在类构造函数中,清空所有指针以便于清楚其后续操作的状态。析构函数释放各种资源、指针并清空指针,最后卸载COM库。

2)根据指定的设备ID,把基本滤波器与设备捆绑

首先枚举系统所有采集设备,直到列举的ID相同为止,最后BindToObject完成捆绑。

//把指定采集设备与滤波器捆绑
bool CCaptureClass::BindFilter(int deviceId, IBaseFilter **pFilter)
{
if (deviceId < 0) return false;
//
枚举所有的视频捕获设备
ICreateDevEnum *pCreateDevEnum;
//
生成设备枚举器pCreateDevEnum
HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL,
CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum,
(void**)&pCreateDevEnum);
if (hr != NOERROR)  return false;
IEnumMoniker *pEm;
//
创建视频输入设备类枚举器
hr = pCreateDevEnum->CreateClassEnumerator
(CLSID_VideoInputDeviceCategory,
&pEm, 0);
if (hr != NOERROR) return false;
pEm->Reset();         //
复位该设备
ULONG cFetched;
IMoniker *pM;
int index = 0;
//
获取设备
while(hr = pEm->Next(1, &pM, &cFetched), hr==S_OK, index <= deviceId)
{
IPropertyBag *pBag;
//
获取该设备的属性集
hr = pM->BindToStorage(0, 0, IID_IPropertyBag, (void **)&pBag);
if(SUCCEEDED(hr))
{
VARIANT var;
var.vt = VT_BSTR;     //
保存的是二进制的数据
hr = pBag->Read(L"FriendlyName", &var, NULL);
         //
获取FriendlyName形式的信息
if (hr == NOERROR)
{
//
采集设备与捕获滤波器捆绑
if (index == deviceId) pM->BindToObject(0, 0,
IID_IBaseFilter,         (void**)pFilter);
SysFreeString(var.bstrVal); //
释放二进制数据资源,必须释放


}   
pBag->Release();
}
pM->Release();
index++;
}
return true;
}

 该函数的传入参数是采集设备的索引号和捕获设备的滤波器。根据索引号查询系统中的视频捕获设备。以友好名字(FriendlyName)的方式获取所选设备的信息,然后把查询成功的设备与传入的滤波器捆绑,返回捕获设备的滤波器。

3)设置视频显示窗口

DirectShow的显示窗口与IVideoWindow接口的设置基本相同,把传入的显示窗口的句柄捆绑到IvideoWindow接口上。

/*设置视频显示窗口的特性*/
HRESULT CCaptureClass::SetupVideoWindow()
{
HRESULT hr;
//m_hWnd
为类CCaptureClass的成员变量,在使用该函数前须初始化
hr = m_pVW->put_Visible(OAFALSE);                  //
视频窗口不可见
hr = m_pVW->put_Owner((OAHWND)m_hWnd);      //
窗口所有者:传入的窗口句柄
if (FAILED(hr)) return hr;
hr = m_pVW->put_WindowStyle(WS_CHILD | WS_CLIPCHILDREN);//
设置窗口类型
if (FAILED(hr)) return hr;
ResizeVideoWindow();                           //
更改窗口大小
hr = m_pVW->put_Visible(OATRUE);              //
视频窗口可见
return hr;
}
/*
更改视频窗口大小*/
void CCaptureClass::ResizeVideoWindow()
{
if (m_pVW) {
//
让图像充满整个指定窗口
CRect rc;
::GetClientRect(m_hWnd,&rc);    //
获取显示窗口的客户区
m_pVW->SetWindowPosition(0, 0, rc.right, rc.bottom);
//
设置视频显示窗口的位置
}
}

在调用该函数前,需要把应用程序的显示窗口句柄传入以初始化m_hWnd。首先视频窗口不可见,捆绑传入的窗口句柄为视频窗口,设置窗口类型。ResizeVideoWindow函数获取显示窗口的客户区域,利用视频窗口接口的方法SetWindowPosition设置视频显示窗口的位置。

4)预览采集到的视频数据

使用上述有关的类成员函数初始化滤波器链表管理器,把指定采集设备的滤波器添加到链表中,然后渲染RenderStream方法把所有的滤波器链接起来,最后根据设定的显示窗口预览采集到的视频数据,具体实现过程如下。

/*开始预览视频数据*/
HRESULT CCaptureClass::PreviewImages(int iDeviceID, HWND hWnd)
{
     HRESULT hr;

     

     // 初始化视频捕获滤波器链表管理器

     hr = InitCaptureGraphBuilder();

     if(FAILED(hr))

     {

         AfxMessageBox(_T("Failed to get video interfaces!"));

         returnhr;

     }

 

     // 把指定采集设备与滤波器捆绑

     if(!BindFilter(iDeviceID, &m_pBF))

         returnS_FALSE;

     // 把滤波器添加到滤波器链表中

     hr = m_pGB->AddFilter(m_pBF, L"Capture Filter");

     if( FAILED( hr ) )

     {

         AfxMessageBox(_T("Can’t add the capture filter"));

         returnhr;

     }

    // Create the Sample Grabber.

    hr = CoCreateInstance(CLSID_SampleGrabber,NULL, CLSCTX_INPROC_SERVER,

         IID_IBaseFilter, (void**)&pGrabberF); 

     if( FAILED( hr ) )

     {

         AfxMessageBox(_T("Can’t create the grabber"));

         returnhr;

     }

    hr = pGrabberF->QueryInterface(IID_ISampleGrabber, (void**)&pGrabber); 

 

     // 把滤波器添加到滤波器链表中

  

     hr = m_pGB->AddFilter(pGrabberF, L"Sample Grabber");

     if( FAILED( hr ) )

     {

         AfxMessageBox(_T("Can’t add the grabber"));

         returnhr;

     }

    // Add the Null Renderer filter to the graph.

    hr = CoCreateInstance(CLSID_VideoRenderer, NULL, CLSCTX_INPROC_SERVER,  

         IID_IBaseFilter, (void**)&pNull);

    hr = m_pGB->AddFilter(pNull, L"VideoRender");

     if( FAILED( hr ) )

     {

         AfxMessageBox(_T("Can’t add the VideoRender"));

         returnhr;

     }

     // 渲染媒体,把链表中滤波器连接起来

    hr = m_pCapture->RenderStream( &PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video, m_pBF, pGrabberF, pNull);

     if( FAILED( hr ) )

     {

         AfxMessageBox(_T("Can’t build the graph"));

         returnhr;

}
//
设置视频显示窗口
m_hWnd = hWnd;         //
初始化窗口句柄
SetupVideoWindow();   //
设置显示窗口
hr = m_pMC->Run();   //
开始采集、预览视频,在指定窗口显示视频
if(FAILED(hr)) {
AfxMessageBox(_T("Couldn't run the graph!"));
return hr;
}
return S_OK;
}

上述程序从最初的创建滤波器链表管理器、枚举系统视频采集设备、把采集设备与捕获滤波器捆绑,到添加滤波器到滤波器链表、设置视频显示窗口,最后开始运行媒体:采集、预览视频,包含了使用DirectShow SDK开发视频采集、预览的整个技术过程。函数功能独立而又完整。

5‍)保存采集到的数据

‍把捕获的视频以AVI格式写文件。注意设置前停止调用滤波器链表,设置完成后再运行链表。

 /*设置捕获视频的文件,开始捕捉视频数据写文件*/
HRESULT CCaptureClass::CaptureImages(CString inFileName)
{

HRESULT hr=0;
m_pMC->Stop();      //
先停止视频
//
设置文件名,注意第二个参数的类型
hr = m_pCapture->SetOutputFileName( &MEDIASUBTYPE_Avi,

inFileName.AllocSysString(), &pMux, NULL );
//
渲染媒体,链接所有滤波器
hr = m_pCapture->RenderStream( &PIN_CATEGORY_CAPTURE,

&MEDIATYPE_Video, m_pBF, NULL, pMux );
pMux->Release();
m_pMC->Run();     //
回复视频
return hr;
}

预览视频后,用户可以使用该函数存储捕获的视频数据。首先停止视频媒体,利用ICaptureGraphBuilder2的方法SetOutputFileName设置存储捕获数据的文件名,然后渲染视频媒体,RenderStream方法自动链接图表中的滤波器,最后开始运行视频媒体。

6)捕获图片

把捕获的视频以bmp格式写文件。我们使用使用Sample Grabber filter抓取图像。sample Grabber使用两种模式抓取图像:缓冲模式和回调模式,缓冲模式向下传递采样时拷贝每个采样,而回调模式对于每个采样调用程序定义的回调函数。我们采用缓冲模式。

BOOL CCaptureClass::CaptureBitmap(const char* outFile)//const char * outFile)

{

     HRESULT hr=0;

    //取得当前所连接媒体的类型

    AM_MEDIA_TYPE mt; 

    hr = pGrabber->GetConnectedMediaType(&mt); 

    // Examine the format block.

    VIDEOINFOHEADER *pVih; 

    if((mt.formattype == FORMAT_VideoInfo) && 

    (mt.cbFormat >= sizeof(VIDEOINFOHEADER)) && 

    (mt.pbFormat != NULL) ) 

    

    pVih = (VIDEOINFOHEADER*)mt.pbFormat; 

    

    else 

    

    // Wrong format. Free the format block and return an error.

    returnVFW_E_INVALIDMEDIATYPE; 

    }

    // Set one-shot mode and buffering.

    hr = pGrabber->SetOneShot(TRUE);

 if(SUCCEEDED(pGrabber->SetBufferSamples(TRUE)))

     {

        boolpass = false;

        m_pMC->Run();

        longEvCode=0; 

        hr = pEvent->WaitForCompletion( INFINITE, &EvCode ); 

        //find the required buffer size

        longcbBuffer = 0;

        if(SUCCEEDED(pGrabber->GetCurrentBuffer(&cbBuffer, NULL)))

         {

            //Allocate the array and call the method a second time to copy the buffer:

            char*pBuffer = new char[cbBuffer];

            if(!pBuffer) 

         {

            // Out of memory. Return an error code.

             AfxMessageBox(_T("Out of Memory"));

            }

            hr = pGrabber->GetCurrentBuffer(&cbBuffer, (long*)(pBuffer));

            //写到BMP文件中

            HANDLE hf = CreateFile(LPCTSTR(outFile), GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0, NULL);

            if(hf == INVALID_HANDLE_VALUE)

            {

             return0;

            }

 

            // Write the file header.

            BITMAPFILEHEADER bfh;

            ZeroMemory(&bfh, sizeof(bfh));

            bfh.bfType = 'MB';  // Little-endian for "MB".

            bfh.bfSize = sizeof( bfh ) + cbBuffer + sizeof(BITMAPINFOHEADER);

            bfh.bfOffBits = sizeof( BITMAPFILEHEADER ) + sizeof(BITMAPINFOHEADER);

            DWORD dwWritten = 0;

            WriteFile( hf, &bfh, sizeof( bfh ), &dwWritten, NULL );

 

            // Write the bitmap format

            BITMAPINFOHEADER bih; 

            ZeroMemory(&bih, sizeof(bih));

            bih.biSize = sizeof( bih ); 

   bih.biWidth = pVih->bmiHeader.biWidth; 

            bih.biHeight = pVih->bmiHeader.biHeight; 

            bih.biPlanes = pVih->bmiHeader.biPlanes; 

            bih.biBitCount = pVih->bmiHeader.biBitCount; 

            dwWritten = 0; 

            WriteFile(hf, &bih, sizeof(bih), &dwWritten, NULL);        

 

            //write the bitmap bits

            dwWritten = 0; 

            WriteFile( hf, pBuffer, cbBuffer, &dwWritten, NULL );

            CloseHandle( hf );

            pass = true;

    }

 

               returnpass;

 

     }     

     hr = pGrabber->SetOneShot(FALSE); 

 

}

预览视频后,用户可以使用该函数捕获图像。这个函数自动为捕获的图片命名并保存。

你可能感兴趣的:(C++,OpenCV)