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

2.3 VFW技术实现视频采集

VFW的视频捕获主要由AVICap窗口类来完成,AVICap窗口类为应用程序提供了一个基于消息的接口,使应用程序不仅能访问视频和波形音频硬件,还能在将视频流采集到硬盘的过程中进行控制。

2.3.1 VFW功能概述

VFW通过AVICap来实现视频流采集和单帧采集,并提供对视频源的控制,它能直接访问视频缓冲区,不需要分配中间文件,实时性强、效率高。并且通过简单的函数调用,可以很方便地设置在系统中已经注册的压缩和解压缩驱动,实现视频流、音频流的实时压缩。

概括来讲,AVIcap窗口类的主要功能有如下几个方面:

● 动态连接和断开音视频采集设备;

● 设置视频采集速率;

● 设置视频格式及来源;

● 指定保存数据的文件名及路径;

● 提供Overlay(视频叠加)和Preview(视频预览)两种显示模式;

● 实时采集和存储单帧视频图象,采集视/音频流并存入AVI影音文件;

● 通过AVICap的回调函数(宏)获得有关视频采集的状态和数据。

这些功能的实现是通过调用宏、消息和函数来完成的,开发人员在自己的应用程序中可以根据具体的应用,灵活运用宏(capXX)、消息和函数来实现这些功能。

使用VFW进行视频采集的基本流程是:

● 创建视频采集窗口,这是所有采集工作及VFW设置的基础。

● 注册系统回调函数,使得在采集过程中VFW自动回调相应的函数。

● 连接视频采集设备,获取视频采集设备的能力及状态信息。

● 获取和设置采集窗口参数,如视频分辨率、颜色空间和采集速率等。

● 设置采集窗口的显示模式为Preview或Overlay。

● 根据不同需要,选择采集图像到缓存还是文件。

● 视频采集结束时,将采集窗口与驱动断开连接,否则将导致视频驱动无法释放,其它程序将不能使用该采集设备。

2.3.2 VFW开发流程

根据视频采集的基本流程,其工作过程如图2-16所示,首先创建视频采集窗口,然后配置回调函数并连接采集设备,系统根据回调函数的功能采集每帧图像,用户就可以直接访问视频数据,最后在退出采集程序时需要关闭捕获设备并销毁相关资源。

图2-16 VFW工作流程图

2.3.3 VFW回调函数

VFW的很多功能是通过回调函数实现的,如读取视频图像、获取设备当前状态等。使用回调函数的过程是:首先注册回调函数,然后VFW根据设备状态自动调用回调函数。常用的回调函数如表2-3所示。

表2-3 VFW的注册回调函数

这些回调函数的参数形式均为:

capSetCallBackOn_xx(HWND hwnd, CAP_XX_CALLBACK fpProc),其中hwnd为视频采集窗口的句柄,fpProc为回调函数指针。

2.3.4 VFW视频采集

1.建立单文档应用程序

现在基于VC++2005的项目建立向导,创建一个单文档、无工具栏和状态栏的应用程序。

Step 1 启动VC++2005,选择菜单“文件→新建→项目”。在“新建项目”对话框中,选择“Visual C++”项目类型下的“MFC”,选中“模板”的“MFC应用程序”。项目命名为vfwcapture。单击“确定”开启“MFC应用程序向导”。根据向导提示逐步建立应用程序。

Step 2 为项目添加功能菜单“VFW”,并插入3个子菜单,ID设置如表2-4所示。编辑菜单如图2-17所示。

表2-4 VFW采集子菜单

图2-17 VFW功能菜单

Step 3 修改窗口样式,在MainFrm.cpp中的函数PreCreateWindow中添加:

    cs.cx = 352+8;
    cs.cy = 288+54;
    cs.lpszName=_T("VFW采集");       //要设置的标题
    cs.style&   =~FWS_ADDTOTITLE;   //覆盖应用程序名称,只显示lpszName

Step 4 引入链接库vfw32.lib及csss.lib

