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文件,最后退出应用程序。