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 文章多多包涵。如果您喜欢或不喜欢它,或者对它有任何疑问,请告诉我。


