一个简单的视频处理框架,通过DirectShow抓取帧作为位图






4.88/5 (44投票s)
本文展示了如何轻松有效地将视频中的所有帧提取为位图。
引言
我最近遇到了处理视频片段并提取一些特征以进行进一步分析的任务。基本思想很简单:只需将视频的每一帧提取到位图中,然后做任何我想做的事情。我不想被视频解码的复杂性所困扰。我只是想使用一个现有且易于使用的框架来提取帧。这时,广泛使用的DirectShow框架进入了我的考虑范围。
我在网上搜索了一些解决方案,确实找到了一些。不幸的是,我找到的方案大多只专注于抓取单个帧。这对于我的任务来说效率太低了。这些方法的典型提取速度在我的机器上大约是每秒5到8帧,这将在本文后面进一步讨论。请记住,视频的典型播放速度是每秒25帧!因此,我不得不深入研究DirectShow框架并自己编写一些东西。幸运的是,我最终在DirectShow示例视频ruby.avi上达到了每秒300帧以上的速度,这个视频的帧图像尺寸有点小,并且压缩程度不高:-)。
背景
以下是我发现的可以——或者应该——完成任务的方法。
IMediaDet
这可能是最易于使用的解决方案。有一篇文章演示了这一点,不幸的是它是用中文写的。我的文章的读者可能无法理解那篇文章。这种方法的主要思想是:
- 创建
IMediaDet
接口的实例。 - 打开一个视频文件。
- 使用
get_OutputStreams()
枚举视频中的所有流并找到视频流。 - 获取有关视频流的信息,例如持续时间或帧图像大小。
- 使用
GetBitmapBits()
在指定媒体时间获取帧图像,或者如果您只是将该帧的快照保存到文件中,则使用WriteBitmapBits()
。
这可能是枚举视频文件所有帧最简单的方法。不幸的是,正如我之前提到的,它的性能并不令人满意。
ISampleGrabber,一次性模式
有一篇好文章演示了这种方法。它比IMediaDet
使用起来并没有困难多少,但其性能也没有好多少。使用它优于IMediaDet
的优点是:
- 易于使用。
- 易于过程控制,因为您可以自由跳转到视频的任何部分或只是完成工作。
因此,这适用于仅从视频中获取一些快照。
编写转换过滤器
通过编写自己的转换过滤器,您可以在DirectShow框架内完成您的工作。这种方法是最强大和最有效的方法。另一方面,您需要非常了解DirectShow才能使其工作。实现转换过滤器也相当困难。有几篇MSDN文章解释了如何编写转换过滤器,以及一个实现示例抓取器原地转换过滤器的示例。请注意,此处演示的示例抓取器与ISampleGrabber
非常相似,以回调模式而不是一次性模式工作。然而,这对我来说似乎太难了,而且要考虑的事情太多了,因为我只是DirectShow的新手。最后,我决定不使用这种方法。
DirectShow的替代方案
据我所知,还有另一个广泛使用的框架可能完成我的任务:OpenCV。OpenCV是一个著名的开源跨平台计算机视觉库,其中视频任务只是其中一小部分。我最初遇到了一些构建问题,但也许我应该稍后研究它。
Using the Code
我的框架
最后,我选择了回调模式而不是一次性模式的ISampleGrabber
。也就是说,我运行DirectShow图表,它连续解码视频文件的视频帧。每次解码一帧,DirectShow都会调用一个用户定义的回调,提供图像数据。分析工作可以在这里完成。
图表设置
视频分析框架应该包含额外的组件,因此用于分析的DirectShow图表如下:在源过滤器之后立即添加一个ISampleGrabber
,它负责解码工作。ISampleGrabber
之后是一个NullRenderer
,它只是不做进一步的渲染工作。我在互联网上找到的最常用的连接过滤器的代码如下:
HRESULT ConnectFilters(IGraphBuilder *pGraph,
IBaseFilter *pFirst, IBaseFilter *pSecond)
{
IPin *pOut = NULL, *pIn = NULL;
HRESULT hr = GetPin(pFirst, PINDIR_OUTPUT, &pOut);
if (FAILED(hr)) return hr;
hr = GetPin(pSecond, PINDIR_INPUT, &pIn);
if (FAILED(hr))
{
pOut->Release();
return E_FAIL;
}
hr = pGraph->Connect(pOut, pIn);
pIn->Release();
pOut->Release();
return hr;
}
然而,对于某些视频文件,行pGraph->Connect(pOut, pIn)
会失败。我检查后发现,这类视频文件的源过滤器有一个以上的输出引脚,而GetPin()
只返回第一个。返回的输出引脚可能不是输出视频帧数据的引脚。因此,我修改了代码,使其适用于DirectShow支持的所有视频文件:
HRESULT ConnectFilters(IGraphBuilder *pGraph,
IBaseFilter *pFirst, IBaseFilter *pSecond)
{
IPin *pOut = NULL, *pIn = NULL;
HRESULT hr = GetPin(pSecond, PINDIR_INPUT, &pIn);
if (FAILED(hr)) return hr;
// Try each output pin of pFirst
IEnumPins *pEnum;
pFirst->EnumPins(&pEnum);
while(pEnum->Next(1, &pOut, 0) == S_OK)
{
PIN_DIRECTION PinDirThis;
pOut->QueryDirection(&PinDirThis);
if (PINDIR_OUTPUT == PinDirThis)
{
hr = pGraph->Connect(pOut, pIn);
if(!FAILED(hr))
{
break;
}
}
pOut->Release();
}
pEnum->Release();
pIn->Release();
pOut->Release();
return hr;
}
然而,如果错误地打开一个音频文件(例如MP3),上面的代码就会阻塞,ConnectFilters()
将永远不会返回。我不知道如何避免这种情况 :-(。
控制分析过程
我的框架中分析过程的控制与使用DirectShow编写视频播放器非常相似。图表设置完成后,调用pControl->Run()
开始分析。之后,您可以调用pEvent->WaitForCompletion(INFINITE, &evCode)
等待分析完成。或者,您可以调用IMediaControl
中的其他控制方法来暂停或停止分析过程。
分析视频
在我的框架中,大部分内容都封装在CVideoAnaDoc
中。分析视频时,您所要做的就是将自己的分析代码写入CVideoAnaDoc::ProcessFrame()
中。CVideoAnaDoc::ProcessFrame()
在每次新帧到来时由框架调用,将其帧图像数据作为参数提供。示例如下:
HRESULT CVideoAnaDoc::ProcessFrame(double SampleTime,
BYTE *pBuffer, long nBufferLen)
{
// TODO: Put the frame processing code here
// Keep in mind that code here is executed within another thread,
// so do consider the data access problem among threads
// SampleTime is real media time of the frame sample
// pBuffer is the DIBits of the frame sample image
// The following code demonstrates how to save
// a snapshot to BMP file every 10 frames
if(0 == m_nCurFrame % 10)
{
CString strFilename;
strFilename.Format("C:\\Snap%d.bmp", m_nCurFrame / 10);
FILE *pfSnap = fopen(strFilename, "wb");
// BITMAPFILEHEADER
fwrite(&m_Bfh, sizeof(m_Bfh), 1, pfSnap);
// BITMAPINFOHEADER
fwrite(&m_Bih, sizeof(m_Bih), 1, pfSnap);
fwrite(pBuffer, nBufferLen, 1, pfSnap); // DIBits
fclose(pfSnap);
}
// The following code demonstrates how
// to get rgb values of a specified pixel
// You can write a loop to examine all pixels
// Keep in mind the pixel data is stored
// from bottom to top in pBuffer
int x = 0;
int y = 0;
// # of bytes per line
int nLineBytes = (m_Bih.biWidth * 24 + 31) / 32 * 4;
BYTE *pLine = pBuffer + (m_Bih.biHeight - y - 1) * nLineBytes;
BYTE *pPixel = pLine + 3 * x;
BYTE B = *pPixel;
BYTE G = *(pPixel + 1);
BYTE R = *(pPixel + 2);
// m_nCurFrame indicates which frame is being processed
m_nCurFrame++;
return S_OK;
}
关于演示项目
我提供的演示项目采用Visual Studio 8.0(Visual Studio 2005)格式。DirectShow SDK现在包含在Platform SDK中,以前包含在DirectX SDK中。我从Microsoft下载的Platform SDK不再支持Visual Studio 6.0(参考此处),因此我无法提供Visual Studio 6.0版本。我的系统上也没有安装Visual Studio 7.x,所以也没有Visual Studio 7.x版本。但是,如果您在Visual Studio 7.x中创建一个合适的项目,并将我项目中的所有代码复制到其中,它应该可以工作。
历史
- 版本3,2008-03-02
- 修复了可能导致资源泄露的错误。
- 现在使用通知而不是线程等待处理完成。
- 内存管理方面的一个小更新。
- 版本2,2007-06-12
- 修复了导致资源泄漏的错误。(感谢bowler_jackie_lin)
- 修复了可能导致在分析开始时检索到错误的帧计数的错误。
- 版本1,2006-07-02
- 首次发布。