在WPF项目中使用摄像头的例子

说明:WPF中没有现成的摄像头支持控件,需要自己用DShow来实现。在网上找了很多实例程序都不能满足要求,最后在别人的基础上自己完成了这部分代码。下面是详细介绍。         功能:在WPF项目中定义一个区域,在其中显示摄像头当前拍到的内容,并且可以同时把拍下的内容保存为avi文件。

在WPF应用程序中嵌入Win32控件

WPF提供的用于视频播放的控件都没有与摄像头交互的能力,所以必须用传统的DirectShow技术来提供对摄像头的支持。这就涉及到WPF应用程序和Win32控件交互的问题。这是本项目中最棘手的问题,实际也是最先完成的部分。

提供摄像头支持的类是由非托管C++代码实现的,其接口定义如下:

 

public class DShowConnector

{

public:

     //建立DirectShow环境,完成大部分初始化工作

     BOOL Setup( HWND hWnd );

     //设定窗口高度和宽度

     BOOL FinalizeLayout( int width, int height );

     //启动摄像头,预览场景

     BOOL Start();

     //关闭摄像头

     BOOL Stop();

     //暂定屏幕回放

     BOOL Pause();

     //启动摄像头,预览场景并将视频保存到文件

     BOOL Capture();

     //停止保存到文件

     BOOL StopCapture();

     //清理资源

     BOOL Cleanup();

};

 

要在WPF应用程序中使用Win32控件,最好的办法是使用C++/CLI,这是一种支持托管代码的C++版本,要使用它只需在项目或文件的编译选项中添加/clr编译开关,并且保证代码和编译选项中没有和/clr开关冲突的项即可。

由于Visual C++不支持被编译过的XAML,故为了仍能自由地使用WPF的优良特性,我在工程中添加了两个项目:项目一是标准的WPF项目,生成的是exe可执行文件,用来构造用户界面并完成几乎所有用户交互操作;项目二是一个开启了/clr编译开关的C++项目,专门提供摄像头支持,生成的是dll库文件。在解决方案的项目依赖项设置中设置前者依赖于后者,并将后者输出的dll文件添加至前者的引用项中,这样在WPF项目中便可以直接调用C++/CLI项目的接口。下面介绍此接口的实现方法。

WPF定义了专门用于交互操作的HwndHost类,在WPF中使用Win32控件的核心就是要从该类派生出用于创建包含Win32内容的对象,并重写该类的BuildWindowCore和DestryWindowCore方法。前者用于返回一个可以当作宿主的HWND,后者负责在不再需要HWND的时候清理资源。因本项目中需要的视频采集预览窗口完全没有用户交互动作,故可省略消息循环机制的实现。

 

     public ref class WebCamClass : public HwndHost

     {

     private:

         DShowConnector *pConnector;

HWND handle;

 

     public:

         //Constructor     

         WebCamClass()

         {

              pConnector = NULL;

         }

 

         virtual Size ArrangeOverride(Size finalSize) override

         {

              pConnector->FinalizeLayout(finalSize.Width,finalSize.Height);

              return finalSize;

         }

 

         virtual HandleRef BuildWindowCore(HandleRef hwndParent) override

         {

              //创建一个“中间”窗口,作为WPF窗口的子窗口,视频窗口的父窗口

              handle = CreateWindowEx(0, L"Static",

                    NULL,

                    WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,

                    10, 10, // x, y

                    20,20,  // height, width

                   (HWND) hwndParent.Handle.ToPointer(),    // parent hwnd

                    0,      // hmenu

                    0,      // hinstance

                    0);     // lparam

 

              //实例化pConnecter指针

              pConnector = new DShowConnector();

              //建立与摄像头的连接

              if(!pConnector->Setup(handle))

              {

                   pConnector->Cleanup();

                   throw gcnew Exception("Could not connect to camera");

              }

              return HandleRef(this, IntPtr(handle));

         }

 

         virtual void DestroyWindowCore(HandleRef hwnd) override

         {

              //清理资源

              pConnector->Cleanup();

              delete pConnector;

         }

};

 

另外,在WebCamClass中还定义了Start、Stop、Capture、StopCapture四个公有成员函数,它们只是DShowConnector类中Start、Pause、Capture、StopCapture函数的简单封装,在此不再赘述。

DirectShow体系结构分析

