:基于VFW的视频监控系统

来源:百度文库 编辑:九乡新闻网 时间:2024/03/29 14:05:09
 基于VFW的视频监控系统  VFW是微软提出的一套视频编程接口,这一套工作现在已经有些过时,目前最为流行的就是DirectShow。我没用过DirectShow,但听说它的功能比VFW更为强大,复杂度也比较高。我在利用MFC进行VFW进行视频编程的过程中,总是出现一个莫名其妙的问题,程序流程是正确的,但运行在不同的机器上,结果就有些差异。我写的一个Demo程序在Windows XP上能够很正常地运行,在Win7上,画面显示速度就慢的惊人,甚至不显示。好了,废话少说,直接切入正题。

VFW编程中,要利用到以下知识:

AVI文件的操作,视频流的压缩和解压缩,视频捕获,甚至对于一些高级应用,还要用到音频录制和音频回放。

首先一点如果进行VfW的编程,在MFC中,请在如果用到VFW的地方,写入以下语句:

#include

#pragma comment(lib,"vfw32.lib")

AVI格式文档的写操作

对于AVI文件的格式,网络上有很多的资源,请参看以下文献:

http://home.chinavideo.org/space.php?uid=4525&do=blog&id=19(中文文献)

但是网络上关于AVi的中文文献描述大多含糊不清,最好的方法是在谷歌英文版里面输入AVI Format关键词,来进行查询,英文文献大多正规,而且很认真,中文的文献一般都是到处转帖,没有多少原创,一个帖子里有错误,传了很多遍,错误仍旧存在。

在Google里有一篇英文文献,专门是讲述AVI格式的,其地址如下:

www.alexander-noe.com/video/documentation/avi.pdf

如果您没有多少时间去研究AVi文件格式,而是直接向切入正题,那么没关系,VFW已经给我们封装了很多的操作,基本上在写入数据的时候,只需要三个函数就都可以搞定,这就是软件工作带给我们的好处,下面我按照程序的流程,一步步解释下这些函数作用。

1.       确定AVI文件保存地址。在MFC中,我们可以直接指定一个地址,然而,我们要让用户拥有自己的确定权。我们可以利用CFileDialog类让用户自己指定一个文件存放地址,可能会有人问我,如果我要确定一个目录该如何办,MFC提供给我们两个函数来满足你的要求:SHBrowseForFolder和SHGetPathFromIDList这两个函数,关于这两个函数,你可以参考相关文献。

2.       得到了AVI文件存放的地址,下一步就是开始“磨刀霍霍向文件”了。调用如下两个函数:

AVIFileInit();

AVIFileOpen(文件指针,文件存放完整路径名(包括文件名),文件读写格式);

此时我们得到了一个文件指针,我们此时还要建立一个文件流,来方便我们对视频流的操作。那什么是流呢?流就如同是一个管道,管道的一端是文件,另外一端是数据,我们将视频流放到管道里,流自动将数据存放到文件里,否则的话,用户还得去控制文件存取,还需要费心地去了解底层的东西。

我们需建立一个流指针,我们以后还得需要利用该指针去操作流:

PAVISTREAM ps;

别忙去建立管道,我们还得在建立管道的时候,对这个管道进行一些粉刷。我们需要对刷子进行一些修饰,修饰的时候,我们利用AVISTREAMINFO strhdr这个变量,我们要将刷粉的颜色设置为我们喜欢的颜色,选择一个大小合适的刷子等等。

选好了刷子后,我们就可以去建立一个连接文件的管道流了。

AVIFileCreateStream();

关于该函数,我需要对strhdr变量做一些说明,strhdr中fccType对于视频数据应该设置为streamtypeVIDEO, 注意它不是一个变量,而是常量;还有strhdr.dwSuggestedBufferSize大小确定问题,如果太小了,以后在写入的时候,程序会重新分配缓存大小,将导致速度变慢,如果太大了,又是浪费空间资源,鱼与熊掌不可兼得,我们一般将该数值设置为BMP图像中数据的大小,也就是BITMAPINFOHEADER中biSizeImage大小,不包括文件头信息(BITMAPFILEHEADER)和信息头信息(BITMAPINFO);对于strhdr.rcFrame,一般设置为图像的宽度和图像长度即可。

上面的函数调用成功后,我们就成功地建立一个流了,我们以后就可以利用管道ps来操作文件了。呵呵,万事俱备,只欠东风。

3.       MFC封转了许多的操作,我们在向管道里塞数据的时候,只需要设置每一帧图像的头部,然后就可以去写真正的视频数据了。其实就是写入一副除去BMP头部信息后剩下的一部分,首先设置(写入)第几帧BMP图像的BITMAPINFO信息,这是下面第一个函数要干的事情,设置完成后,我们就可以利用下面第二个函数去写入视频数据了。

AVIStreamSetFormat()

AVIStreamWrite()

对于第一个函数,我们在设置BITMAPINFO的信息过程中的时候,注意要与AVIStreamWrite写入视频数据要对应,就是BITMAP图像的信息头文件信息要与图像数据对应,信息头文件说数据没有压缩,而下面写入的数据却是压缩的,成何体统呢。

有人可能会问什么是帧呢?帧在本应用中分为视频帧和音频帧,对于视频帧,就相当于一副图像。当出现要写入的视频数据的时候,我们就要重复再重复地调用上面两个函数,直到空间用完,或是硬件“暴亡”或是海枯石烂了。

4.       直到你要歇事走人了,要关闭文件读写了,此时调用下面两个函数进行毁尸灭迹了。

AVIStreamClose()关闭流指针

AVIFileRelease()关闭文件指针

 

在整个流程中,上面三个粗体的函数比较麻烦,因为需要你设置一些参数,需要你去了解参数的含义,如果想省事的话,就参考其他人写的程序,设置一些比较常用的数量。另外一点,需要注意的就是三个变量,即流指针、文件指针和当前帧,请不要将这三枚核弹给丢了,否则,你的政权就要被推翻。

 

视频捕捉

 

视频捕捉,多少人与之有说不清道不明的关系,银行里面摄像头可以保证我们交易的正常运行,道路交通的视频监控又让很多的人畏之如虎,不知多少人在它面前栽了跟头。好了,转入正题。

VFW的视频捕捉很有一番意思,它必须要先基于一个窗口创建一个视频捕获窗口,啥子哟,什么基于另一个窗口呀?举个例子吧,对于一个单文档的MFC程序,对于视图而言,它是一个窗口,如果想要创建一个视频捕获窗口,你可以在视图上建立这个视频捕获窗口,哦,为啥呀?内部规定,这个理由不是理由。对于我们编程人员在考虑问题的时候,首先考虑的就是如何用程序来实现,如果利用现在的知识解决不了的话,很容易就会放弃这个方案,其实,我们拿到一个问题的时候,首先思考的应该是解决后应该是什么样子,任何一个创意99%都是可以用程序解决的,只有创意不一定。如果用户要求在这个对话框中实现一个按钮,而你现有的知识解决不了,就否定用户的这个需求,这是不正确的。程序员在拿到问题的时候,还是那句话,先想想问题解决后应该是什么样,然后再去想想问题如何解决,不要凭自己现有知识直接给出答案。言归正传,AVI创建自己的捕获窗口函数如下:

HWND capCreateCaptureWindow()

该函数返回一个捕获视频窗口的句柄,其中需要传入一个窗口句柄,该窗口句柄就是捕获窗口父窗口的句柄,除此之外,还需要向该窗口传入捕获窗口位置的变量,这很容易理解了。找到自己的教室(父窗口),找到自己的座位(位置变量),然后就可以在上面涂鸦(捕获视频)了。

  额,我现在有了捕获视频窗口,我们还要定义它的消息回调函数,视频捕获也是基于消息机制的,当捕获窗口接收到数据的时候,操作系统就要调用相应的回调函数去处理这些数据。我们需要声明对于某些消息,需要调用那些函数进行处理。

capSetCallbackOnVideoStream(HWND, 视频流缓冲器满需要调用的回调函数名称);

当然,还有一些消息需要处理,一般来说,对于捕获视频来讲,上面函数就够了,如果还想了解更多内容,请参阅:

http://dev.csdn.net/htmls/74/74480.html

http://dev.csdn.net/htmls/74/74565.html

这两篇文献讲述了更多关于视频捕获的知识。

在完成了前期工作后,那么我们就可以连接设备了。

capDriverConnect(HWND , index)

上面函数中index代表连接第index个视频摄像头,从0开始数起。那如果你想问,我如何知道系统装了几个摄像头,那么请在调用该函数之前调用:

capGetDriverDescription(iIndex, szDevName, MAX_PATH, szDevVersion, MAX_PATH);

szDevName和szDevVersion返回当前第iIndex个设备的设备名称和版本,该函数可以枚举出系统中每个摄像头以及其详细信息。不过对于一般系统,只有一个摄像头,我们就不用该函数了。

在连接成功摄像头后,我们就要设置捕获数据的一些参数了,首先要查询摄像头能够支持的功能:

CAPDRIVERCAPS m_caps;

capDriverGetCaps(m_hWndCap,&m_caps,sizeof(CAPDRIVERCAPS));

一般来讲,我们还需要设置一个参数:

       if (m_caps.fHasOverlay)

       {

              capOverlay(m_hWndCap,TRUE);//设置Overlay

       }

 

其次是获取捕获窗口的缺省参数:

CAPTUREPARMS CapParms = {0};

capCaptureGetSetup(m_hCapWnd, &CapParms, sizeof(CapParms));

然后修改刚刚获得的参数

CapParms.fAbortLeftMouse = FALSE; // 退出鼠标设置

CapParms.fAbortRightMouse = FALSE; // ...

CapParms.fYield = TRUE; // 使用背景作业

CapParms.fCaptureAudio = FALSE; // 不获取声音

CapParms.wPercentDropForError = 50; // 允许遗失的百分比

最后设置设置捕获窗口的相关参数:

capCaptureSetSetup(m_hCapWnd, &CapParms, sizeof(CapParms));

需要注意其中一个参数fYield,当该函数设置为TRUE时,程序将在后台设置一个线程进行视频捕获,从而将工作交给前台来完成,当然,我们设置为TRUE后,最好写入以下语句:

capSetCallbackOnYield(m_hWndCap,NULL);//对Yield消息不予处理。

这部分参考了:

http://hi.baidu.com/xzm12345/blog/item/ab83f673459e271a8701b0a5.html

上述文献简介了视频捕获的一个简单流程,适合于短期突击人士。

 

然后,我们需要去设置视频图像一个格式,首先获取系统默认的图像格式:

       int fsize=capGetVideoFormatSize(m_hWndCap);

       capGetVideoFormat(m_hWndCap,&lpbiIn,fsize);//等价于capGetVideoFormat ( m_hWndCap , & lpbiIn , sizeof(lpbiIn ) );

该函数调用成功后,就会得到一个BITMAPINFO结构的图像首部信息,也就是变量lpbiIn所载入的信息, 该信息就是视频获取到图像数据的格式信息。如果你不喜欢的话,当然也可以改变这些信息:

capSetVideoFormat(m_hWndCap, & lpbiIn, sizeof(lpbiIn))

OK,一些都准备好了,Ready GO!:

capCaptureSequenceNoFile();

该函数一声令下,视频捕获工作就正式开始了,该函数只捕获数据,并不生成捕获文件,我们自己去处理这些捕获数据。

此时,当捕获一帧图像数据后,操作系统就会调用已经注册的回调函数,我们声明的注册函数为(你可以自己定义一个不同名称的函数,但参数和返回必须按照下面格式进行命名):

LRESULT FAR PASCAL VideoCallbackProc(HWND hWnd,LPVIDEOHDR lpVHdr)

{

}

我们需要了解lpVHdr这个变量,该变量中有两个成员需要掌握,第一是lpVHdr –>dwBytesUsed,第二是lpVHdr –>lpData,前一个变量表明了得到视频数据的大小,第二个指针变量指向了真实的一帧视频数据,其中并不包含其首部信息,其首部信息就存储到了上述例子的lpbiIn中。有了这样的一帧图像信息,你就可以为所欲为了。

好了,当我们捕获完数据了,可以做结束工作了:

              capCaptureAbort(m_hWndCap);

              capDriverDisconnect(m_hWndCap);

              Sleep(500);

              capSetCallbackOnVideoStream(m_hWndCap,NULL);

需要注意是,即使我们调用完这些函数后,系统有可能还会去调用VideoCallbackProc这个回调函数,毕竟回调函数的调用是有些延迟的,请注意这些问题。

 

压缩和解压缩

