您的位置:首页 > 运维架构

基于VFW的视频监控系统

2011-03-26 17:16 495 查看
基于VFW的视频监控系统

王利朋 2011年3月25日
现在是研二下半学期,和往常一样,我们又要开始准备找工作了。寡人人品不济,只好呆在实验室,抽空将自己的想法包括以前想干的事情,用程序实现。下面将目前所掌握的基于VFW的视频监控的知识予以列出,以供后来人少走些弯路。
VFW是微软提出的一套视频编程接口,这一套工作现在已经有些过时,目前最为流行的就是DirectShow。我没用过DirectShow,但听说它的功能比VFW更为强大,复杂度也比较高。我在利用MFC进行VFW进行视频编程的过程中,总是出现一个莫名其妙的问题,程序流程是正确的,但运行在不同的机器上,结果就有些差异。我写的一个Demo程序在Windows XP上能够很正常地运行,在Win7上,画面显示速度就慢的惊人,甚至不显示。好了,废话少说,直接切入正题。
VFW编程中,要利用到以下知识:
AVI文件的操作,视频流的压缩和解压缩,视频捕获,甚至对于一些高级应用,还要用到音频录制和音频回放。
首先一点如果进行VfW的编程,在MFC中,请在如果用到VFW的地方,写入以下语句:
#include <vfw.h>
#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<iNumFrames; 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();
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: