前几天在帮师兄做一个视频截图的模块,采用了DirectShow的接口来访问视频文件。开发工具使用的是Visual C 2005 Express 和Visual C# 2005 Express,VC 写的一个封装了对DirectShow的接口访问的DLL,然后在C#做的界面程序里面调用。
1. 关于DirectShow的视频截图方法
DirectShow以前是属于DirectX内的一个部分,后来Microsoft把DirectShow归入了Platform SDK内了。关于DirectShow如何来截取视频文件内部的图片picture,在网上可以搜索到很多。在MSDN关于DirectShow SDK的教程里面,也有专门举例如何使用DirectShow的IMediaDet接口来截视频流内的截图的:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wcedshow/html/_dxce_dshow_directshow.asp
使用DirectShow来访问视频文件就可以避免去了解各种视频压缩文件格式,编码格式等等很繁琐甚至是困难的问题了。
2. 选择哪个时间点来截视频图片呢?
但是,选取哪个时间点的视频图片呢?我们在看Windows浏览器里面视频文件的微缩图都是视频文件的第一帧,但是如果第一帧是全黑或者全白呢?那么我们看到的这个截下来的视频图片并没有任何意义。甚至比如电影开头的演员字幕等帧,对于观众来说都没有多大的意义。一部电影的截图选择,如果按照精彩镜头来分,那么需要计算机去理解该电影的内容,这个工作在现阶段来说,涉及到计算机视觉,数字图像,人工智能等前沿技术,不大可能做得出来。
考虑到全黑,全白,以及片头字幕等没有意义的帧图片的特点,就是颜色个数相对较少,相对单调。于是,可以通过一个颜色个数的阈值,来对所有帧图片进行筛选。将颜色个数小于阈值的剔除。一般使用颜色丰富的图片,肯定帧图片更加丰富。
但是,在24位真彩色中,R,G,B都是0-255,任何一个分量相差了一点点,视觉上来说,差异并不大,但是对于计算机来说,就完全是两个颜色了,这种过于精确的颜色统计,对于人来说并不见得好。于是,我选择使用颜色的灰度值来代替真彩色RGB的统计。关于RGB到灰度值的公式,选择的是最简单的:
GRAY(灰度) = (R G B) / 3
3. 实现一个测试算法的Demo
好了,大体的截图选取算法思想就是这样了。下面我就一步一步来把这个算法实现的Demo,通过Visual C 2005 Express和Visual C# 2005 Express开发工具做出来。
首先是做封装DirectShow的Win32 DLL。
Microsoft那里下载的Visual C 2005 Express并没有附带Platform SDK,Windows的最新Platform SDK可以直接从Microsoft的MSDN那里下载到(我选择的是Windows 2003 Server RC2)。按照MSDN上所述的,搭建起Visual C 2005 Express内的Platform SDK设置后就可以开发Win32的程序了。
下面是封装的DLL的程序代码:

//MovieGrabberDLL.cpp:定义DLL应用程序的入口点。

//

#include"stdafx.h"

#include"MovieGrabberDLL.h"

BOOLAPIENTRYDllMain(HANDLEhModule,

DWORDul_reason_for_call,

LPVOIDlpReserved

)


...{

switch(ul_reason_for_call)


...{

caseDLL_PROCESS_ATTACH:

caseDLL_THREAD_ATTACH:

caseDLL_THREAD_DETACH:

caseDLL_PROCESS_DETACH:

break;

}

returnTRUE;

}


/**//**

*抓取视频的截图

*@paramaPath视频文件的位置

*@return

*/

MOVIEGRABBERDLL_APIHANDLEGrabMovieFrame(LPCTSTRaPath,intgrayColorCountThreshold)