要完成上节提到的DShowConnector类,首先要对DirectShow的架构有所了解。下图是一张经典的DirectShow的运行地位和结构图:

<!--[if !vml]--><!--[endif]-->

其中虚线以下运行在R0级别的驱动程序在安装好Windows系统和摄像头驱动后便已经就绪,我们只需要按照特定的接口使用它们。DirectShow最核心的概念是Filter Graph模型,也就是图中央最大的矩形框内的部分,它负责媒体数据流的处理。参与数据处理的各个模块称为Filter,各个不同功能的Filter在Filter Graph中按一定的顺序连接成一条流水线协同工作。

Filter Graph设计

DirectShow中的Filter大致分为三类:Source Filters、Transform Filters和Rendering Filters。Source Filters主要负责从数据源取得数据,然后将数据往下传输;Transform Filters负责数据的格式转换、传输;Rendering Filters主要负责数据的最终去向。本项目中Source Filter的数据源即是摄像头,数据的最终去向在Preview状态下是渲染到屏幕窗口,在Capture状态下既要渲染到屏幕窗口又要记录到文件。

Microsoft Windows Platform SDK 6.1中提供了一个工具软件GraphEdit,利用它可以方便地构造出自己需要的Filter Graph,构造完成后可直接运行查看效果。

首先,摄像头就绪后,在GraphEdit中枚举系统里所有已经注册的Filter,可以找到168-USB PC Camera,用它作为Source Filter即可。

<!--[if !vml]--><!--[endif]-->

 

以Video Renderer作为Rending Filter,连接好之后形成一条完整的链路。

可以看到本项目所使用的168-USB PC Camera只有一个输出端口(更常见的情况是提供Preview和Capture两个输出端口),因此为了能够同时为屏幕输出和文件输出两条链路提供数据流,需要加入一个特殊的Filter--Smart Tee。最终设计的Filter Graph如下:

<!--[if !vml]--><!--[endif]-->

 

用摄像头进行视频的捕捉及回放

在GraphEdit中只能预览Filter Graph设计的效果,具体的代码实现还要手动来一步步完成。

视频捕捉程序的基本流程如下:

 

<!--[if !vml]--><!--[endif]-->

需定义的全局变量:

    

IGraphBuilder *m_pGraph;   

     IVideoWindow *m_pWindow;   

     IMediaControl *m_pControl; 

IBaseFilter *m_pCapture;   

 

其中m_pWindow和m_pControl都是通过IGraphBuilder::QueryInterface方法绑定到m_pGraph的辅助接口,前者用于设置窗口属性(FinalizeLayout函数中用它来调整窗口尺寸),后者用于控制整个Graph的工作的启动和停止(实现DShowConnector类中Start和Stop函数的关键)。

m_pCapture不同于一般的Filter用CoCreateInstance函数创建,而是在枚举系统设备的过程中使用BindToObject方法来创建。枚举函数实现如下:

 

HRESULT DShowConnector::EnumForCaptureSource()

{

     HRESULT hr;

     ICreateDevEnum *pDevEnum = NULL;

     IEnumMoniker *pEnum = NULL;

     IMoniker *pMoniker = NULL;

     VARIANT varName;

 

     hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL,CLSCTX_INPROC_SERVER,

                            IID_ICreateDevEnum,(void**)&pDevEnum);

     if (SUCCEEDED(hr))

     {

         hr = pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory,&pEnum, 0);

         hr = pEnum->Next(1, &pMoniker, NULL);

         if(SUCCEEDED(hr))

         {

              IPropertyBag *pPropBag;

              hr = pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)(&pPropBag));

              VariantInit(&varName);

              hr = pPropBag->Read(L"Description", &varName, 0);

              if (FAILED(hr))

              {

                   hr = pPropBag->Read(L"FriendlyName", &varName, 0);

              }

              SAFE_RELEASE(pPropBag);

            hr = pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&m_pCapture);

         }

     }

     SAFE_RELEASE(pMoniker);

     SAFE_RELEASE(pEnum);

     SAFE_RELEASE(pDevEnum);

     return hr;

}

 

Rending Filter也不必手工创建,IGraphBuilder提供了智能连接方法IGraphBuilder::Render,只要将输出端口传递给该函数,它会自动加入必要的Filter完成剩余部分Filter Graph的构建,直至连接至Rending Filter。

    

     IPin *pOut;

     hr = GetPin(m_pCapture,PINDIR_OUTPUT,&pOut);

