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采集预览