MPEG-4/H.264视频编解码工程实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 DirectShow实现视频采集

DirectShow从VC++开发人员角度来看是SDK(Software Development Kits,软件开发包),更直接来讲就是静态库(LIB)、动态链接库(DLL);从计算机软件专业角度来讲是COM组件,而使用DirectShow开发的应用程序则是COM组件的客户程序。

2.2.1 DirectShow概述

一般的DirectShow应用程序,如捕获、播放多媒体等并不需要详细掌握COM技术,但当开发自己的Filter组件时,就需要对COM技术有较为深入的了解。由于本书是应用案例开发,因此Filter的开发细节请参见DirectShow的在线帮助。

Microsoft DirectShow是为多媒体流的应用而开发的。它支持多媒体流的捕获(Capture)、预览(Preview)。运用DirectShow,用户可以很方便地从支持WDM驱动模型的采集卡上捕获数据,并且可以进行相应的后期处理甚至存储到文件中,同时可以方便的构建影音文件的回放、编辑等操作。使用DirectShow能够完成如下工作:

(1)音视频多媒体流的捕获和预览;

(2)支持多种媒体格式:ASF、MPEG、AVI、MP3、WAVE的回放;

(3)集成其他DirectX技术,增强音视频硬件如声卡、显卡的性能;

(4)视频文件的回放、非线性编辑等;

(5)支持DVD、DV等设备;

(6)定制自己的Filter。

DirectShow的设计初衷就是简化在Windows平台上开发流媒体应用程序,把应用程序从数据传输、硬件差别和同步机制等复杂问题中分离开来。DirectShow使用DirectSound和DirectDraw技术提高音视频流媒体的吞吐量、高效的渲染、表现流媒体数据到用户声卡和显卡。同时,DirectShow在流媒体数据中封装了时间标签以保证其同步回放。

为处理各种可能的流媒体数据源、媒体格式和硬件设备,DirectShow采用了模块化结构,在这些模块中混合、匹配这些应用,模块又称为“滤波器”(Filters)。DirectShow使用滤波器支持WDM驱动模型采集卡、早期的VFW模式采集卡、各种音视频CODEC (ACM:Audio Compression Manager,VCM:Video Compression Manager)。图2-1是音视频应用、DirectShow组件和DirectShow可支持的硬件组件的关系框图。

图2-1 DirectShow SDK系统框图

框图中的虚线框就是DirectShow部分,分析可以看出它起到了呈上起下的作用。应用程序根据一定的机制(命令和事件),通过访问DirectShow,以完成对底层设备如视频捕获设备、声卡/显卡、音视频编解码器CODEC等的访问和维护。DirectShow内可部使用滤波器图表管理器管理源滤波器、转换滤波器和渲染滤波器三种Filters。

DirectShow最初并不是DirectX SDK的成员,从DirectX 6.0开始才出现。目前开发应用的主流是DirectX SDK 8.0和9.0,但是到DirectX SDK 9.0以后的版本,DirectShow又被移出了DirectX,而被包含在了Windows SDK中。另外特别需要注意,DirectX SDK 9.0也有几个版本,其中DirectX SDK 9.0C并不包含DirectShow,而DirectX SDK 9.0b则包含DirectShow。所以用户在下载较新的DirectX SDK时,则需要额外下载DirectX SDK Extras之类的升级包。例如Visual Studio 2005 Professional Edition(VS2005)中包含了DirectX SDK,但并没有DirectShow SDK,则用户需要再下载Extras升级包(dxsdk_feb2005_extras.exe)。

作者开发用的DirectShow是从某网站外挂链接的dx9sdk.exe,即DirectX SDK 9.0。ftp地址分别如下:

(1)ftp://ftp.jaring.my/pub/microsoft/directx/directx9/dx9sdk.exe

(2)ftp://ftp.qut.edu.au/pub/mirrors/microsoft/directx/directx9/dx9sdk.exe

(3)ftp://ftp.yzu.edu.tw/mirror/pub1/Windows/MSDownload/directx/9.0/sdk/dx9sdk.exe

2.2.2 开发环境配置

假设dx90sdk.exe安装在d:\DXSDK,在使用DirectShow前需要做两个环境配置,一是DirectShow的有关功能库的配置,二是软件VC++2005的开发环境配置。

1.生成DirectShow链接库

应用DirectShow开发自己的程序需要多个静态链接库,如strmiids.lib、quartz.lib、strmbasd.lib或strmbase.lib等,后两个库需要用户自己编译生成,而其他两个库DirectShow已经提供。表2-1列出了使用DirectShow开发多媒体应用程序时常用的库。

表2-1 DirectShow常用库

基于VC++2005开发软件使用DirectShow,首先需要用户编译DirectShow自带的工程BaseClasses,以生成DirectShow的不同版本的功能库。同时由于DirectShow是基于VC++的早期版本,所以使用VC++2005编译DirectShow SDK会出现很多编译问题。需要用户根据编译器的提示定位问题、分析问题并解决问题。

Step 1 启动VisualC++2005

选择“开始→运行”命令,键入devenv(开发环境:Develop Environment),快速启动VisualC++2005

Step 2 编译BaseClasses项目

在VC++2005开发环境中,选择“文件→打开→项目/解决方案”命令,打开“BaseClasses”项目,编译生成strmbasd.lib、strmbase.lib静态库。

注意,在打开项目baseclasses.sln时,如果VC++2005有询问时,默认选择“同意”或“确定”。使用快捷键F7编译生成项目。初次编译VC++2005会提示很多错误或者警告,有的需要用户手工修改,或者修改VC++2005环境配置或编译选项;有的是特质问题,需找到相应的解决方法。

错误的定位和解决:下述错误主要是由于VC++2005不支持变量的默认定义,需要显定义变量类型。

1)Winnt.h中添加“#define POINTER_64 __ptr64”

2)Ctlutil.h中修改“LONG operator=(LONG);”

3)Winutil.cpp中修改“UINT Count = 0;”

4)Outputq.cpp中修改“long iDone = 0;”

5)wxdebug.cpp中修改“static int g_dwLastRefresh = 0;”

警告的分析和解决,错误“warning C4996: “_vsnwprintf”被声明为否决的”一般是由于VC++6.0程序升级到VC++2005时经常遇到的一类警告,通常是字符串函数的操作引起的。解决办法为,选择“项目→属性→配置属性→C/C++→预处理器→预处理器定义”命令,在命令行中添加:_CRT_SECURE_NO_DEPRECATE。

Step 3 批生成多版本的静态库

用户在使用VC++6.0时,编译生成一般有Debug和Release两个版本。而在VC++2005中,则又新增了Debug及Release的Uicode版本。因为在VC++2005的Unicode模式下,一个字符占2个字节,而旧版VC++1个字符占1个字节,即是ANSI格式。所以,VC++2005提供了4种版本的生成,见图2-2,开发人员根据需要使用不同的生成。

图2-2 批编译生成配置

至此4个版本的DirectShow的Baseclasses静态库均编译生成,后面就可以用它们来开发自己的流媒体应用程序。但是,为使VC++2005能够访问这些库和函数,还需要配置开发软件的路径。

2.VC++开发环境配置

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

Step 1 添加Include路径

要添加的Include内容:

D:\DXSDK\Include

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

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

添加过程为,在VC++2005开发环境下,选择“工具→选项→项目和解决方案→VC++目录”命令,在显示以下内容的目录下选择“包含文件”,若包含的文件没有上面列出的内容,则需要单击按钮,新插入一行,根据提示添加包含的路径,如图2-3所示。

图2-3 添加包含文件对话框

Step 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

添加过程为,在VC++2005开发环境下,选择“项目和解决方案→VC++目录”命令,在显示以下内容的目录的下拉列表框中选择“库文件”。如果列表中没有上述的路径,单击按钮,依次添加所需要的Lib路径。设置完整的库文件对话框如图2-4所示。

图2-4 添加库文件对话框

Step 3 添加链接库支持

上述是设置VC++开发软件的目录(Directories)、库文件或头文件的路径,但不是具体的文件。所以,要链接相关库支持,还应把库包含到开发环境中。

基于DirectShow开发流媒体应用程序,一般需要链接strmiids.lib和quartz.lib。添加路径为:项目→属性→配置属性→链接器→输入→附加依赖项,键入:strmiids.lib quartz.lib,库名之间用空格分开,如图2-5所示。另外,在程序中使用DirectShow类或接口的程序中,还应添加 #include <dshow.h>。

图2-5 添加链接库属性页

除了上述环境配置方法外,也可以在源程序文件的开头部分,编程引入:#pragma comment(lib,"strmiids.lib")。

另外,如果程序中没有使用dshow.h,而是streams.h,则需要链接strmbasd.lib、winmm.lib。在源程序文件的开头添加如下代码:

    #pragma comment(lib,"strmbasd.lib")
    #pragma comment(lib,"winmm.lib")
    #include <streams.h>

在编译时,编译器会报以下错误:

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

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

2.2.3 使用VMR快速捕获视频

经典的基于DirectShow实时捕获视频通常是应用ICaptureGraphBuilder2标准接口,利用该接口的方法RenderStream自动建立、链接滤波器链表,该过程对用户来说,视频数据是透明的,无法直接访问。DirectShow的视频混合渲染VMR(Video Mixing Renderer)技术能够实现视频图像的流畅显示,可以从显示的图像中直接捕获。首先VMR在流畅预览RGB的同时,用户捕捉图像帧,然后从RGB空间转换到YUV空间,供视频图像处理算法直接处理。

VMR-7可用于Windows XP操作系统,且是默认选择,相比旧的显示滤波器功能有较大的提升。VMR-9是DirectX 9.0中的视频渲染技术,采用Direct3D技术。VMR-9不是默认选择的滤波器,对系统要求更高,但功能更强、效果更好,它使用了最新的图像API函数,提供了最好的显示性能。根据视频的显示窗口特点,VMR分为有窗口和无窗口两种模式。Video Renderer只支持窗口模式,VMR支持有窗口(Window)和无窗口(Windowless)两种模式,默认为有窗口。在无窗口模式中,可以把视频和应用程序的主界面窗口进行捆绑。

VMR在清晰、流畅显示视频的同时,能灵活地捕获图像帧,这也是选择VMR技术的一个重要原因,因为用户想直接对图像数据做处理。

基于DirectShow开发流媒体应用程序,一般首先利用DirectX自带的GraphEdit程序将各个Filter组件快速搭建,若成功运行则再编程实现媒体应用。下面展示了GraphEdit快速构建VMR的滤波器链表,可以从中感受VMR的处理效果。

Step 1 启动GraphEdit程序,插入“Video Capture Sources”滤波器,如图2-6所示。

图2-6 视频源滤波器

Step 2 由于VMR-9显示的数据格式是根据系统显卡的支持格式确定的,如ARGB32/YUV2等。而如果采集设备不支持这些类型时,则需要在采集设备和VMR之间增加一个转换滤波器。通常有两种滤波器可用于媒体类型的匹配转换:AVI Decompressor和Color Space Converter。选择并插入“AVI Decompressor”和“Color Space Converter”滤波器,如图2-7所示。

图2-7 插入媒体类型匹配滤波器

Step 3 选择并插入“Video Mixing Render 9”滤波器,如图2-8所示。

图2-8 插入VMR-9滤波器

Step 4 至此所有滤波器已经插入完毕,现在把这些滤波器链接起来。要特别注意,链接的顺序为从左至右,即源滤波器、“Color Space Converter”和VMR-9,如图2-9所示。

图2-9 GraphEdit工具

Step 5 单击工具栏的按钮,开始运行滤波器链表。如果没有出现预览视频窗口,请首先确定视频设备是否安装或正常工作,然后再运行。

Step 6 单击工具栏的按钮停止预览,然后单击按钮断开链接,再连接“Source Filter→AVI Decompressor→VMR-9”,如图2-10所示。

图2-10 GraphEdit工具

Step 7 单击工具栏的按钮,运行链表,开始预览视频,如图2-11所示。

图2-11 VMR技术预览视频图像

通过上面的GraphEdit快速演示得知,VMR能够流畅地捕获、预览视频图像,下面就通过编程实现上述的视频捕获预览功能。

2.2.4 视频捕获类CVmrCapture

为完成视频采集、预览和捕获的任务,封装了CVmrCapture类,以方便使用VMR技术实现相关任务。根据GraphEdit的滤波器内容和链表链接等的操作,编程实现前述过程以显示、捕捉图像。

Step 1 在头文件VmrCapture.h中定义类CVmrCapture,代码如下:

        enum PLAYER_STATE{INIT,RUNNING,PAUSED,STOPPED};
    class CVmrCapture
    {
    public:
        CVmrCapture();                                      //类构造器
        virtual~CVmrCapture();                             //类析构器
        int EnumDevices(HWND hList);                          //枚举设备
        HRESULT Init(int iDeviceID,HWND hWnd,int iWidth,int iHeight);//根据设备索引号初始化
        DWORD GetFrame(BYTE**pFrame);                        //获取捕获的图像帧
        BOOL Pause();                                        //暂停预览、捕获
        DWORD GrabFrame();                                   //截获图像
        void CloseInterfaces(void);                          //关闭接口,释放资源
        void SaveGraph(CString wFileName);                    //保存滤波器链表
    protected:
        IGraphBuilder            *m_pGB;                    //滤波器链表管理器
        IMediaControl            *m_pMC;                    //媒体控制接口
        IMediaEventEx            *m_pME;                    //媒体事件接口
        IVMRWindowlessControl9   *m_pWC;                    //VMR-9接口
        IPin                     *m_pCamOutPin;//视频采集滤波器引脚
        IBaseFilter              *m_pDF;                    //视频采集滤波器
        PLAYER_STATE        m_psCurrent;
        int       m_nWidth;                                 //图像帧宽度
        int       m_nHeight;                                //图像帧高度
        BYTE      *m_pFrame;                                //捕获的图像帧数据指针
        long m_nFramelen;                                   //捕获的图像帧数据大小
        bool BindFilter(int deviceId,IBaseFilter**pFilter);   //设备与滤波器捆绑
        HRESULT InitializeWindowlessVMR(HWND hWnd); //初始化无窗口模式的VMR
        HRESULT InitVideoWindow(HWND hWnd,int width,int height);          //初始化视频窗口
        void StopCapture();
        void DeleteMediaType(AM_MEDIA_TYPE*pmt);                       //删除媒体类型
        bool Convert24Image(BYTE*p32Img,BYTE*p24Img,DWORD dwSize32);    //颜色空间转换
    private:
    };

类CVmrCapture封装了应用VMR技术的成员函数及变量,包括类的构造器、析构器;滤波器链表管理器接口、媒体控制接口、媒体事件接口等;图像帧的宽度、高度等参数。

Step 2 在VmrCapture.cpp文件中实现类CvmrCapture的初始化、析构及成员函数的定义等。

        // 释放资源的宏定义
        #define RELEASE_POINTER (x) { if (x) x->Release(); x = NULL; }
        /*类构造器*/
    CVmrCapture::CVmrCapture()
    {
        CoInitialize(NULL);            //初始化COM库
        //接口指针清空
        m_pGB=NULL;           m_pMC=NULL;
        m_pME=NULL;           m_pWC=NULL;
        m_pDF=NULL;           m_pCamOutPin=NULL;
        m_pFrame=NULL;        m_nFramelen=0;
    m_psCurrent=STOPPED;
}
    /*类析构器*/
CVmrCapture::~CVmrCapture()
{
    CloseInterfaces();             //清空指针、释放资源
    CoUninitialize();              //卸载COM库
}
    /* 释放所有资源,断开链接*/
void CVmrCapture::CloseInterfaces(void)
{
    HRESULT hr;
    // 停止媒体回放
    if(m_pMC) hr = m_pMC->Stop();
    m_psCurrent = STOPPED;
    // 释放并清零接口指针
    if(m_pCamOutPin)
      m_pCamOutPin->Disconnect();               //断开引脚的链接
    RELEASE_POINTER(m_pCamOutPin);              //视频采集滤波器引脚清空
    RELEASE_POINTER(m_pMC);                     //媒体控制接口清空
    RELEASE_POINTER(m_pGB);                     //滤波器链表管理器接口清空
    RELEASE_POINTER(m_pWC);                     //VMR-9接口清空
    RELEASE_POINTER(m_pDF);                     //视频采集滤波器接口清空
    // 释放分配的内存
    if(m_pFrame!=NULL)
        delete[]m_pFrame;                       //释放图像空间缓存
}

上述程序完成资源释放、指针清空等工作。在退出应用程序时,调用此函数。

Step 3 枚举本地系统的采集设备,代码如下:

         /* 枚举本地系统的采集设备*/
    int  CVmrCapture::EnumDevices(HWND hList)
    {
         if(!hList)return  -1;
         int id=0;
         //枚举所有视频采集设备
         ICreateDevEnum*pCreateDevEnum;
         //生成设备枚举器
         HRESULT hr=CoCreateInstance(CLSID_SystemDeviceEnum,NULL,
                              CLSCTX_INPROC_SERVER,
                              IID_ICreateDevEnum, (void**)&pCreateDevEnum);
       if (hr != NOERROR) return -1;
         IEnumMoniker*pEm;
            //创建视频类枚举器
         hr=pCreateDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&pEm,0);
         if(hr!=NOERROR)return-1;
         pEm->Reset();       //复位设备
        ULONG cFetched;
        IMoniker *pM;
        while(hr = pEm->Next(1, &pM, &cFetched), hr==S_OK) //根据索引获取设备
        {
            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)
                {
                    id++;
                    //把该设备的信息添加到组合框
                    (long)SendMessage(hList, CB_ADDSTRING, 0,(LPARAM)var.bstrVal);
                    SysFreeString(var.bstrVal);   //释放资源
                }
                pBag->Release();                  //释放资源
            }
            pM->Release();                        //释放资源
        }
        return id;
    }

上述程序实现了枚举系统所有的视频采集设备,然后把FriendlyName信息添加到传入参数组合框内。

