Webcamera, Multithreading and VFW






4.79/5 (22投票s)
一篇关于多线程环境中摄像头帧捕获的文章。

引言
有几种方法可以捕获和处理网络摄像头图像:WIA、DirectShow、VFW... 网上有很多 C# VFW 示例,其中大多数使用.NET 剪贴板将每个帧的数据从缓冲区传输到Bitmap
可识别的对象。不幸的是,这使得多线程不可用,并降低了 FPS(每秒帧数)。本机 Win32 剪贴板和多线程可以解决速度问题,但我认为这不是最优雅的解决方案,应该还有其他方法可以从 Avicap 获取帧。我参考了 MSDN(参见上面的 VFW 链接)并发现了函数回调是可用的。本文将分步介绍如何在多线程环境中,使用avicap32.dll(VFW)捕获帧。
想法
这是你在网上看到很多示例中使用的一种方法
|
这是我使用过的一种方法
|
API
[DllImport("avicap32.dll", EntryPoint = "capCreateCaptureWindow")]
static extern int capCreateCaptureWindow(string lpszWindowName,
int dwStyle, int X, int Y,
int nWidth, int nHeight, int hwndParent, int nID);
capCreateCaptureWindow
函数为视频流捕获创建一个新窗口并返回其句柄。此函数在步骤 1 中调用。
[DllImport("user32", EntryPoint = "SendMessage")]
static extern bool SendMessage(int hWnd, uint wMsg, int wParam, int lParam);
SendMessage
函数向一个或多个窗口发送指定的。它调用指定窗口的窗口过程,直到窗口过程处理完该消息才返回。请注意,wParam
和lParam
都指定了消息特定的额外信息,即数字、结构指针、缓冲区。此项目中的SendMessage
函数通过不同的lParam
类型进行了重载。它在步骤 2、3、4 和 5 中调用。
[DllImport("avicap32.dll")]
static extern bool capGetDriverDescription(intwDriverIndex,
[MarshalAs(UnmanagedType.VBByRefStr)] ref String lpszName, int cbName,
[MarshalAs(UnmanagedType.VBByRefStr)] ref String lpszVer, int cbVer);
capGetDriverDescription
函数检索捕获驱动程序的版本描述。wDriverIndex
参数指定捕获驱动程序的索引。索引的范围可以是从 0 到 9。
结构体包括:BITMAPINFO
、BITMAPINFOHEADER
和VIDEOHDR
。VIDEOHDR
结构体由回调函数使用。它包含缓冲的帧数据。BITMAPINFO
结构体定义了 Windows 兼容的、设备无关的Bitmap
(DIB)的尺寸和颜色信息。BITMAPINFOHEADER
结构体包含有关 DIB 尺寸和颜色格式的信息。在本例中,它定义了视频格式(帧大小和每帧位数)。
代码内部
解决方案的主要部分是WebCamera
类库,它由三个类组成
WebCameraDevice
WebCameraEventArgs
WebCameraDeviceManager
WebCameraDevice
public WebCameraDevice(int frameWidth, int frameHeight, int preferredFPS,
int camID, int parentHwnd)
{
/*...*/
}
这会初始化一个新的WebCameraDevice
对象实例。preferredFPS
参数的重点:通常,网络摄像头支持最高 30 FPS。我在我的 A4Tech 网络摄像头上能获得的最高 FPS 是 20。此外,FPS 还取决于驱动程序的细节。例如,启用闪烁会稍微降低 FPS。使用WebCamDeviceManager
类获取所有可用设备及其索引。camID
参数代表所选设备的索引。
public void Start()
{
/*...*/
camHwnd = capCreateCaptureWindow("WebCam", 0, 0, 0, frameWidth,
frameHeight, parentHwnd, camID); // Step 2
//Try to connect a capture window to a capture driver
if (SendMessage(camHwnd, WM_CAP_DRIVER_CONNECT, 0, 0))
{
//Step 3, fill bitmap structure (see source)
/*...*/
// Enables preview mode
SendMessage(camHwnd, WM_CAP_SET_PREVIEW, 1, 0);
// Sets the frame display rate in preview mode. 34 ms ~ 29FPS
SendMessage(camHwnd, WM_CAP_SET_PREVIEWRATE, 34, 0);
// Sets the format of captured video data.
SendBitmapMessage(camHwnd, WM_CAP_SET_VIDEOFORMAT,
Marshal.SizeOf(bInfo), ref bInfo);
// Multithreading begins here
frameThread = new Thread(new ThreadStart(this.FrameGrabber));
bStart = true; // Flag variable
frameThread.Priority = ThreadPriority.Lowest;
frameThread.Start();
}
/*...*/
}
WebCameraDevice
中的多线程机制包括AutoResetEvent
对象和帧捕获工作线程。将preferredFPS
设置为0
允许用户手动控制帧捕获过程。工作线程等待(调用WaitOne()
),直到用户调用AutoResetEvent
对象的Set()
方法(WaitHandle
收到信号)。否则,将调用带有preferredFPSms
((1000 / preferredFPS)
)参数的WaitOne(..)
以等待指定的毫秒数。
调用Start()
方法后,工作线程开始将帧捕获到缓冲区。WebCameraDevice
对象将引发OnCameraFrame
事件,该事件在Bitmap
形式中包含帧数据。
private void FrameGrabber()
{
while (bStart) // if worker active thread is still required
{
/*...*/
// get the next frame. This is the SLOWEST part of the program
SendMessage(camHwnd, WM_CAP_GRAB_FRAME_NOSTOP, 0, 0);
//Make a function callback
SendHeaderMessage(camHwnd, WM_CAP_SET_CALLBACK_FRAME, 0,
delegateFrameCallBack);
/*...*/
}
}
此代码块中发生了什么?bStart
变量是一个标志,在调用Stop()
方法时变为false
。只要bStart
保持true
,WM_CAP_GRAB_FRAME_NOSTOP
消息就会用来自捕获设备的单个未压缩帧填充帧缓冲区。然后会进行回调。我们使用delegateFrameCallBack
变量而不是直接回调函数名,以避免 GC 错误。尝试将delegateFrameCallBack
替换为FrameCallBack
(回调函数名)并观察会发生什么。回调函数如下所示:
private void "code-string" name="<span">"FrameCallBack">FrameCallBack(IntPtr hwnd, ref VIDEOHEADER hdr)
{
if (OnCameraFrame != null)
{
Bitmap bmp = new Bitmap(frameWidth, frameHeight, 3 *
frameWidth, System.Drawing.Imaging.PixelFormat.Format24bppRgb,
hdr.lpData);
OnCameraFrame(this, new WebCameraEventArgs(bmp));
}
if (preferredFPSms == 0)
{
// blocks thread until WaitHandle receives a signal
autoEvent.WaitOne();
}
else
{
// blocks thread for preferred milliseconds
autoEvent.WaitOne(preferredFPSms, false);
}
}
如您所见,该函数包含了所有的Bitmap
转换、事件引发和WaitHandler
操作。就是这样!剩余的方法是
public void Set()
{
//Send a signal to the current WainHandle and allow blocked worker
//(FrameGrabber) thread to proceed
autoEvent.Set();
}
public void Stop()
{
try
{
bStart = false;
Set();
SendMessage(camHwnd, WM_CAP_DRIVER_DISCONNECT, 0, 0);
}
catch { }
}
public void ShowVideoDialog()
{
SendMessage(camHwnd, WM_CAP_DLG_VIDEODISPLAY, 0, 0);
}
它是如何工作的?
我们有一个包含所有必要网络摄像头图像捕获类的类库。首先,我们必须获取可用的 VFW 设备并将其显示给用户
public FormMain()
{
InitializeComponent();
WebCameraDeviceManager camManager = new WebCameraDeviceManager();
// fill combo box with available devices' names
cmbDevices.Items.AddRange(camManager.Devices);
// First available video device.
// I always receive "Microsoft WDM Image Capture (Win32)"
cmbDevices.SelectedIndex = 0;
}
开始按钮和OnCameraFrame
事件处理程序的代码
private void btnStart_Click(object sender, EventArgs e)
{
/*...*/
camDevice = new WebCameraDevice
(320, 200, 0, cmbDevices.SelectedIndex, this.Handle.ToInt32());
// Register for event notification
camDevice.OnCameraFrame +=
new WebCameraFrameDelegate(camDevice_OnCameraFrame);
camDevice.Start();
/*...*/
}
void camDevice_OnCameraFrame(object sender, WebCameraEventArgs e)
{
/*...*/
ImageProcessing.Filters.Flip(e.Frame, false, true); // Explained below
pictureBox.Image = e.Frame;
camDevice.Set(); // comment this if prefferedFPS != 0
/*...*/
}
您是否注意到WebCameraDevice
构造函数中prefferedFPS
参数的0
值?这就是为什么在camDevice_OnCameraFrame
事件处理程序中调用Set()
方法。您还记得camDevice
对象内部发生了什么吗?如果没有,请查看上面的FrameCallBack
。
意外的图像翻转
出现了意外的垂直图像翻转。我还没有弄清楚原因。它只发生在BITMAPINFOHEADER
缓冲区转换的情况下。也许Bitmap
类存在 bug。为了避免翻转,我参考了 Christian Graus 的一篇很棒的《C# 和 GDI+ 图像处理入门》文章。在 Bob Powell 的网站上找到了一个快速的灰度滤镜。
历史
- 发布 - 2007 年 9 月 12 日
附注
我想请您对我写的第一篇 CodeProject 文章多多包涵。如果您喜欢或不喜欢它,或者对它有任何疑问,请告诉我。