在VC++2005中包含库vfw32.lib及cscc.lib,并在调用库函数的文件中包含vfw.h及convert.h头文件。

    #include <vfw.h>
    #include “conver.h”

源程序中直接引入库:

    #pragma comment(lib,”vfw32.lib”)
    #pragma comment(lib,”csss.lib”)

或者在VC++2005开发环境中选择“项目”→“属性”→“配置属性”→“链接器”命令,输入库名。另外,忽略特定库libc.lib。

Step 5 在使用VFW开发时,在MainFrm.cpp中定义全局变量以控制采集和显示等功能。

    #define XDIM 704                            //图像宽度,预设的足够大
    #define YDIM 576                            //图像高度,预设的足够大
    unsigned char bufo[XDIM*YDIM*3+40];         //送客户区显示的图像,RGB格式
    LPBITMAPINFO   lpbiIn;                    //输入格式
    COMPVARS       pc;                        //编码设置结构体
    BOOL              bPreview=FALSE;         //是否在预览
    CFrameWnd     m_wndSource;                //创建的新窗口
    HWND          m_hWndCap;                  //VFW设备窗口
    CRect             disRect;                //显示窗口的客户区域
    CMainFrame*pMainFrame=NULL;               //MainFrame 指针
    int           CurrentID;                  //捕获音频、视频或音视频
    enum     VFW_STATE{PREVIEW,NOWORK};       //
    VFW_STATE     m_vfwState=PREVIEW;         //当前状态

Step 6 其他变量定义及初始化

在类CmainFrame中定义变量以捕获视频、记录帧数等。

    CAPDRIVERCAPS  m_caps;                      //VFW驱动结构
    DWORD          m_Frame;                     //记录帧数
    ColorSpaceConversions conv;                  //颜色空间转换对象

在MainFrm.cpp的窗口创建函数OnCreate()中做有关初始化。

    lpbiIn  =new BITMAPINFO;                        //BMP位图结构
    pMainFrame=this;                               //获取主窗口
    ::GetClientRect(m_wndSource.m_hWnd,&disRect);  //获取新创建窗口的客户区
    bPreview=FALSE;                                //显示模式为Overlay叠加模式

2.创建视频窗口

VFW的设备控制和视频数据的读写均通过窗口句柄来实现,所以首先需要创建视频窗口。使用函数capCreateCaptureWindow创建视频窗口。添加子菜单“配置设备”的事件处理,并在CMainFrame中定义,然后创建窗口。

    void CMainFrame::OnVfwInit()
    {
        //TODO: 在此添加命令处理程序代码
        DWORD fsize;
        // 创建视频窗口
        if(!m_wndSource.CreateEx(WS_EX_TOPMOST,NULL,
                                  _T("Source"),WS_OVERLAPPED|WS_CAPTION,
                                  CRect(0,0,352,288),NULL,0))
            return;
        m_hWndCap = capCreateCaptureWindow(_T("Capture Window"),WS_CHILD|WS_VISIBLE,
                                            0,0,352,288, m_wndSource.m_hWnd,0);
    }

3.定义并注册回调函数

通常用到的回调函数有三个:错误回调、状态改变回调和视频获取回调。在MainFrm. cpp中定义回调函数。

Step 1 错误回调函数

    LRESULT CALLBACK EXPORT ErrorCallbackProc(HWND hWnd, int nErrID, LPSTR lpErrorText)
    {
        if(nErrID==0)
            return TRUE;
        MessageBox(NULL,(LPCWSTR)lpErrorText,_T(""),MB_OK);
        return TRUE;
    }

上述代码实现当采集设备出错时,通知用户保护现场并做系统维护。

Step 2 状态回调函数

    LRESULT FAR PASCAL StatusCallbackProc(HWND hWnd, int nID, LPSTR lpStatusText)
    {
        if(nID==IDS_CAP_END){
            if((CurrentID==IDS_CAP_STAT_VIDEOAUDIO)||(CurrentID==IDS_CAP_STAT_VIDEOONLY))
                return TRUE;
        }
        CurrentID = nID;
        return (LRESULT) TRUE;
    }

