65.9K
CodeProject 正在变化。 阅读更多。
Home

Webcamera, Multithreading and VFW

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (22投票s)

2007年9月17日

CPOL

5分钟阅读

viewsIcon

245100

downloadIcon

9575

一篇关于多线程环境中摄像头帧捕获的文章。

Screenshot - vfwwebcam1.jpg

引言

有几种方法可以捕获和处理网络摄像头图像:WIADirectShowVFW... 网上有很多 C# VFW 示例,其中大多数使用.NET 剪贴板将每个帧的数据从缓冲区传输到Bitmap可识别的对象。不幸的是,这使得多线程不可用,并降低了 FPS(每秒帧数)。本机 Win32 剪贴板和多线程可以解决速度问题,但我认为这不是最优雅的解决方案,应该还有其他方法可以从 Avicap 获取帧。我参考了 MSDN(参见上面的 VFW 链接)并发现了函数回调是可用的。本文将分步介绍如何在多线程环境中,使用avicap32.dll(VFW)捕获帧。

想法

这是你在网上看到很多示例中使用的一种方法
  1. 创建捕获窗口。
  2. 将捕获窗口连接到设备。
  3. 设置视频格式(像素高度和宽度)。
  4. 将帧捕获到临时不可访问的缓冲区。
  5. 将视频帧缓冲区的内容和关联的调色板复制到剪贴板。
  6. 从剪贴板获取帧,将数据转换为 RGB Bitmap
  7. 处理Bitmap。转到步骤 4。
这是我使用过的一种方法
  1. 创建捕获窗口。
  2. 将捕获窗口连接到设备。
  3. 设置视频格式(像素高度和宽度,每帧位数)。
  4. 将帧捕获到临时不可访问的缓冲区。
  5. 进行回调(使缓冲区数据在VIDEOHDR结构中可用)。
  6. 获取帧数据,将数据转换为 RGB Bitmap
  7. 处理Bitmap。转到步骤 4。

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函数向一个或多个窗口发送指定的。它调用指定窗口的窗口过程,直到窗口过程处理完该消息才返回。请注意,wParamlParam都指定了消息特定的额外信息,即数字、结构指针、缓冲区。此项目中的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。

结构体包括:BITMAPINFOBITMAPINFOHEADERVIDEOHDRVIDEOHDR结构体由回调函数使用。它包含缓冲的帧数据。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保持trueWM_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 文章多多包涵。如果您喜欢或不喜欢它,或者对它有任何疑问,请告诉我。

© . All rights reserved.