...{

HRESULThr;

//定义IMediaDet接口实例

CComPtr<IMediaDet>pDet;

hr=pDet.CoCreateInstance(__uuidof(MediaDet));

if(FAILED(hr))

returnNULL;

//将影片文件名转换成BSTR类型

CComBSTRopenBSTR(aPath);

//设置IMediaDet接口的文件关联

hr=pDet->put_Filename(openBSTR);

if(FAILED(hr))

returnNULL;

//从影片中检索视频流和音频流

longlStreams;

hr=pDet->get_OutputStreams(&lStreams);

if(FAILED(hr))

returnNULL;

//取出影片的视频流,因为帧的信息是保存在视频流中的

boolbFound=false;

for(inti=0;i<lStreams;i )


...{

GUIDmajor_type;

hr=pDet->put_CurrentStream(i);

if(SUCCEEDED(hr))

hr=pDet->get_StreamType(&major_type);

if(FAILED(hr))

break;

if(major_type==MEDIATYPE_Video)


...{

bFound=true;

break;

}

}

if(!bFound)

returnNULL;

longwidth=0,height=0;//存储位图的宽和高(单位:象素)

AM_MEDIA_TYPEmt;

hr=pDet->get_StreamMediaType(&mt);

if(SUCCEEDED(hr))


...{

if((mt.formattype==FORMAT_VideoInfo)&&

(mt.cbFormat>=sizeof(VIDEOINFOHEADER)))


...{

//得到VIDEOINFOHEADER结构指针,VIDEOINFOHEADER结构包含一些与视频

//有关的信息,其中含有BITMAPINFORHEADER结构

VIDEOINFOHEADER*pVih=(VIDEOINFOHEADER*)(mt.pbFormat);

width=pVih->bmiHeader.biWidth;

height=pVih->bmiHeader.biHeight;

if(height<0)height*=-1;

}

else

hr=VFW_E_INVALIDMEDIATYPE;

MyFreeMediaType(mt);//释放AM_MEDIA_TYPE结构

}

if(FAILED(hr))

returnNULL;

return(HANDLE)LookforSuitableMovieFrame(pDet,width,height,grayColorCountThreshold);

}


/**//**

*写入合适视频帧截图到磁盘

*@parampDetDirectShow的IMediaDet接口

*@paramwidth截图的长

*@paramheight截图的宽

*@paramgrayColorCountThreshold灰度颜色个数阈值

*/

HBITMAPLookforSuitableMovieFrame(IMediaDet*pDet,intwidth,intheight,intgrayColorCountThreshold)


...{

longsize;

doubletime=0.0;

doubletotaltime;

//获取整个视频的时间长度

pDet->get_StreamLength(&totaltime);

//每1秒,截取视频截图

for(time=0.0;time<totaltime;time =1.0)


...{

//获取bitmap的buffer大小

HRESULThr=pDet->GetBitmapBits(time,&size,0,width,height);

if(SUCCEEDED(hr))


...{

char*pBuffer=newchar[size];

if(!pBuffer)

returnNULL;

hr=pDet->GetBitmapBits(time,0,pBuffer,width,height);

if(SUCCEEDED(hr))


...{

//Findtheaddressofthestartoftheimagedata.

void*pData=pBuffer sizeof(BITMAPINFOHEADER);

if(IsSuitableMovieFrame(pData,width,height,grayColorCountThreshold))


...{

BITMAPINFOHEADER*bmih=(BITMAPINFOHEADER*)pBuffer;

HDChdcDest=GetDC(0);

BITMAPINFObmi;

ZeroMemory(&bmi,sizeof(BITMAPINFO));

CopyMemory(&(bmi.bmiHeader),bmih,sizeof(BITMAPINFOHEADER));

HBITMAPhBitmap=CreateDIBitmap(hdcDest,bmih,CBM_INIT,

pData,&bmi,DIB_RGB_COLORS);

delete[]pBuffer;

returnhBitmap;

}

}

delete[]pBuffer;

}

}

returnNULL;

}


/**//**

*检测一个位图是否是合适的视频截图

*@parampData位图的点色数组

*@paramwidth位图的长

*@paramheight位图的宽

*@paramgrayColorCountThreshold灰度颜色个数阈值

*/

boolIsSuitableMovieFrame(void*pData,intwidth,intheight,intgrayColorCountThreshold)


...{

BYTE*pixels=(BYTE*)pData;

intnumGrayColor=0;

intsize=width*height;

intgraycolor;

inti,j;

int*appearedcolors=newint[grayColorCountThreshold];

intnumappearedcolors=0;

for(i=0;i<size;i )


...{

//计算当前点的灰度值,采用的RGB转换灰度的公式是GRAY=(R G B)/3

graycolor=(pixels[i*3] pixels[i*3 1] pixels[i*3 2])/3;

//检测该灰度色是否之前出现过

for(j=0;j<numappearedcolors;j )


...{

if(graycolor==appearedcolors[j])

break;

}

if(j==numappearedcolors)//如果是新的灰度颜色值


...{

numappearedcolors ;

if(numappearedcolors==grayColorCountThreshold)//如果灰度颜色个数满足阈值


...{

delete[]appearedcolors;

returntrue;//返回信息,合适

}

else


...{

appearedcolors[j]=graycolor;//记录下该灰度颜色值

}

}

}

delete[]appearedcolors;

returnfalse;//返回信息,不合适

}