Step 3 视频获取回调函数

视频缓冲区填充满时,回调该函数。对视频数据的处理均在此函数实现,现在要求把捕获的数据拷贝到显示缓冲区,并送显。

    LRESULT FAR PASCAL VideoCallbackProc(HWND hWnd, LPVIDEOHDR lpVHdr)
    {
        unsigned char *bufi;
        bufi=new unsigned char[lpVHdr->dwBytesUsed+40];       //original image
        memcpy((void *)(bufi), lpVHdr->lpData, lpVHdr->dwBytesUsed); //获取图像数据
        {
            // 转换颜色空间
            pMainFrame->conv.YV12_to_RGB24(bufi,
                    bufi+(lpbiIn->bmiHeader.biWidth*lpbiIn->bmiHeader.biHeight),
                    bufi+(lpbiIn->bmiHeader.biWidth*lpbiIn->bmiHeader.biHeight*5/4),
                    &bufo[40],
                    lpbiIn->bmiHeader.biWidth,
                    lpbiIn->bmiHeader.biHeight);
        }
        pMainFrame->GetActiveView()->InvalidateRect(NULL,FALSE); //送显
        delete bufi;
        return (LRESULT) TRUE;
    }

特别提醒,用户对视频数据的所有直接处理均在此回调函数中实现,本项目首先从VFW设备中拷贝YUV420数据,然后转换到RGB空间,最后传递消息给主窗口显示图像。

定义了回调函数后,接下来在MainFrm.h中声明回调函数:

    LRESULT CALLBACK EXPORT ErrorCallbackProc(HWND hWnd, int nErrID, LPSTR lpErrorText);
    LRESULT FAR PASCAL StatusCallbackProc(HWND hWnd, int nID, LPSTR lpStatusText);
    LRESULT FAR PASCAL VideoCallbackProc(HWND hWnd, LPVIDEOHDR lpVHdr);

注册上述三个回调函数:

    void CMainFrame:: OnVfwInit ()
    {
        //TODO: 在此添加命令处理程序代码
        DWORD fsize;
        // 创建视频窗口
        if(!m_wndSource.CreateEx(WS_EX_TOPMOST,NULL,
                                  _T("Source"),WS_OVERLAPPED|WS_CAPTION,
                                  CRect(0,0,352,288),NULL,0))
            return;
        m_hWndCap = capCreateCaptureWindow(_T("Capture Window"),WS_CHILD|WS_VISIBLE,
                                            0,0,352,288, m_wndSource.m_hWnd,0);
        capSetCallbackOnError(m_hWndCap,(FARPROC)ErrorCallbackProc);          //出错回调
        capSetCallbackOnStatus(m_hWndCap,(FARPROC)StatusCallbackProc);          //状态回调
        capSetCallbackOnVideoStream(m_hWndCap,(FARPROC)VideoCallbackProc);     //读取视频流
    }

4.显示视频图像

在视频图像的显示中,采用VFW的DrawDib函数组,窗口任意比例的图像实时显示。

Step 1 在vfwcaptureView.h中定义成员变量。

    HDRAWDIB m_hdd;

Step 2 在vfwcaptureView.cpp中打开、关闭DrawDib。

    CvfwcaptureView::CvfwappView()
    {
        //TODO: 在此处添加构造代码
        m_hdd=DrawDibOpen();    // 打开DrawDib
    }
    CvfwcaptureView::~CvfwappView()
    {
        DrawDibClose(m_hdd);     // 关闭DrawDib
    }

Step 3 图像显示

通常动态的显示图像并刷新是在视图中完成的,本项目同样如此。

    void CvfwcaptureView::OnDraw(CDC * pDC)
    {
        CvfwappDoc* pDoc = GetDocument();
        ASSERT_VALID(pDoc);
        if (!pDoc)
            return;
        //TODO: 在此处为本机数据添加绘制代码
        RECT rect;
        GetClientRect(&rect);   //获得客户区域
        /*****************Add the bitmap information.********************************************/
        ((LPBITMAPINFOHEADER)bufo)->biSize = sizeof(BITMAPINFOHEADER);
        ((LPBITMAPINFOHEADER)bufo)->biWidth = lpbiIn->bmiHeader.biWidth;
        ((LPBITMAPINFOHEADER)bufo)->biHeight = lpbiIn->bmiHeader.biHeight;
        ((LPBITMAPINFOHEADER)bufo)->biPlanes = 1;
        ((LPBITMAPINFOHEADER)bufo)->biBitCount=24;                 //24位位图
        ((LPBITMAPINFOHEADER)bufo)->biCompression=BI_RGB;          //RGB格式
        ((LPBITMAPINFOHEADER)bufo)->biSizeImage =
                                  lpbiIn->bmiHeader.biWidth*lpbiIn->bmiHeader.biHeight*3;
        ((LPBITMAPINFOHEADER)bufo)->biClrUsed = 0;
        ((LPBITMAPINFOHEADER)bufo)->biXPelsPerMeter = 0;
        ((LPBITMAPINFOHEADER)bufo)->biYPelsPerMeter = 0;
        ((LPBITMAPINFOHEADER)bufo)->biClrImportant = 0;
        /*****************Add the bitmap information.********************************************/
        DrawDibDraw(m_hdd,
                      pDC->GetSafeHdc(),
                      rect.left,rect.top,  rect.right,rect.bottom,
                      (LPBITMAPINFOHEADER)bufo,&bufo[40],   //bufo的前40个字节是BMP文件头
                      0,0, -1,-1,
                      0);
    }

首先获取待显示位图的信息,然后取得窗口的客户区,最后用DrawDibDraw函数实现图像显示。

5.捕获和预览视频

在捕获视频时总希望先预览视频,然后再捕获保存。假定系统仅安装了一个视频采集设备。在注册回调函数后,连接视频设备,启动捕获。

Step 1 连接视频设备

    void CMainFrame::OnVfwInitvfw()
    {
        //TODO: 在此添加命令处理程序代码
        DWORD fsize;
        // 创建视频窗口
        if(!m_wndSource.CreateEx(WS_EX_TOPMOST,NULL,
                                  _T("Source"),WS_OVERLAPPED|WS_CAPTION,
                                  CRect(0,0,352,288),NULL,0))
            return;
        m_hWndCap = capCreateCaptureWindow(_T("Capture Window"),WS_CHILD|WS_VISIBLE,
                                            0,0,352,288,
                                            m_wndSource.m_hWnd,0);
        // 注册回调函数
        capSetCallbackOnError(m_hWndCap,(FARPROC)ErrorCallbackProc);
        capSetCallbackOnStatus(m_hWndCap,(FARPROC)StatusCallbackProc);
        capSetCallbackOnVideoStream(m_hWndCap,(FARPROC)VideoCallbackProc);
        // 连接视频设备
        capDriverConnect(m_hWndCap,0); //index : 0--9
        // 获取驱动器参数
        capDriverGetCaps(m_hWndCap,&m_caps,sizeof(CAPDRIVERCAPS));
        if (m_caps.fHasOverlay)
            capOverlay(m_hWndCap,TRUE);
        // 设置预览速率开始预览
        capPreviewRate(m_hWndCap,1000/25);
        capPreview(m_hWndCap,bPreview); //
        fsize = capGetVideoFormatSize(m_hWndCap);
        capGetVideoFormat(m_hWndCap, lpbiIn, fsize);
        AfxMessageBox(_T("初始化成功!"));
    }

上述黑体程序中,首先连接视频设备,然后取得驱动器参数,根据m_caps的内容判断采集设备驱动器的性能,最后设置预览速率开始预览。

Step 2 配置视频格式和图像参数