Step 4 构建滤波器链表,添加滤波器,链接并运行链表。应用程序调用该函数实现VMR的有关初始化,并在最后运行VMR,实现视频的采集和预览,代码如下:

    HRESULT CVmrCapture::Init(int iDeviceID,HWND hWnd, int iWidth, int iHeight)
    {
        HRESULT hr;
        //再次调用函数,释放已经建立的链表
        CloseInterfaces();
        // 创建IGraphBuilder
        hr=CoCreateInstance(CLSID_FilterGraph,NULL,  CLSCTX_INPROC_SERVER,
                      IID_IGraphBuilder, (void **)&m_pGB);
        if (SUCCEEDED(hr))
        {
            // 创建VMR并添加到Graph中
            InitializeWindowlessVMR(hWnd);
            // 把指定的设备捆绑到一个滤波器
            if(!BindFilter(iDeviceID, &m_pDF))
                return S_FALSE;
            // 添加采集设备滤波器到Graph中
            hr = m_pGB->AddFilter(m_pDF, L"Video Capture");
            if (FAILED(hr)) return hr;
            // 获取捕获滤波器的引脚
            IEnumPins  *pEnum;
            m_pDF->EnumPins(&pEnum);
            hr |= pEnum->Reset();
            hr |= pEnum->Next(1, &m_pCamOutPin, NULL);
            // 获取媒体控制和事件接口
              hr |= m_pGB->QueryInterface(IID_IMediaControl, (void **)&m_pMC);
              hr |= m_pGB->QueryInterface(IID_IMediaEventEx, (void **)&m_pME);
            // 设置窗口通知消息处理
              //hr = pME->SetNotifyWindow((OAHWND)hWnd, WM_GRAPHNOTIFY, 0);
            // 匹配视频分辨率,对视频显示窗口设置
            hr |= InitVideoWindow(hWnd,iWidth, iHeight);
            // 为捕获图像帧申请内存
            m_nFramelen=iWidth*iHeight*3;
            m_pFrame=(BYTE*) new BYTE[m_nFramelen];
            // 运行Graph,捕获视频
            m_psCurrent = STOPPED;
            hr |= m_pGB->Render(m_pCamOutPin);
            hr |= m_pMC->Run();
            if (FAILED(hr)) return hr;
            m_psCurrent = RUNNING;
        }
        return hr;
    }

上述程序使用VMR-9技术实现视频的采集、存储任务。首先如果不是第一次调用,则关闭所有接口并释放资源。接着创建IGraphBuilder作为滤波器链表管理器,然后添加VMR滤波器到链表中,把指定的采集设备索引与捕获滤波器捆绑,并把该滤波器添加到链表中。再获取捕获滤波器的引脚,获取媒体控制接口和事件接口。设置窗口通知消息处理,根据输入的视频分辨率匹配采集设备的分辨率。最后使用自动渲染功能Render把滤波器链表链接起来。使用媒体控制接口方法Run运行媒体。以下为该函数的子功能实现。

Step 5 创建VMR滤波器,并添加到Graph链表中,代码如下:

        /* 创建VMR,添加、设置VMR*/
    HRESULT CVmrCapture::InitializeWindowlessVMR(HWND hWnd)
    {
       IBaseFilter* pVmr = NULL;
       // 创建VMR
       HRESULT hr = CoCreateInstance(CLSID_VideoMixingRenderer, NULL,
                                  CLSCTX_INPROC, IID_IBaseFilter, (void**)&pVmr);
       if (SUCCEEDED(hr))
       {
            //添加VMR到滤波器链表中
          hr = m_pGB->AddFilter(pVmr, L"Video Mixing Renderer");
          if (SUCCEEDED(hr))
          {
              // 设置无窗口渲染模式
              IVMRFilterConfig* pConfig;
              hr = pVmr->QueryInterface(IID_IVMRFilterConfig, (void**)&pConfig);
              if( SUCCEEDED(hr))
              {
                pConfig->SetRenderingMode(VMRMode_Windowless);
                pConfig->Release();
              }
                  // 设置传入的窗口为显示窗口
              hr = pVmr->QueryInterface(IID_IVMRWindowlessControl, (void**)&m_pWC);
              if( SUCCEEDED(hr)) {
                m_pWC->SetVideoClippingWindow(hWnd);     //设置视频剪辑窗口
              }
          }
          pVmr->Release();
      }
      return hr;
    }

首先使用CoCreateInstance创建VMR的接口pVmr,然后把VMR滤波器添加到滤波器链表中。设置视频显示为无窗口模式,首先在pVmr接口下查询IVMRFilterConfig接口,以参数VMRMode_Windowless使用SetRenderingMode的方法完成设置。最后设置传入的窗口为视频剪辑窗口。

Step 6 设置捕获图像帧的格式,遍历所有格式是否有预定格式,若没有则以默认格式捕获。代码如下:

    HRESULT CVmrCapture::InitVideoWindow(HWND hWnd,int width, int height)
    {
        HRESULT hr;   //返回值
        RECT rcDest;   //矩形区域
        IAMStreamConfig*pConfig; //流配置接口
        IEnumMediaTypes*pMedia; //枚举媒体类型接口
        AM_MEDIA_TYPE*pmt=NULL,*pfnt=NULL;  //媒体类型
        hr = m_pCamOutPin->EnumMediaTypes( &pMedia ); //获取捕获设备的所有媒体类型
        if(SUCCEEDED(hr))
        {
                //把视频的所有格式遍历一遍,看是否有预定的格式
          while(pMedia->Next(1, &pmt, 0) == S_OK)
          {
              if( pmt->formattype == FORMAT_VideoInfo )
              {
                  VIDEOINFOHEADER *vih = (VIDEOINFOHEADER *)pmt->pbFormat;
                      // 当前的格式是否与预定格式相同,即宽和高是否相同
                  if( vih->bmiHeader.biWidth == width && vih->bmiHeader.biHeight == height )
                  {
                      pfnt = pmt; //记录当前媒体格式
    break;   //退出循环
}
DeleteMediaType( pmt );//格式不匹配,则删除当前查询的媒体格式
}
}
pMedia->Release();
}
// 获取流配置接口
hr = m_pCamOutPin->QueryInterface( IID_IAMStreamConfig, (void **) &pConfig );
if(SUCCEEDED(hr)){
// 有预定的媒体格式
if( pfnt != NULL ){
hr=pConfig->SetFormat( pfnt );
DeleteMediaType( pfnt );
}
// 没有预定的格式,读取缺省媒体格式
hr = pConfig->GetFormat( &pfnt );
if(SUCCEEDED(hr)){
m_nWidth=((VIDEOINFOHEADER*)pfnt->pbFormat)->bmiHeader.biWidth;  //读取高
m_nHeight=((VIDEOINFOHEADER*)pfnt->pbFormat)->bmiHeader.biHeight; //读取宽
DeleteMediaType( pfnt );
}
}
// 获取传入窗口的区域,以设置显示窗口
::GetClientRect (hWnd,&rcDest);
hr = m_pWC->SetVideoPosition(NULL, &rcDest); //设置视频窗口位置
return hr;
}
/* 删除媒体类型*/
void CVmrCapture::DeleteMediaType(AM_MEDIA_TYPE *pmt)
{
if (pmt == NULL) { //为空则直接返回
return;
}
if(pmt->cbFormat!=0){   //格式块大小不为0
//释放由CoTaskMemAlloc或CoTaskMemRealloc申请的内存块
CoTaskMemFree((PVOID)pmt->pbFormat);
pmt->cbFormat = 0;
pmt->pbFormat = NULL;
}
if(pmt->pUnk!=NULL){    //IUnknown接口不为空
pmt->pUnk->Release();   //释放资源
pmt->pUnk = NULL;
}
CoTaskMemFree((PVOID)pmt);
}

无论是采用CoTaskMemAlloc函数还是用CreateMediaType函数分配的内存都可以用DeleteMediaType函数来释放。

Step 7 从VMR中获取一帧图像数据,转换到RGB24颜色空间,其中m_pFrame指向RGB24格式的数据。代码如下:

    /* 从VMR中获取一帧图像格式:RGB24/I420*/
    DWORD CVmrCapture::GrabFrame()
    {
        if(m_pWC ){
          BYTE* lpCurrImage = NULL;
          if(m_pWC->GetCurrentImage(&lpCurrImage) == S_OK){
                // 读取图像帧数据lpCurrImage
                LPBITMAPINFOHEADER  pdib=(LPBITMAPINFOHEADER)lpCurrImage;
                if(m_pFrame==NULL||(pdib->biHeight*pdib->biWidth*3)!=m_nFramelen)    {
                      if(m_pFrame!=NULL)
                      delete []m_pFrame;
                      m_nFramelen = pdib->biHeight * pdib->biWidth * 3;
                      m_pFrame = new BYTE [pdib->biHeight * pdib->biWidth * 3] ;
                }
                if(pdib->biBitCount==32)  {
                      DWORD  dwSize=0,dwWritten=0;
                      BYTE *pTemp32;
                      pTemp32=lpCurrImage + sizeof(BITMAPINFOHEADER);
                      //change from 32 to 24 bit /pixel
                      this->Convert24Image(pTemp32, m_pFrame, pdib->biSizeImage);
                }
                CoTaskMemFree(lpCurrImage);  //释放图像lpCurrImage
            } else { return -1; }
        }else{ return -1; }
        return m_nFramelen;
    }

