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

多功能网络摄像头 C# 库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (132投票s)

2010年11月8日

CPOL

9分钟阅读

viewsIcon

1129713

downloadIcon

85249

易于使用的 C# 网络摄像头库,没有奇怪的 DLL 引用或 PInvoke 调用

目录

更新于 2015-06-06

感谢 Static Flux 提交的广泛的拉取请求。我将尝试跟进更多更改。如果您有代码更改要分享,请 在 GitHub 上提交您自己的拉取请求

更新于 2015-02-23

感谢 bntrAxel,我认为分辨率问题终于解决了。请参阅 此讨论以获取更多信息。为了便于将来改进库的协作,我已设置 Github 存储库,单击此处获取 C# 网络摄像头存储库。如果您改进了该库,请随时提交拉取请求,以便他人能够受益于您的工作。

文章其余部分未修改 - 目前保留其原始形式。

引言

那是 2005 年,当时我第一次需要从网络摄像头捕获图像,而那时正是为 Imagine Cup 项目 开始编码的时候。幸运的是,我有一个相当不错的团队,而承担这项任务的队友(Filip Popović)在搜寻网络和阅读大量文章(当然包括 CodeProject 的文章)几天后完成了一个 Visual Studio 项目。

从那时起,每当我需要从网络摄像头捕获图像时,我都会回到 2005 年的旧代码。尽管代码对我有用,但我并不满意。问题在于解决方案的基础 - Filip 使用了 PInvoke,并通过剪贴板将图像复制到 Bitmap 中,以便在 C# 中进行操作。并非我不怪他 - 这些年来我无法改进他的解决方案才是更大的问题。我尝试的每一次搜索都找到了类似的东西(这是我喜欢的唯一解决方案);我读得越多,就越意识到我缺少某些知识(C++ 和 DirectShow)才能开发出更好的东西。

这就是我写这篇文章的第一个原因——如果您擅长 C++ 和 DirectShow,请继续阅读,看看您是否能提供帮助。第二个原因是,我也希望将本文作为所有面临相同问题的 C# 开发者的汇聚点 - 我看到 StackOverflow 上充斥着关于这个主题的问题,而且大多数现有解决方案要么写得非常(非常、非常)糟糕,要么过于概念化。

所以,别担心这篇文章只是一个简单的求助;)我作为基础分享的解决方案性能相当不错且稳定,我敢肯定,读完这篇文章后,您将终于拥有一个可靠的库,在 C# 中轻松捕获网络摄像头图像时可以依赖它。

免责声明

我目前使用的库完全提取自 Touchless SDK(如果您有几秒钟的时间,请务必查看它 - Touchless 是那些会让你想立即开始玩起来的酷项目之一)。这意味着我不主张任何版权;我所做的只是剔除与网络摄像头无关的部分,以促进更轻松的重用。如果这篇文章确实激发了改进,我将确保这些改进会集成回 Touchless SDK,以感谢 Michwass 和他的团队所做的出色工作。

那么,我们如何从网络摄像头捕获图像?

下载随文章附带的源代码后,您应该会看到以下三个项目

  • Demo - 一个简单的 Windows Forms 项目,演示了如何使用网络摄像头。它引用了 WebCamWrapper,而 WebCamWrapper 又引用了 WebCamLib。
  • WebCamLib - 这是魔法发生的地方 - 这是一个 C++ 项目,只有两个文件(WebCamLib.hWebCamLib.cpp),它使用 DirectShow 查询网络摄像头并返回结果。
  • WebCamWrapper - 一个 C++ 项目之上的 C# 包装器,可以轻松集成到 .NET 世界。

作为起点,我推荐查看 Demo\MainForm.cs 的代码。此表单实现了您在访问网络摄像头时可能想到的大多数操作。首先是遍历连接到计算机的网络摄像头

private void MainForm_Load(object sender, EventArgs e)
{
    if (!DesignMode)
    {
        comboBoxCameras.Items.Clear();
        foreach (Camera cam in CameraService.AvailableCameras)
            comboBoxCameras.Items.Add(cam);

        if (comboBoxCameras.Items.Count > 0)
            comboBoxCameras.SelectedIndex = 0;
    }
}

您在代码中看到的 CameraService 类包含在 WebCamWrapper 项目中,它是主类 CameraMethods 的主要包装器,而 CameraMethods 是 C++ WebCamLib 项目中唯一实现的一个类。CameraServiceAvailableCameras 暴露为 Camera 类的列表,其中包含特定网络摄像头的逻辑。一旦用户选择了摄像头,您显然会想开始捕获

private CameraFrameSource _frameSource;
private static Bitmap _latestFrame;