视频采集设备提供了缺省的视频格式和图像参数,用户也可以进行修改。利用VFW功能函数配置“视频格式”。对项目vfwcapture的“VFW”菜单子项“视频格式”,添加消息处理函数。

    void CMainFrame::OnVfwFormat ()
    {
        //TODO: 在此添加命令处理程序代码
        DWORD fsize;
        if(m_caps.fHasDlgVideoFormat){
            capDlgVideoFormat(m_hWndCap);                 //显示视频格式对话框
            fsize=capGetVideoFormatSize(m_hWndCap);       //读取新的格式大小
            capGetVideoFormat(m_hWndCap,lpbiIn,fsize);    //读取新格式数据
        }
        if(m_caps.fHasDlgVideoSource){
            capDlgVideoSource(m_hWndCap);                 //显示图像源参数:亮度、色度和饱和度等
        }
    }

视频格式属性下的图像分辨率和颜色空间,如图2-18所示。调整图像参数,如图2-19所示。

图2-18 配置视频格式

图2-19 调整图像参数

Step 3 捕获视频

响应项目vfwcapture的“VFW”菜单子项“捕获视频”,并添加消息处理函数。

    void CMainFrame:: OnVfwCapture ()
    {
        //TODO: 在此添加命令处理程序代码
        CAPTUREPARMS CapParms;
        bPreview=!bPreview;                          //捕获预览开关
        if(bPreview){
            capCaptureGetSetup(m_hWndCap,&CapParms,sizeof(CAPTUREPARMS)); //获取参数
            CapParms.dwIndexSize=324000;
            CapParms.fMakeUserHitOKToCapture=!CapParms.fMCIControl;
            CapParms.wPercentDropForError=100;       //丢帧百分比
            CapParms.wNumVideoRequested=5;           //视频缓冲区最大数量
            CapParms.wChunkGranularity=0;
            CapParms.fYield=TRUE;                    //产生后台线程进行视频捕捉
            CapParms.fCaptureAudio=FALSE;            //没有捕获音频
            CapParms.vKeyAbort=0;                    //终止捕获的键
            CapParms.fAbortLeftMouse=CapParms.fAbortRightMouse=FALSE;  //鼠标左右键控制停止捕捉
            CapParms.dwRequestMicroSecPerFrame=1000000/25;    //捕获的帧率25f/s
            capSetCallbackOnYield(m_hWndCap,NULL);          //回调函数为空
            // 配置捕获参数CapParms
            capCaptureSetSetup(m_hWndCap,&CapParms,sizeof(CAPTUREPARMS));
            //如果要捕获视频流,则要使用函数指定不生成文件,否则将会自动生成AVI文件
            capCaptureSequenceNoFile(m_hWndCap);
            m_vfwState  =PREVIEW;
        }else{
            capCaptureAbort(m_hWndCap);
            m_vfwState  =NOWORK;
        }
    }

上述配置过程中,实现了多次单击“捕获视频”子菜单可以开始或终止捕获视频。

Step 4 关闭VFW采集

重载窗口关闭消息WM_CLOSE,添加处理函数。

    void CMainFrame::OnClose()
    {
        //TODO: 在此添加消息处理程序代码和/或调用默认值
        if (m_vfwState==PREVIEW){
            capCaptureAbort(m_hWndCap);                      //终止捕获
            capDriverDisconnect(m_hWndCap);                  //断开与驱动的连接
            Sleep(100);                                      //等待前面的操作完毕
            capSetCallbackOnError(m_hWndCap,NULL);           //注销回调函数
            capSetCallbackOnStatus(m_hWndCap,NULL);          //注销回调函数
            capSetCallbackOnVideoStream(m_hWndCap,NULL);     //注销回调函数
            delete lpbiIn;
            Sleep(100);                                      //确保前面的操作完毕
        }
        CFrameWnd::OnClose();
    }

首先终止捕获,断开驱动连接,注销回调函数,释放内存资源。至此在“配置设备”→“视频格式”→“捕获视频”后,实时显示VFW捕获的视频数据,效果如图2-20所示。

图2-20 VFW采集预览