使用 DirectX 和 .NET 捕获样本






4.70/5 (44投票s)
2003年8月11日
5分钟阅读

505536

45692
同时捕获视频和帧的解决方案。
引言
本文档旨在解释如何同时捕获视频和帧。它基于 NETMaster 的 SampleGrabber 程序和 Brian Low 的 DirectX.Capture。这一切的根源是 NETMaster 的出色工作,DirectShow.NET,SampleGrabber 只是一个示例应用程序。我说出色是因为我几乎完全理解了其他项目或类,但 DirectShow.NET 远远超出了我的知识范围。
在开始讲解之前,我必须说明我不是一个专业的程序员,这项工作是我写过的第一个比较认真的程序之一,所以可能有人能找到更好的解决方案来完成同样的事情。
至此,你可能已经猜到我的母语不是英语,而是西班牙语,很抱歉我的语法错误。我使用的是 Visual Studio .NET 西班牙语版本编写的代码,因此自动生成的注释是西班牙语,但我的注释是英语。
VB 代码
首先,要运行演示程序,您需要安装 NET Framework 1.1。如果您还没有安装,请检查 Windows Update。
在这里我想解释一下 VB 项目的工作原理,我将在下一节中解释 sampleGrabber 过滤器。主项目是 CapSample,它有三个窗体;MW、AddCam 和 CW,以及一个模块 ModCap。让我们开始描述。
"MW" 代表 "Main Window"(主窗口),它是初始窗体。创建时,它会创建一个新的窗体 AddCam,该窗体负责选择可用摄像头。如果点击 OK 按钮,AddCam 将连接到摄像头。连接是通过使用 DirectX.Capture 的捕获类完成的,如前所述,我将在下一节讨论我对该类的修改。
一旦连接了摄像头(AddCam 完成,如果点击 Cancel 会发生错误),MW 会继续初始化并设置预览板(我不确定这个词,在西班牙语中这个控件叫做“Panel”),然后它会为捕获类的 FrameCaptureComplete
事件添加一个处理程序并初始化计数器。最后创建 CW(它代表 "Configuration Window" - 配置窗口),负责配置摄像头。通过点击 OK,ConfParamCam()
将设置选定的参数,然后它将设置捕获目录(如果不存在,会发生错误),并捕获一帧并通过 CaptureInformation.CaptureInfo.CaptureFrame()
渲染预览流。
配置好摄像头后,您可以点击 Frame 按钮捕获帧,点击 Start 按钮开始视频捕获。当您点击 Stop 时,它会停止捕获,但会调用 ConfParamCam()
来设置先前选择的参数,并调用 PrepareCam()
来递增捕获文件的名称。关于 VB 代码我就说这么多。让我们开始讲捕获类。
DirectX.Capture 类的修改
我的捕获类是 Brian Low 的捕获类和 NETMaster 的 SampleGrabberNET 程序的混合体。
在 createGraph()
中,我添加了以下代码
AMMediaType media = new AMMediaType();
media.majorType= MediaType.Video;
media.subType = MediaSubType.RGB24;
media.formatType = FormatType.VideoInfo;
hr = sampGrabber.SetMediaType( media );
if( hr<0 ) Marshal.ThrowExceptionForHR( hr );
和
mediaEvt = (IMediaEventEx) graphBuilder;
baseGrabFlt = (IBaseFilter) sampGrabber;
hr = graphBuilder.AddFilter( baseGrabFlt, "DS.NET Grabber" );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
第一块是为了能使用该过滤器,第二块是过滤器本身。重要的事情出现在 renderGraph()
中,在向您展示代码之前,我必须说明所有 WDM 设备都有两个 PIN,捕获和预览,有时只有一个(捕获),但 Smart Tee 过滤器会将捕获 PIN 转换为捕获和预览,如果需要,智能连接会为我们添加。因此,我将使用捕获 PIN 将视频捕获到文件,使用预览 PIN 捕获帧。RenderGraph()
分为两个 if 语句,一个是为了准备所有过滤器将视频捕获到文件,由 wantCaptureRendered
控制,另一个是为了渲染预览流,我添加了变量 renderStream
来避免 Brian Low 原始类的正常工作,因为当 baseGrabFlt
设置好时,我不知道如何配置摄像头,并且在捕获停止后,摄像头会丢失其配置参数,直到使用 renderStream
更新这些参数,我才停止渲染。
第一个 if 与 Brian Low 类中的第一个 if 相同,第二个看起来是这样的。
// Render preview stream and launch the baseGrabFlt to capture frames
// ==================================================================
if ( wantPreviewRendered && renderStream && !isPreviewRendered )
{
/// Render preview (video.PinPreview -> baseGrabFlt -> renderer)
/// At this point intelligent connect is used, because my
/// webcams don't have a preview pin and
/// a capture pin, so Smart Tee filter will be used.
/// I have tested it using GraphEdit.
/// I can type hr = captureGraphBuilder.RenderStream( ref cat,
/// ref med, videoDeviceFilter, null, baseGrabFlt);
/// because baseGrabFlt is a transform filter,
/// like videoCompressorFilter.
cat = PinCategory.Preview;
med = MediaType.Video;
hr = captureGraphBuilder.RenderStream( ref cat, ref med,
videoDeviceFilter, baseGrabFlt, null );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
// Get the IVideoWindow interface
videoWindow = (IVideoWindow) graphBuilder;
// Set the video window to be a child of the main window
hr = videoWindow.put_Owner( previewWindow.Handle );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
// Set video window style
hr = videoWindow.put_WindowStyle( WS_CHILD | WS_CLIPCHILDREN
| WS_CLIPSIBLINGS);
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
// Position video window in client rect of owner window
previewWindow.Resize += new EventHandler( onPreviewWindowResize );
onPreviewWindowResize( this, null );
// Make the video window visible, now that it is properly positioned
hr = videoWindow.put_Visible( DsHlp.OATRUE );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
hr = mediaEvt.SetNotifyWindow( this.Handle,
WM_GRAPHNOTIFY, IntPtr.Zero );
if( hr < 0 )
Marshal.ThrowExceptionForHR( hr );
isPreviewRendered = true;
didSomething = true;
// Begin Configuration of SampGrabber <<<<<<-----------------
AMMediaType media = new AMMediaType();
hr = sampGrabber.GetConnectedMediaType( media );
if( hr < 0 )
Marshal.ThrowExceptionForHR( hr );
if( (media.formatType != FormatType.VideoInfo) ||
(media.formatPtr == IntPtr.Zero) )
throw new NotSupportedException( "Unknown Grabber Media Format" );
videoInfoHeader = (VideoInfoHeader) Marshal.PtrToStructure(
media.formatPtr, typeof(VideoInfoHeader) );
Marshal.FreeCoTaskMem( media.formatPtr );
media.formatPtr = IntPtr.Zero;
hr = sampGrabber.SetBufferSamples( false );
if( hr == 0 )
hr = sampGrabber.SetOneShot( false );
if( hr == 0 )
hr = sampGrabber.SetCallback( null, 0 );
if( hr < 0 )
Marshal.ThrowExceptionForHR( hr );
// Finish Configuration of SampGrabber <<<<<<----------------
}
if ( didSomething )
graphState = GraphState.Rendered;
在这四行中,baseGrabFlt
被设置好了,请注意我使用 baseGrabFlt
作为转换过滤器。
cat = PinCategory.Preview;
med = MediaType.Video;
hr = captureGraphBuilder.RenderStream( ref cat, ref med,
videoDeviceFilter, baseGrabFlt, null );
if( hr < 0 ) Marshal.ThrowExceptionForHR( hr );
关于 sampleGrabber 的配置,我只注释了以下几行。
hr = sampGrabber.SetBufferSamples( false );
if( hr == 0 )
hr = sampGrabber.SetOneShot( false );
if( hr == 0 )
hr = sampGrabber.SetCallback( null, 0 );
if( hr < 0 )
Marshal.ThrowExceptionForHR( hr );
SampleGrabber 有三种不同的方法来捕获帧,我选择了 SetCallBack
,因为它更有用,当调用一个函数时,sampleGrabber 将捕获一个帧。
我们需要更多东西来完成帧的捕获,这由以下代码块完成。
void OnCaptureDone()
{
int hr;
if( sampGrabber == null )
return;
hr = sampGrabber.SetCallback( null, 0 );
int w = videoInfoHeader.BmiHeader.Width;
int h = videoInfoHeader.BmiHeader.Height;
if( ((w & 0x03) != 0) || (w < 32) || (w > 4096)
|| (h < 32) || (h > 4096) )
return;
int stride = w * 3;
GCHandle handle = GCHandle.Alloc( savedArray, GCHandleType.Pinned );
int scan0 = (int) handle.AddrOfPinnedObject();
scan0 += (h - 1) * stride;
Bitmap b = new Bitmap( w, h, -stride,
PixelFormat.Format24bppRgb, (IntPtr) scan0 );
handle.Free();
savedArray = null;
ImageCaptured.Image = b;
//Launch the event
FrameCaptureComplete(ImageCaptured);
return;
}
protected override void WndProc( ref Message m )
{
if( m.Msg == WM_GRAPHNOTIFY )
{
if( mediaEvt != null )
OnGraphNotify();
return;
}
base.WndProc( ref m );
}
// graph event (WM_GRAPHNOTIFY) handler
void OnGraphNotify()
{
DsEvCode code;
int p1, p2, hr = 0;
do
{
hr = mediaEvt.GetEvent( out code, out p1, out p2, 0 );
if( hr < 0 )
break;
hr = mediaEvt.FreeEventParams( code, p1, p2 );
}
while( hr == 0 );
}
int ISampleGrabberCB.SampleCB( double SampleTime,
IMediaSample pSample )
{
return 0;
}
int ISampleGrabberCB.BufferCB(double SampleTime,
IntPtr pBuffer, int BufferLen )
{
if( captured || (savedArray == null) )
{
return 0;
}
captured = true;
bufferedSize = BufferLen;
if( (pBuffer != IntPtr.Zero) && (BufferLen > 1000)
&& (BufferLen <= savedArray.Length) )
Marshal.Copy( pBuffer, savedArray, 0, BufferLen );
try
{
this.BeginInvoke( new CaptureDone( this.OnCaptureDone ) );
}
catch (ThreadInterruptedException e)
{
MessageBox.Show(e.Message);
}
catch (Exception we)
{
MessageBox.Show(we.Message);
}
return 0;
}
从头开始,ISampleGraberCB.BufferCB(...)
是一个填充缓冲区的功能,当缓冲区满了时,它就有了一帧,并调用 OnCaptureDone()
,这是创建图像的函数。当图像构建完成后,它会发出一个事件发送给该事件的接收者。
ISampleGrabberCB.SampleCB(...)
必须被定义,因为它没有被使用。对我来说,另外两个函数是黑魔法,我能猜到它们在做什么,但我不理解。
最后是调用以捕获帧的方法。
public void CaptureFrame()
{
int hr;
if(firstFrame)
{
assertStopped();
// Re-render the graph (if necessary)
renderStream = true;
renderGraph();
// Start the filter graph: begin capturing
hr = mediaControl.Run();
if ( hr != 0 ) Marshal.ThrowExceptionForHR( hr );
firstFrame = false;
}
captured = false;
if(savedArray == null )
{
int size = videoInfoHeader.BmiHeader.ImageSize;
if( (size<1000) || (size > 16000000) )
return;
savedArray = new byte[ size + 64000];
}
hr = sampGrabber.SetCallback( this, 1 );
}
firstFrame
是为了避免多次执行下一行。
hr = mediaControl.Run()
我认为描述已经完成了。我希望它对您有用 :-)
系统要求
我一直在使用 AMDXP@1500,512Mb RAM 和 WIN2k。我安装了 DirectX 9.0,但我想,它也可以与 DirectX 8.1 一起工作。
作为摄像头,我使用了 Philips ToUcam Pro 和 Creative Video Blaster Web Cam 5,没有出现问题。我还安装了 Pinacle PCTV Pro,但它无法将视频捕获到文件。我不知道为什么,但目前我没有足够的时间去查找问题的原因。
反馈和改进
我会尽量关注论坛,如果您需要帮助,我很乐意为您效劳。我认为我们必须共享代码来提高我们程序的性能,并帮助人们做新的事情,例如,如果我没有读过 Brian Low 的代码,我就永远不会写这段代码。谢谢!