voidMyFreeMediaType(AM_MEDIA_TYPE&mt)


...{

if(mt.cbFormat!=0)


...{

CoTaskMemFree((PVOID)mt.pbFormat);

mt.cbFormat=0;

}

if(mt.pUnk!=NULL)


...{

mt.pUnk->Release();

mt.pUnk=NULL;

}

}
其中,为了使用DirectShow,我们除了需要windows.h外,还需要dshow.h,qedit.h和atlbase.h三个头文件,最后再加上一个strmiids.lib库文件。
接下来就开启Visual C# 2005 Express来做一个简单的界面程序。为什么选择C# 来开发界面程序呢?原因很简单,因为C#很简单,同时Visual C# 2005 Express这样免费又功能强大的工具可以使用。
界面程序很简单,就下面这个样子:<BR clear=all>
[img]http://school.cfan.com.cn/pro/c/h003/h19/img20061123015052224.jpg">
C# 部分调用前面写好的DLL函数,实现DDshow的抓图。 MovieGrabberDLL.cs源代码如下:

usingSystem;

usingSystem.Collections.Generic;

usingSystem.Text;

usingSystem.Runtime.InteropServices;

usingSystem.Drawing;

namespaceMovieGrabberCSharp


...{

classMovieGrabberDLL


...{

[DllImport("MovieGrabberDLL.dll")]

publicstaticexternintfnMovieGrabberDLL();

[DllImport("MovieGrabberDLL.dll")]

publicstaticexternIntPtrGrabMovieFrame(stringaPath,intgrayColorCountThreshold);

publicstaticBitmapGrabMovieFrameBitmap(stringaPath,intgrayColorCountThreshold)


...{

IntPtrhBitmap=GrabMovieFrame(aPath,grayColorCountThreshold);

if(hBitmap==IntPtr.Zero)

returnnull;

returnBitmap.FromHbitmap(hBitmap);

}

publicstaticBitmapGrabMovieFrameBitmap(stringaPath)


...{

returnGrabMovieFrameBitmap(aPath,8);

}

}

}
窗口类MainForm.cs的源代码如下:

usingSystem;

usingSystem.Collections.Generic;

usingSystem.ComponentModel;

usingSystem.Data;

usingSystem.Drawing;

usingSystem.Text;

usingSystem.Windows.Forms;

namespaceMovieGrabberCSharp


...{

publicpartialclassMainForm:Form


...{

publicMainForm()


...{

InitializeComponent();

}

privatevoidOpenMovieFilePathButton_Click(objectsender,EventArgse)


...{

OpenFileDialogdlg=newOpenFileDialog();

if(dlg.ShowDialog()==DialogResult.OK)


...{

MovieFilePathTextBox.Text=dlg.FileName;

}

}

privatevoidGrabberButton_Click(objectsender,EventArgse)


...{

Bitmapbitmap=MovieGrabberDLL.GrabMovieFrameBitmap(MovieFilePathTextBox.Text);

if(bitmap!=null)


...{

MessageBox.Show("抓图成功!");

GrabberPictureBox.SizeMode=PictureBoxSizeMode.StretchImage;

GrabberPictureBox.Image=bitmap;

GrabberPictureBox.Invalidate();

GrabberPictureBox.Refresh();

}

else


...{

MessageBox.Show("失败!");

}

}

privatevoidExitButton_Click(objectsender,EventArgse)


...{

this.Close();

}

}

}
编译完成后,我们使用Windows里面的一个intro.wmv视频文件来做测试,具体路径是:C:\WINDOWS\system32\oobe\imagee\intro.wmv。之所以选择这个文件作为视频测试文件,因为这个视频是大家安装完成后WINXP后都会自动启动的Windows XP的介绍视频,而且这个视频的开始部分是全黑,然后渐渐变亮,再到Windows XP的动画部分。如果用Windows自带的浏览器看微缩图显示,就是下面这个结果:
[img]http://school.cfan.com.cn/pro/c/h003/h19/img20061123015125320.jpg">
可以看到,这个intro.wmv的微缩图是完全的一张黑色图片,我们并不能看到任何关于视频文件有意义的内容。
下面启动我们刚才编写的Demo视频截图工具来截一下图片,同样这个视频文件,可以看到这个的结果。
[img]http://school.cfan.com.cn/pro/c/h003/h19/img20061123015126321.jpg">
其中,程序里面默认给出的灰度颜色个数阈值是8,那么就是说,至少图片要有8个不同的颜色灰度值才会截取,而之前的全黑,全白就自然滤过了。