上述程序获取当前显示图像,如果格式错误则重新申请空间。由于采集的图像多为32位,所以需要转换成24位以便于视频算法处理。捕获的图像帧存储在CVmrCapture的成员变量m_pFrame中,该函数返回图像帧的数据大小。

Step 8 转换ARGB32空间到RGB24空间。代码如下:

    /* ARGB32 to RGB24 */
    bool CVmrCapture::Convert24Image(BYTE *p32Img, BYTE *p24Img,DWORD dwSize32)
    {
        if(p32Img != NULL && p24Img != NULL && dwSize32>0) //确认操作有效
        {
            DWORD dwSize24;
            dwSize24=(dwSize32*3)/4;           //RGB32与RGB24的像素点空间只差了一个字节
            BYTE *pTemp=p32Img,*ptr=p24Img;
            for (DWORD index = 0; index < dwSize32/4 ; index++)
            {
                unsigned char r=*(pTemp++);    unsigned char g=*(pTemp++);
                unsigned char b=*(pTemp++);     (pTemp++);     //跳过alpha分量
                *(ptr++)=b;    *(ptr++)=g;    *(ptr++)=r;    //记录RGB 3分量
            }
        } else {
            return false; //指针不合法
        }
        return true;
    }

RGB32与RGB24格式只差分量Alpha,所以在转换时丢弃每个像素点的Alpha分量。

2.2.5 VMR编程实现视频采集

在如下的案例中运用类CVmrCapture实现基于VMR的采集视频、定时捕获图像,并从ARGB32转换到YUV420(I420)格式,保存YUV文件。

Step 1 应用VC++2005应用程序向导建立对话框程序框架,项目名称为DshowCapture。

Step 2 添加控件:3个Button、1个ComboBox、1个Picture Control,如表2-2所示。根据其功能修改所有控件的ID。

表2-2 控件功能列表

Step 3 修改控件大小,重排其位置。添加控件后的主界面如图2-12所示。

图2-12 DshowCapture主界面

Step 4 控件与变量捆绑

为便于控制,把IDC_VIDEO_WINDOW控件、IDC_LISTDEVICE控件分别与变量捆绑,方式是使用控件的右键“添加变量”,激活“添加成员变量向导”。添加后的代码如下:

    // 显示捕获的图像
    CStatic m_videoWindow;
    // 组合框列表,显示设备名称
    CComboBox m_listCtrl;

Step 5为使用类CVmrCapture的变量及函数,在DshowCapture项目中添加VmrCapture.h.cpp和VmrCapture.h。另外,由于需要颜色空间最后转换到YUV420,所以引入库cscc.lib及convert.h。

    #include <dshow.h>
    #include "convert.h"
    #include "VmrCapture.h"

Step 6 在当前项目中链接库“cscc.lib strmiids.lib quartz.lib”,方法是:“项目→属性→配置属性→链接器→输入→附加依赖项”,键入上述名称。在“忽略特定库”中键入“libc.lib”。

Step 7 定义类CdshowCaptureDlg的成员变量,在文件DshowCaptureDlg.cpp中添加如下代码:

    CVmrCapture m_VMRCap;                   //VMR类
    ColorSpaceConversions conv;             // 颜色空间转换类
    UINT m_timerID;                         // 定时器ID
    CString m_yuvFileName;                  //YUV文件名
    CFile m_pFile;                          // 文件类
    int m_imageWidth;                       // 图像宽度
    int m_imageHeight;                      // 图像高度
    BYTE*p_yuv420;                         //YUV420空间

Step 8 在文件DshowCaptureDlg.cpp中,定义宏参数为修改提供方便,代码如下:

    #define TIMER_ID 1                   // 定时器ID
    #define TIMER_DELAY 20               // 定时器周期
    #define CAMERA_WIDTH  352           // 图像宽度
    #define CAMERA_HEIGHT  288          // 图像高度

Step 9 在文件DshowCaptureDlg.cpp中的函数OnInitDialog()中添加如下代码,以初始化变量。

    m_VMRCap.EnumDevices(this->m_listDevice);     //枚举系统采集设备,设备名称添加到组合框
    m_listDevice.SetCurSel(0);                    //选择0索引的设备(系统装有一个采集设备)
    m_yuvFileName=_T("");                         //yuv文件名
    m_fileState=FALSE;                            //文件当前状态
    m_imageWidth=CAMERA_WIDTH;                    //预采集的图像宽度
    m_imageHeight=CAMERA_HEIGHT;                  //预采集的图像高度
    p_yuv420=NULL;                                //YUV420数据空间

Step 10 为了周期性的从VMR中读取图像数据,添加定时器消息处理函数。在VC++2005开发环境下,在“类视图”内的类CdshowCaptureDlg中右键单击“属性”,点击消息按钮,下拉并定位到WM_TIMER,选择“<添加>OnTimer”,添加如下代码。

    void DshowCaptureDlg::OnTimer(UINT_PTR nIDEvent)
    {
        //TODO: 在此添加消息处理程序代码和/或调用默认值
        if(nIDEvent==TIMER_ID)               //定时器ID
        {   //定时获取图像帧,转换颜色空间,写文件
            if(m_fileState)                   //文件已经打开
            {
                DWORD dwSize;
                dwSize=this->m_VMRCap.GrabFrame();   //获取图像帧数据
                if (dwSize >0 )
                {
                      BYTE *pImage;
                      this->m_VMRCap.GetFrame(&pImage);//读取图像内容
                      //把RGB24转换为YUV420
                      conv.RGB24_to_YV12(pImage,this->p_yuv420,m_imageWidth,m_imageHeight);
                      //写YUV420数据到文件
                      m_pFile.Write(p_yuv420,m_imageWidth*m_imageHeight*3/2);
                }
            }
        }
        CDialog::OnTimer(nIDEvent);
    }

成员函数GrabFrame获取图像数据,GetFrame只是获取前者保存的数据指针。为便于视频算法处理,转换RGB24到YUV420。该转换使用了经典的cscc.lib库。

Step 11 双击主界面按钮,添加按钮单击的消息处理功能。

方法为:双击“预览图像”,添加事件处理:

    void CDshowCaptureDlg::OnBnClickedPreview()
    {
        //TODO: 在此添加控件通知处理程序代码
        HWND hwnd=this->m_videoWindow.GetSafeHwnd();        //获取显示窗口句柄
        int id=this->m_listCtrl.GetCurSel();                     //获取当前设备索引
        HRESULT hr = m_VMRCap.Init(id,hwnd,m_imageWidth,m_imageHeight);//初始化所有任务并启动预览
        if (FAILED(hr))
            AfxMessageBox(_T("无法创建滤波器链表!"));         //创建失败
    }

上述程序实现视频预览功能。首先获取显示窗口的句柄,根据设定的设备ID号、图像宽度及高度来初始化所有任务并开始预览,如创建中出租任务错误则表明创建失败。视频预览效果如图2-13所示。

图2-13 VMR视频预览

之后,双击“保存图像”按钮,添加事件处理:

    void CDshowCaptureDlg::OnBnClickedCapture()
    {
        //TODO: 在此添加控件通知处理程序代码
        CFileDialog dlg(FALSE);                     //另存为文件对话框
        if (dlg.DoModal()==IDOK)
        {
            m_yuvFileName=dlg.GetPathName();       //获取输入的文件路径名
            BOOL ret=m_pFile.Open(m_yuvFileName,   //打开、创建文件
                                CFile::modeCreate|CFile::modeWrite|CFile::typeBinary);
            if (!ret) {
                AfxMessageBox(_T("创建YUV文件失败!"));
                return ;
            } else m_fileState = ret;
            if(!p_yuv420)                        //分配YUV420图像空间
                p_yuv420 = new BYTE [m_imageWidth*m_imageHeight*3/2];
            this->KillTimer(TIMER_ID);              //清除以前创建的定时器
            this->SetTimer(TIMER_ID,TIMER_DELAY,NULL); //启动定时器
        }
    }

上述程序完成YUV420空间的创建,周期性的采集图像。该消息处理函数的效果图如图2-14、图2-15所示。需特别注意,在保存图像前,要先预览图像,否则程序会出错。

图2-14 另存为对话框

图2-15 同时预览和捕获的主界面

最后,双击“终止捕捉”按钮,添加事件处理:

    void CDshowCaptureDlg::OnBnClickedQuit()
    {
        //TODO: 在此添加控件通知处理程序代码
        KillTimer(TIMER_ID);
        if (p_yuv420)
            delete[]p_yuv420;        //释放内存
        if (m_fileState)
            m_pFile.Close();          //关闭YUV文件
        CDialog::OnOK();             //退出程序
    }

上述程序实现终止捕获、存储等任务,释放内存,关闭YUV文件,最后退出应用程序。