private void btnStart_Click(object sender, EventArgs e)
{
    if (_frameSource != null && _frameSource.Camera == comboBoxCameras.SelectedItem)
        return;

    thrashOldCamera();
    startCapturing();
}

_frameSource 是我们将用于保存当前选定 Camera 的变量。Touchless 开发人员决定不将他们的捕获源仅限于网络摄像头(这无疑是一个明智的选择),因此他们创建了一个通用的 IFrameSource 接口,CameraFrameSource 实现了该接口……这就是这个类为何成为容器而不是直接使用 Camera 类。其余代码不言自明 - 如果我们选择相同的帧源,我们将退出;如果不是,我们将销毁旧摄像头并启动一个新的。接下来是 startCapturing 方法

private void startCapturing()
{
    try
    {
        Camera c = (Camera)comboBoxCameras.SelectedItem;
        setFrameSource(new CameraFrameSource(c));
        _frameSource.Camera.CaptureWidth = 320;
        _frameSource.Camera.CaptureHeight = 240;
        _frameSource.Camera.Fps = 20;
        _frameSource.NewFrame += OnImageCaptured;

        pictureBoxDisplay.Paint += new PaintEventHandler(drawLatestImage);
        _frameSource.StartFrameCapture();
    }
    catch (Exception ex)
    {
        comboBoxCameras.Text = "Select A Camera";
        MessageBox.Show(ex.Message);
    }
}

private void setFrameSource(CameraFrameSource cameraFrameSource)
{
    if (_frameSource == cameraFrameSource)
        return;

    _frameSource = cameraFrameSource;
}

private void drawLatestImage(object sender, PaintEventArgs e)
{
    if (_latestFrame != null)
    {
        e.Graphics.DrawImage(_latestFrame, 0, 0, _latestFrame.Width, _latestFrame.Height);
    }
}

public void OnImageCaptured(Touchless.Vision.Contracts.IFrameSource frameSource, 
                            Touchless.Vision.Contracts.Frame frame, double fps)
{
    _latestFrame = frame.Image;
    pictureBoxDisplay.Invalidate();
}

我们首先从 ComboBox 获取选定的 Camera,然后用它来创建和设置 CameraFrameSource。后面的行会影响捕获参数(请务必记住这三行,因为我们稍后会回来讨论它们),之后我们订阅了两个事件。

第一个事件 NewFrame,每当 WebCamLib 从网络摄像头捕获图像时都会引发。如您所见,我们将该图像保存到本地变量 _latestFrame 中,然后您可以进行任何额外的图像处理。第二个事件只是一个花哨的(更有效率的)方式来表达 pictureBoxDisplay.Image = frame.Image。出于某种原因,过于频繁地设置 PictureBox 上的 Image 属性会导致闪烁,而我们显然不希望那样 - 相反,我们通过使 PictureBox 无效然后处理其 paint 事件来绘制网络摄像头的当前图像。

现在所有这些都已实现,我们只需 StartFrameCapture 即可享受来自网络摄像头的视图。尝试一下 - 按 F5,然后在表单加载后单击“Start”按钮。

Rent is too damn high

当您厌倦了观看自己时,只需关闭表单。回到 Visual Studio 后,查看 thrashOldCamera 方法(它也从 Form_ClosingbtnStop_Click 方法中使用)

private void thrashOldCamera()
{
    if (_frameSource != null)
    {
        _frameSource.NewFrame -= OnImageCaptured;
        _frameSource.Camera.Dispose();
        setFrameSource(null);
        pictureBoxDisplay.Paint -= new PaintEventHandler(drawLatestImage);
    }
}

好吧,没什么特别的 - 我们取消订阅前面提到的两个事件,将 _frameSource 变量设置为 null,并调用 Camera 上的 Dispose,以便 C++ WebCamLib 可以执行清理操作。

信不信由你——就是这样。在 C# 中使用网络摄像头图像方面,没有更关键的东西需要解释或实现。MainForm.cs 中存在的额外代码只是为了保存当前图像

private void btnSave_Click(object sender, EventArgs e)
{
    if (_frameSource == null)
        return;

    Bitmap current = (Bitmap)_latestFrame.Clone();
    using (SaveFileDialog sfd = new SaveFileDialog())
    {
        sfd.Filter = "*.bmp|*.bmp";
        if (sfd.ShowDialog() == DialogResult.OK)
        {
            current.Save(sfd.FileName);
        }
    }

    current.Dispose();
}

并显示配置对话框

private void btnConfig_Click(object sender, EventArgs e)
{
    // snap camera
    if (_frameSource != null)
        _frameSource.Camera.ShowPropertiesDialog();
}