对于我们来讲,程序员,找到自己的兴趣和维持自己的兴趣是一个很不容易的过程,但这个过程是甜蜜的。老板今天讲述了很多有用的东西,但在某种程度上印证了自己先前的论断是正确的,也就是找到自己的兴趣点和维持自己的兴趣是一个相互作用的过程。找到自己的兴趣点是很不容易的,首先要找到自己的兴趣点,需要拓宽自己的知识领域,另外一点是不要活在别人的眼光里。事实胜于雄辩,我个人以前经过很多事情,有一些事别人都认为是不可能做成的事情,我经过努力不也做成了。找到自己兴趣点后,就不要随随便便地由别人的眼光去否定自己,这是一个很重要的点,那么如何去维持自己的兴趣点,不可否认的是我们都还不是圣人,我们不可能平白无故地维持过长的兴趣的时间。那么我们该如何办呢?那就是得到成就感,我们辛辛苦苦地劳动后,得到了成功的实验结果,这是一个个人的安慰,将实验成果分享出去,当得到别人的赞赏不也是一种成就感吗?所以估计大家多多分享自己的劳动成果。说了这么多,我们该返回正题了。

不管是压缩还是解压缩,我们需要得到句柄,拿到别人的把柄后,你就可以为所欲为了。

       HIC hic2;//解压缩句柄

       HIC hic1; //压缩句柄

目前,开源的编码解码器常见的有XVID,还有一个收费的DIVX,此外还有其他的一些编码。XVID和DIVX名字正好相反,很有意思吧。关于XVID的描述,网络上有相关的文档,有一位仁兄写了一篇《XVID应用编程接口(API)简介(v0.1)》,有兴趣的同学可以去看看。但是仅有XVID还是不够的,VFW提供了统一的接口函数去调用XVID,那么系统如何去查找XVID呢?此时你还得要去修改系统注册表,做一些配置工作,很麻烦吧。没关系,网络上提供了一个XVID的编码器安装软件,直接安装上去就可以了,自动就会替我们设置系统环境。安装文件下载地址:

http://mydown.yesky.com/soft/multimedia/videoeditor/378/441878.shtml

好,做好这些工作后,我们就可以打开编码器了。

       hic1=ICOpen(/*mmioFOURCC('v','i','d','c')*/ICTYPE_VIDEO,mmioFOURCC('X','V','I','D'),ICMODE_COMPRESS);

下面说下如何打开编码器,我们需要确定编码后每一帧图像的格式,系统给我们提供了两个函数来完成这样的功能:

       if (ICCompressGetFormat(hic1,&CPublic::lpbiIn,&CPublic::lpbiTmp)!=ICERR_OK)

       {

              AfxMessageBox("编码器不能够读取格式");

              return;

       }

       if (ICCompressQuery(hic1,&CPublic::lpbiIn,&CPublic::lpbiTmp)!=ICERR_OK)

       {

              AfxMessageBox("不能够处理编码器的读取格式");

              return;

       }

系统会根据CPublic::lpbiIn的输入图像的格式,根据自己内部支持的格式,查询编码后图像格式CPublic::lpbiTmp, 然后我们需要确定编码过程中的一些参数:

COMPVARS CPublic::pc;

       CPublic::pc.cbSize=sizeof(COMPVARS);

       CPublic::pc.dwFlags=ICMF_COMPVARS_VALID;

       CPublic::pc.hic=hic1;

       CPublic::pc.fccType=/*mmioFOURCC('v','i','d','c')*/ICTYPE_VIDEO;

       CPublic::pc.fccHandler=mmioFOURCC('X','V','I','D');

       CPublic::pc.lpbiOut=&CPublic::lpbiTmp;//输出格式

       CPublic::pc.lKey=100;//key帧频率

       CPublic::pc.lQ=10000;

OK,设置完以后我们就可以开始提示系统,我们从现在开始编码:

ICSeqCompressFrameStart(&CPublic::pc,&CPublic::lpbiIn)

当在需要进行编码的时候:

BYTE* buf1=( unsigned char * ) ICSeqCompressFrame ( &CPublic::pc, 0, buf, &isKeyFrame,, &frameSize);

注意这里的参数,这里的frameSize是一个建议缓冲区大小,也就是压缩后图像的大小,如果很小的话,系统会自己分配一个缓存,只是让系统的速度变慢而已,当函数执行完毕后,它会返回isKeyFrame和frameSize, isKeyFrame表明该帧编码后是否是关键帧,frameSize是编码后的数据大小,buf1就指向编码后的数据,这里frameSize和buf1就能用于下一步操作了。

下面来说一下解码。

打开解码器为:

hic2=ICOpen(/*mmioFOURCC('v','i','d','c')*/ICTYPE_VIDEO,mmioFOURCC('X','V','I','D'),ICMODE_DECOMPRESS);

当然对于解码器,我们需要确定解码后图像的格式,常见的解码图像信息头如下:

       CPublic::lpbiOut.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);

       CPublic::lpbiOut.bmiHeader.biWidth=CPublic::lpbiIn.bmiHeader.biWidth;

       CPublic::lpbiOut.bmiHeader.biHeight=CPublic::lpbiIn.bmiHeader.biHeight;

       CPublic::lpbiOut.bmiHeader.biPlanes=1;

       CPublic::lpbiOut.bmiHeader.biBitCount=24;

       CPublic::lpbiOut.bmiHeader.biCompression=BI_RGB;

       CPublic::lpbiOut.bmiHeader.biSizeImage=CPublic::lpbiIn.bmiHeader.biHeight*CPublic::lpbiIn.bmiHeader.biWidth*3;

       CPublic::lpbiOut.bmiHeader.biXPelsPerMeter=0;

       CPublic::lpbiOut.bmiHeader.biYPelsPerMeter=0;

       CPublic::lpbiOut.bmiHeader.biClrUsed=0;

       CPublic::lpbiOut.bmiHeader.biClrImportant=0;

       这里的解码器信息头格式就是解码后图像数据的格式,当然你可以根据自己的需要来确定这些取值。好了,确定后就可以告诉系统,你可以开始解码了。

       ICDecompressBegin(hic2,&CPublic::lpbiTmp,&CPublic::lpbiOut);

解码的函数如下:

ICDecompress();

当一切都结束后,OK,开始清理工作了。

              //清除解码器参数

              ICSeqCompressFrameEnd(&CPublic::pc);

              ICCompressEnd(hic1);

              ICClose(hic1);

              //清除解码器参数

              ICDecompressEnd(hic2);

              ICClose(hic2);

 

 

AVI文件的读操作

 

弱弱地问一下,AVI文档是如何读取的呢?有始有终嘛,既然我们能够存取AVI文件,当然也能够成功地读取AVI文件,首先我们定义一个指向AVI文件的指针。

PAVIFILE  avi;

       在给定文件名的路径的基础下,我们就可以打开文件了。

AVIFileOpen( 
PAVIFILE * ppfile,    
  LPCTSTR szFile,       
  UINT mode,              
  CLSID  pclsidHandler) 
由于我们只是读取AVI文件,这里的mode我们可以写为OF_READ,szFile就是AVI文件存放的路径。这里pclsidHandler可以设置为NULL。
然后我们应该建立一个文件流,以方便我们进行下面的操作:
PAVISTREAM pStream;
AVIFileGetStream(avi, &pStream, streamtypeVIDEO /*video stream*/,0/*first stream*/);
好了,现在我们又该如何办呢?此时如果我们需要得到AVI文件的一些格式信息,请调用下面函数:
        AVIFILEINFO avi_info;
        AVIFileInfo(avi, &avi_info, sizeof(AVIFILEINFO));//得到文件信息
avi_info里面显示有AVI文件中每一帧图像的宽度和高度,包括帧的数量。现在打开AVI文件:
        PGETFRAME pFrame;
        pFrame=AVIStreamGetFrameOpen(pStream, NULL );//打开获取帧信息
我们如果要遍历整个AVI文件,我们需要得到两个变量,第一个就是起始帧的位置,第二就是帧数量。
iFirstFrame=AVIStreamStart(pStream);//得到起始帧,一般为0
iNumFrames=AVIStreamLength(pStream);//得到帧的数量
然后遍历整个AVI文件
 int index=0;
 for (int i=iFirstFrame; i
{
index= i-iFirstFrame;
LPBITMAPINFOHEADER pDIB = (LPBITMAPINFOHEADER) AVIStreamGetFrame(     pFrame, index);
……..
}
上面pDIB其中包含了BITMAPINFO结构的信息头(调色板)和真实的视频数据,请注意这一点,pDIB中存放的数据大小为:
pDIB->biSize+pDIB->biClrUsed*sizeof(RGBQUAD)+pDIB->biSizeImage;
关闭并做一些清理工作
VIStreamRelease() 
AVIStreamGetFrameClose (pFrame)
AVIFileRelease(pfile);   AVIFileExit();