hr = m_pGraph->Render(pOut);

 

从Source Filter到Rending Filter的连接全部完成后,就可以调用IGraphBuilder::QueryInterface方法实例化m_pWindow和m_pControl:

    

     hr = m_pGraph->QueryInterface(IID_IMediaControl, (void**)&m_pControl);

hr = m_pGraph->QueryInterface(IID_IVideoWindow, (void**)&m_pWindow);

 

在获取m_pWindow指针后,需要利用其接口将视频播放窗口设置为WebCamClass类中创建的“中间窗口”的子窗口。这也是Setup函数的最后一步工作。

    

     m_pWindow->put_Owner((OAHWND)hWnd);

m_pWindow->put_WindowStyle(WS_CHILD);

 

其中hWnd是Setup函数的形参,在WebCamClass::BuildWindowCore函数中将其创建的“中间窗口”的句柄传递进来。

上述工作完成后,在Start函数中调用m_pControl->Run()便可开始捕捉视频。

将采集的视频写入文件

对DShowConnector::GetPin函数进行的调试验证了168-USB PC Camera生成的Filter只有一个输出端口,因此为了能对采集到的数据同时进行两路处理,程序的挑战度大大上升。幸运的是,通过学习摸索找到了一个合适的(堪称完美的)智能接口——ICaptureGraphBuilder2。利用它可以方便地创建视频采集Filter Graph,然后再将它添加到IGraphBuilder中。最重要的是,调用ICaptureGraphBuilder2:: RenderStream实现Preview链路时,不管Capture Filter是否有Preview Pin,Capture Graph Builder都能自动正确地处理。如本例中Capture Filter只有一个输出口Capture Output Pin,则Capture Graph Builder会自动插入一个Smart Tee Filter再连接。这样实际得到的就是之前在GraphEdit中设计的Graph。

要使用ICaptureGraphBuilder2,首先要在类定义中添加全局变量声明:

ICaptureGraphBuilder2 *m_pBuild;

在创建IGraphBuilder的同时也要创建ICaptureGraphBuilder2,之后要将二者相关联:

    

     hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,

                            IID_IGraphBuilder, (void**)&m_pGraph);

     hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, 0, CLSCTX_INPROC_SERVER,

                            IID_ICaptureGraphBuilder2, (void**)&m_pBuild);

     hr = m_pBuild->SetFiltergraph(m_pGraph);

 

之后同样是枚举系统设备,创建Source Filter,将其添加至Filter Graph。但不必再手工获取Source Filter的输出端口,而是直接将Filter的指针传递给ICaptureGraphBuilder2:: RenderStream函数:

     hr = m_pBuild->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,

                                m_pCapture, NULL, NULL);

RenderStream的第一个参数说明这是一条Preview链路,将使用默认渲染设备作为Rending Filter。

之后的获取媒体控制接口和窗口控制接口过程与前面一节无异。到现在便用ICaptureGraphBuilder2实现了与前面完全相同的功能,但此时要增加Capture链路却变得极其简单,只需再调用一次RenderStream函数:

 

    IBaseFilter *pMux;

    hr = m_pBuild->SetOutputFileName(&MEDIASUBTYPE_Avi, Filename, &pMux, NULL);

hr = m_pBuild->RenderStream(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video,

                            m_pCapture, NULL, pMux);

pMux->Release();

 

这里RenderStream函数的第一个参数说明这是一条Capture链路,最后一个参数是Rending Filter的指针,在设定Preview链路时,采用的是默认Rending Filter,故此参数为NULL,而Capture链路需要将数据流保存至文件,所以要手动创建一个Filter,即pMux。

RenderStream的倒数第二个参数还可设置用来对视频进行编码压缩的Filter,考虑到此项目中硬盘空间尚不成问题,故此参数暂留为NULL。

至此DShowConnector类便基本完成了。在最终的项目方案里没有用到Preview状态,故StopCapture函数也只是简单地调用了Stop函数。

 

转至:http://snowflurry.ycool.com/post.2919488.html

posted @ 2013-03-19 08:43  不弃的追求  阅读(2093)  评论(0编辑  收藏  举报