前几天在帮师兄做一个视频截图的模块,采用了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接口来截视频流内的截图的:
使用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 "
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
/**
* 抓取视频的截图
* @param aPath 视频文件的位置
* @return
*/
MOVIEGRABBERDLL_API HANDLE GrabMovieFrame(LPCTSTR aPath, int grayColorCountThreshold)
{
HRESULT hr;
// 定义IMediaDet接口实例
CComPtr< IMediaDet > pDet;
hr = pDet.CoCreateInstance(__uuidof(MediaDet));
if (FAILED(hr))
return NULL;
// 将影片文件名转换成BSTR类型
CComBSTR openBSTR(aPath);
// 设置IMediaDet接口的文件关联
hr = pDet->put_Filename(openBSTR);
if (FAILED(hr))
return NULL;
// 从影片中检索视频流和音频流
long lStreams;
hr = pDet->get_OutputStreams(&lStreams);
if (FAILED(hr))
return NULL;
// 取出影片的视频流,因为帧的信息是保存在视频流中的
bool bFound = false;
for (int i=0; i<lStreams; i++)
{
GUID major_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)
return NULL;
long width = 0, height = 0; // 存储位图的宽和高(单位:象素)
AM_MEDIA_TYPE mt;
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))
return NULL;
return (HANDLE)LookforSuitableMovieFrame(pDet,width,height,grayColorCountThreshold);
}
/**
* 写入合适视频帧截图到磁盘
* @param pDet DirectShow的IMediaDet接口
* @param width 截图的长
* @param height 截图的宽
* @param grayColorCountThreshold 灰度颜色个数阈值
*/
HBITMAP LookforSuitableMovieFrame(IMediaDet * pDet, int width, int height, int grayColorCountThreshold)
{
long size;
double time = 0.0;
double totaltime;
// 获取整个视频的时间长度
pDet->get_StreamLength(&totaltime);
// 每1秒,截取视频截图
for(time=0.0; time <totaltime; time+= 1.0)
{
// 获取bitmap的buffer大小
HRESULT hr = pDet->GetBitmapBits(time, &size, 0, width, height);
if (SUCCEEDED(hr))
{
char *pBuffer = new char[size];
if (!pBuffer)
return NULL;
hr = pDet->GetBitmapBits(time, 0, pBuffer, width, height);
if (SUCCEEDED(hr))
{
// Find the address of the start of the image data.
void *pData = pBuffer + sizeof(BITMAPINFOHEADER);
if(IsSuitableMovieFrame(pData,width,height,grayColorCountThreshold))
{
BITMAPINFOHEADER *bmih = (BITMAPINFOHEADER*)pBuffer;
HDC hdcDest = GetDC(0);
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(BITMAPINFO));
CopyMemory(&(bmi.bmiHeader), bmih, sizeof(BITMAPINFOHEADER));
HBITMAP hBitmap = CreateDIBitmap(hdcDest, bmih, CBM_INIT,
pData, &bmi, DIB_RGB_COLORS);
delete[] pBuffer;
return hBitmap;
}
}
delete[] pBuffer;
}
}
return NULL;
}
/**
* 检测一个位图是否是合适的视频截图
* @param pData 位图的点色数组
* @param width 位图的长
* @param height 位图的宽
* @param grayColorCountThreshold 灰度颜色个数阈值
*/
bool IsSuitableMovieFrame( void * pData, int width, int height, int grayColorCountThreshold)
{
BYTE* pixels = (BYTE*)pData;
int numGrayColor = 0;
int size = width*height;
int graycolor;
int i,j;
int* appearedcolors = new int[grayColorCountThreshold];
int numappearedcolors = 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;
return true; // 返回信息,合适
}
else
{
appearedcolors[j] = graycolor; // 记录下该灰度颜色值
}
}
}
delete[] appearedcolors;
return false; // 返回信息,不合适
}
void MyFreeMediaType(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;
}
}
//
#include " stdafx.h "
#include " MovieGrabberDLL.h "
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
/**
* 抓取视频的截图
* @param aPath 视频文件的位置
* @return
*/
MOVIEGRABBERDLL_API HANDLE GrabMovieFrame(LPCTSTR aPath, int grayColorCountThreshold)
{
HRESULT hr;
// 定义IMediaDet接口实例
CComPtr< IMediaDet > pDet;
hr = pDet.CoCreateInstance(__uuidof(MediaDet));
if (FAILED(hr))
return NULL;
// 将影片文件名转换成BSTR类型
CComBSTR openBSTR(aPath);
// 设置IMediaDet接口的文件关联
hr = pDet->put_Filename(openBSTR);
if (FAILED(hr))
return NULL;
// 从影片中检索视频流和音频流
long lStreams;
hr = pDet->get_OutputStreams(&lStreams);
if (FAILED(hr))
return NULL;
// 取出影片的视频流,因为帧的信息是保存在视频流中的
bool bFound = false;
for (int i=0; i<lStreams; i++)
{
GUID major_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)
return NULL;
long width = 0, height = 0; // 存储位图的宽和高(单位:象素)
AM_MEDIA_TYPE mt;
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))
return NULL;
return (HANDLE)LookforSuitableMovieFrame(pDet,width,height,grayColorCountThreshold);
}
/**
* 写入合适视频帧截图到磁盘
* @param pDet DirectShow的IMediaDet接口
* @param width 截图的长
* @param height 截图的宽
* @param grayColorCountThreshold 灰度颜色个数阈值
*/
HBITMAP LookforSuitableMovieFrame(IMediaDet * pDet, int width, int height, int grayColorCountThreshold)
{
long size;
double time = 0.0;
double totaltime;
// 获取整个视频的时间长度
pDet->get_StreamLength(&totaltime);
// 每1秒,截取视频截图
for(time=0.0; time <totaltime; time+= 1.0)
{
// 获取bitmap的buffer大小
HRESULT hr = pDet->GetBitmapBits(time, &size, 0, width, height);
if (SUCCEEDED(hr))
{
char *pBuffer = new char[size];
if (!pBuffer)
return NULL;
hr = pDet->GetBitmapBits(time, 0, pBuffer, width, height);
if (SUCCEEDED(hr))
{
// Find the address of the start of the image data.
void *pData = pBuffer + sizeof(BITMAPINFOHEADER);
if(IsSuitableMovieFrame(pData,width,height,grayColorCountThreshold))
{
BITMAPINFOHEADER *bmih = (BITMAPINFOHEADER*)pBuffer;
HDC hdcDest = GetDC(0);
BITMAPINFO bmi;
ZeroMemory(&bmi, sizeof(BITMAPINFO));
CopyMemory(&(bmi.bmiHeader), bmih, sizeof(BITMAPINFOHEADER));
HBITMAP hBitmap = CreateDIBitmap(hdcDest, bmih, CBM_INIT,
pData, &bmi, DIB_RGB_COLORS);
delete[] pBuffer;
return hBitmap;
}
}
delete[] pBuffer;
}
}
return NULL;
}
/**
* 检测一个位图是否是合适的视频截图
* @param pData 位图的点色数组
* @param width 位图的长
* @param height 位图的宽
* @param grayColorCountThreshold 灰度颜色个数阈值
*/
bool IsSuitableMovieFrame( void * pData, int width, int height, int grayColorCountThreshold)
{
BYTE* pixels = (BYTE*)pData;
int numGrayColor = 0;
int size = width*height;
int graycolor;
int i,j;
int* appearedcolors = new int[grayColorCountThreshold];
int numappearedcolors = 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;
return true; // 返回信息,合适
}
else
{
appearedcolors[j] = graycolor; // 记录下该灰度颜色值
}
}
}
delete[] appearedcolors;
return false; // 返回信息,不合适
}
void MyFreeMediaType(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这样免费又功能强大的工具可以使用。
界面程序很简单,就下面这个样子:
C# 部分调用前面写好的DLL函数,实现DDshow的抓图。 MovieGrabberDLL.cs源代码如下:
using
System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
namespace MovieGrabberCSharp
{
class MovieGrabberDLL
{
[DllImport("MovieGrabberDLL.dll")]
public static extern int fnMovieGrabberDLL();
[DllImport("MovieGrabberDLL.dll")]
public static extern IntPtr GrabMovieFrame(string aPath, int grayColorCountThreshold);
public static Bitmap GrabMovieFrameBitmap(string aPath,int grayColorCountThreshold)
{
IntPtr hBitmap = GrabMovieFrame(aPath, grayColorCountThreshold);
if(hBitmap == IntPtr.Zero)
return null;
return Bitmap.FromHbitmap(hBitmap);
}
public static Bitmap GrabMovieFrameBitmap(string aPath)
{
return GrabMovieFrameBitmap(aPath, 8);
}
}
}
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
namespace MovieGrabberCSharp
{
class MovieGrabberDLL
{
[DllImport("MovieGrabberDLL.dll")]
public static extern int fnMovieGrabberDLL();
[DllImport("MovieGrabberDLL.dll")]
public static extern IntPtr GrabMovieFrame(string aPath, int grayColorCountThreshold);
public static Bitmap GrabMovieFrameBitmap(string aPath,int grayColorCountThreshold)
{
IntPtr hBitmap = GrabMovieFrame(aPath, grayColorCountThreshold);
if(hBitmap == IntPtr.Zero)
return null;
return Bitmap.FromHbitmap(hBitmap);
}
public static Bitmap GrabMovieFrameBitmap(string aPath)
{
return GrabMovieFrameBitmap(aPath, 8);
}
}
}
窗口类MainForm.cs的源代码如下:
using
System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace MovieGrabberCSharp
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void OpenMovieFilePathButton_Click(object sender, EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
if (dlg.ShowDialog() == DialogResult.OK)
{
MovieFilePathTextBox.Text = dlg.FileName;
}
}
private void GrabberButton_Click(object sender, EventArgs e)
{
Bitmap bitmap = MovieGrabberDLL.GrabMovieFrameBitmap(MovieFilePathTextBox.Text);
if (bitmap != null)
{
MessageBox.Show("抓图成功!");
GrabberPictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
GrabberPictureBox.Image = bitmap;
GrabberPictureBox.Invalidate();
GrabberPictureBox.Refresh();
}
else
{
MessageBox.Show("失败!");
}
}
private void ExitButton_Click(object sender, EventArgs e)
{
this.Close();
}
}
}
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace MovieGrabberCSharp
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void OpenMovieFilePathButton_Click(object sender, EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog();
if (dlg.ShowDialog() == DialogResult.OK)
{
MovieFilePathTextBox.Text = dlg.FileName;
}
}
private void GrabberButton_Click(object sender, EventArgs e)
{
Bitmap bitmap = MovieGrabberDLL.GrabMovieFrameBitmap(MovieFilePathTextBox.Text);
if (bitmap != null)
{
MessageBox.Show("抓图成功!");
GrabberPictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
GrabberPictureBox.Image = bitmap;
GrabberPictureBox.Invalidate();
GrabberPictureBox.Refresh();
}
else
{
MessageBox.Show("失败!");
}
}
private void ExitButton_Click(object sender, EventArgs e)
{
this.Close();
}
}
}
编 译完成后,我们使用Windows里面的一个intro.wmv视频文件来做测试,具体路径是:C:/WINDOWS/system32/oobe/ imagee/intro.wmv。之所以选择这个文件作为视频测试文件,因为这个视频是大家安装完成后WINXP后都会自动启动的Windows XP的介绍视频,而且这个视频的开始部分是全黑,然后渐渐变亮,再到Windows XP的动画部分。如果用Windows自带的浏览器看微缩图显示,就是下面这个结果:
可以看到,这个intro.wmv的微缩图是完全的一张黑色图片,我们并不能看到任何关于视频文件有意义的内容。
下面启动我们刚才编写的Demo视频截图工具来截一下图片,同样这个视频文件,可以看到这个的结果。
其中,程序里面默认给出的灰度颜色个数阈值是8,那么就是说,至少图片要有8个不同的颜色灰度值才会截取,而之前的全黑,全白就自然滤过了。