Configuration Dialog

问题

从代码可以看出 - 实现非常简单且干净(不像其他一些使用 WIA、晦涩的 DLL、剪贴板或硬盘保存图像等方法),这意味着问题不多。实际上,目前我只能识别一个问题。您还记得这三行吗?

_frameSource.Camera.CaptureWidth = 320;
_frameSource.Camera.CaptureHeight = 240;
_frameSource.Camera.Fps = 20;

好吧,事实证明它们并没有像宣传的那样工作。首先,让我们谈谈 FPS。如果我们深入到 Camera 类(第 254 行),我们会看到(在从网络摄像头捕获图像后调用的方法):

private void ImageCaptured(Bitmap bitmap)
{
    DateTime dtCap = DateTime.Now;

    // Always save the bitmap
    lock (_bitmapLock)
    {
        _bitmap = bitmap;
    }

    // FPS affects the callbacks only
    if (_fpslimit != -1)
    {
        if (_dtLastCap != DateTime.MinValue)
        {
            double milliseconds = ((dtCap.Ticks - _dtLastCap.Ticks) / TimeSpan.TicksPerMillisecond) * 1.15;
            if (milliseconds + _timeBehind >= _timeBetweenFrames)
            {
                _timeBehind = (milliseconds - _timeBetweenFrames);
                if (_timeBehind < 0.0)
                {
                    _timeBehind = 0.0;
                }
            }
            else
            {
                _timeBehind = 0.0;
                return; // ignore the frame
            }
        }
    }

    if (OnImageCaptured != null)
    {
        var fps = (int)(1 / dtCap.Subtract(_dtLastCap).TotalSeconds);
        OnImageCaptured.Invoke(this, new CameraEventArgs(bitmap, fps));
    }

    _dtLastCap = dtCap;
}

即使您只是瞥了一眼该方法,您可能也看到了它的大部分内容都用于计算帧之间的时间,并且如果帧来得太早就丢弃该帧。这我猜不算太坏 - 在 C# 级别而不是硬件级别控制帧率可能不会要您的命。

但是,找到另外两行,它们会影响捕获图像的大小,也无效(Camera.cs 中的第 235 行)?

private void CaptureCallbackProc(int dataSize, byte[] data)
{
    // Do the magic to create a bitmap
    int stride = _width * 3;
    GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
    var scan0 = (int)handle.AddrOfPinnedObject();
    scan0 += (_height - 1) * stride;
    var b = new Bitmap(_width, _height, -stride, PixelFormat.Format24bppRgb, (IntPtr)scan0);
    b.RotateFlip(_rotateFlip);
    // Copy the image using the Thumbnail function to also resize if needed
    var copyBitmap = (Bitmap)b.GetThumbnailImage(_width, _height, null, IntPtr.Zero);
    //var copyBitmap = (Bitmap)b.Clone();

    // Now you can free the handle
    handle.Free();

    ImageCaptured(copyBitmap);
}

如您所见,图像大小实际上是伪造的。我测试过的大多数摄像头都会返回 640x480 大小的图像。在大多数情况下都可以 - 如果您需要较小的图像,b.GetThumbnailImage 将允许您轻松调整其大小。但是,如果您需要更高分辨率的图像,您就陷入困境了,这可不好。

所以,来自 C++ 世界的任何人都可以提供帮助。我阅读的以下链接给了我一种印象,即只需要以某种方式在 C++ 中调用 Video Format 窗口,就像我们现在调用 Video Source Configuration 窗口(用于设置亮度、对比度等)一样。

Video Format Dialog

  • 使用 DirectShow 设置网络摄像头属性 论坛帖子
  • EasyWebCam 项目 - 大部分情况下很糟糕,但它确实有一个 Video Format 窗口。我反编译了该项目中的 WebCam_Capture.dll,发现所有内容都使用 PInvoke 实现 - 这意味着它对我们的方法无用。所以,如果有人能使用 C++ 和 DirectShow 调用相同的窗口 - 请通过扩展现有的 CameraMethods 类来帮忙。

结论

我希望您离开这篇文章时感到欣慰和快乐,知道即使您只是一个简单的 C# 开发人员,您现在也可以轻松地与您的网络摄像头通信。

如果您有任何要补充的,请随意发挥 - 我会关注评论区。任何求助(即使这个简单的解决方法对您来说还不够简单)、建议(您使用不同的方法做了同样的事情)、批评(您使用不同的方法做了同样的事情,而且您不喜欢这个)或提供帮助的邀请……都欢迎。

历史

© . All rights reserved.