使用 SampleGrabber 的缓冲模式进行视频预览和内存帧捕获。






4.91/5 (42投票s)
本文演示了如何使用缓冲模式下的 ISampleGrabber 接口从外部视频设备捕获视频预览和帧到内存中。

引言
我一直在寻找一个能够捕获外部视频设备的传入视频到内存缓冲区用于图像处理应用程序的项目,但找不到我需要的。Code Project 上现有的文章,使用 DirectShow 捕获帧作为位图的一个简单的视频处理框架,CAviCap 和 CFrameGrabber - AVICap 窗口的包装器,使用极简方法进行实时视频图像处理/帧捕获,使用 DirectShow 进行同时预览和视频捕获,都没有提供用于从流中捕获原始数据的首选缓冲模式。其中一些仅在 WinXP 和 Windows 2000 上进行了测试,这已经相当过时了,最新的也回到了 1999 年,或者捕获的是视频文件的样本等等。
我的电脑上安装了 Vista,并安装了 Windows SDK for Vista 和 DirectX SDK 2007。现在 Direct Show 已成为 Windows SDK 的一部分,如果您想使用 ISampleGrabber
进行原始数据捕获,还需要安装 DirectX SDK。我开发了一个简单的 MFC 应用程序,支持枚举现有视频设备、视频流预览、以缓冲模式捕获原始图像数据(具有所需的帧速率)、使用 GDI+ 将其显示到窗口,并将快照保存到 JPEG 文件。以此应用程序为骨架,您可以轻松开始进行图像/视频处理任务(视频编解码器开发、运动估计、边缘检测)。我将在稍后发布的脸部检测程序中使用它。
背景
您需要对 Direct Show 编程有一定的了解。可以参考上面提到的文章或 Windows Vista SDK 的帮助文档。最好也了解 COM 技术 - 可以参考文章 COM 入门 - 它是和如何使用它。
Using the Code
点击“枚举”按钮将枚举可用的视频设备并选择一个。指定所需的原始图像数据捕获间隔(以毫秒为单位,默认值为 1000,每秒捕获一次数据),然后点击“运行”按钮。左上角的那个小静态控件将预览视频输出,中间的大静态控件将显示捕获的原始图像。要拍摄快照,请双击中心捕获静态控件窗口,捕获的图像将保存到与 Snapshot X.jpg 同一目录下的磁盘上,其中 X 是图像的编号。
为了代码重用,我将 Sample Grabber 和 Video Capture 的实现放在了单独的文件中。我使用了 SDK 示例(SDK\Samples\Multimedia\DirectShow\Capture\PlayCap)中的 Video Capture 例程。我将它们封装成了易于外部应用程序使用的函数,并添加了视频设备枚举功能。
-
void vcGetCaptureDevices(CComboBox& adaptersBox);
-
HRESULT vcCaptureVideo(HWND msgWindow, HWND prvWindow, unsigned int devIndex = 1);
-
void vcStopCaptureVideo();
vcGetCaptureDevices()
函数用于枚举视频设备并将其添加到 ComboBox
中。vcCaptureVideo()
函数使用 msgWindow
应用程序(该应用程序将处理 Filter Graph 的通知消息)启动视频预览到 prvWindow
。devIndex
是要从中捕获数据的视频设备的索引。vcStopCaptureVideo
函数用于停止捕获视频数据。
Sample Grabber 在 vcCaptureVideo
函数中以这种方式添加到 Filter Graph 中
//...
hr = sgAddSampleGrabber(g_pGraph);
if (FAILED(hr)) {
Msg(TEXT("Couldn't add the SampleGrabber filter to the graph! hr=0x%x"), hr);
return hr;
}
hr = sgSetSampleGrabberMediaType();
if (FAILED(hr)) {
Msg(TEXT("Couldn't set the SampleGrabber media type! hr=0x%x"), hr);
return hr;
}
IBaseFilter* pGrabber = sgGetSampleGrabber();
// Render the preview pin on the video capture filter
// Use this instead of g_pGraph->RenderFile
hr = g_pCapture->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,
pSrcFilter, pGrabber/*NULL*/, NULL);
if (FAILED(hr)) {
Msg(TEXT("Couldn't render the video capture stream. hr=0x%x\r\n")
TEXT("The capture device may already be in use by another application.
\r\n\r\n")
TEXT("The sample will now close."), hr);
pSrcFilter->Release();
return hr;
}
hr = sgGetSampleGrabberMediaType();
//...
相应的 sg*
函数位于 samplegrab.cpp 文件中。
HRESULT sgAddSampleGrabber(IGraphBuilder *pGraph)
{
// Create the Sample Grabber.
HRESULT hr = CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER,
IID_IBaseFilter, (void**) & pGrabberFilter);
if (FAILED(hr)) {
return hr;
}
hr = pGraph->AddFilter(pGrabberFilter, L"Sample Grabber");
if (FAILED(hr)) {
return hr;
}
pGrabberFilter->QueryInterface(IID_ISampleGrabber, (void**)&pGrabber);
return hr;
}
HRESULT sgSetSampleGrabberMediaType()
{
AM_MEDIA_TYPE mt;
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
mt.majortype = MEDIATYPE_Video;
mt.subtype = MEDIASUBTYPE_RGB24;
HRESULT hr = pGrabber->SetMediaType(&mt);
if (FAILED(hr)) {
return hr;
}
//Do not stop the graph after one shot
hr = pGrabber->SetOneShot(FALSE);
//Use buffered mode
hr = pGrabber->SetBufferSamples(TRUE);
return hr;
}
IBaseFilter* sgGetSampleGrabber()
{
return pGrabberFilter;
}
HRESULT sgGetSampleGrabberMediaType()
{
AM_MEDIA_TYPE mt;
HRESULT hr = pGrabber->GetConnectedMediaType(&mt);
if (FAILED(hr)) {
return hr;
}
VIDEOINFOHEADER *pVih = (VIDEOINFOHEADER *)mt.pbFormat;
gChannels = pVih->bmiHeader.biBitCount / 8;
gWidth = pVih->bmiHeader.biWidth;
gHeight = pVih->bmiHeader.biHeight;
sgFreeMediaType(mt);
return hr;
}
视频预览和捕获的初始化实现在“运行”按钮点击例程 CVidCapDlg::OnBnClickedRunButton()
中,其中创建了一个定时器,用于查询 Sample Grabber 过滤器捕获的原始图像数据,这是使用它的首选方式,如 SDK 帮助文档“使用 Sample Grabber”主题中所述。
要抓取原始图像数据,请使用 samplegrab.h 中的以下函数:
-
unsigned char* sgGrabData();
-
Gdiplus::Bitmap* sgGetBitmap();
-
long sgGetBufferSize();
第一个函数返回指向包含原始图像数据的缓冲区的指针,如果 Sample Grabber 尚未就绪,则返回 NULL
。请注意,Sample Grabber 过滤器以倒置的位图(底部向上)填充该缓冲区。sgGetBitmap()
函数在调用 sgGrabData()
后返回一个填充了原始图像数据的 GDI+ bitmap
对象,如果数据未被捕获则返回 NULL
。我使用 sgFlipUpDown()
函数在将原始图像复制到 Bitmap
之前进行上下翻转。您应该按照此顺序使用这些函数,首先调用 sgGrabData()
获取原始图像,如果您想要一个 Bitmap
对象来在窗口中绘制它,之后再调用 sgGetBitmap()
。要获取原始图像缓冲区的长度,请调用 sgGetBufferSize()
。
抓取代码如下所示:
unsigned char* sgGrabData()
{
HRESULT hr;
if (pGrabber == 0)
return 0;
long Size = 0;
hr = pGrabber->GetCurrentBuffer(&Size, NULL);
if (FAILED(hr))
return 0;
else if (Size != pBufferSize) {
pBufferSize = Size;
if (pBuffer != 0)
delete[] pBuffer;
pBuffer = new unsigned char[pBufferSize];
}
hr = pGrabber->GetCurrentBuffer(&pBufferSize, (long*)pBuffer);
if (FAILED(hr))
return 0;
else {
sgFlipUpDown(pBuffer);
return pBuffer;
}
}
要确定图像尺寸,请调用这些函数:
-
unsigned int sgGetDataWidth();
-
unsigned int sgGetDataHeight();
-
unsigned int sgGetDataChannels();