多功能网络摄像头 C# 库
易于使用的 C# 网络摄像头库,没有奇怪的 DLL 引用或 PInvoke 调用
目录
更新于 2015-06-06
感谢 Static Flux 提交的广泛的拉取请求。我将尝试跟进更多更改。如果您有代码更改要分享,请 在 GitHub 上提交您自己的拉取请求。
更新于 2015-02-23
感谢 bntr 和 Axel,我认为分辨率问题终于解决了。请参阅 此讨论以获取更多信息。为了便于将来改进库的协作,我已设置 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.h 和 WebCamLib.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 项目中唯一实现的一个类。CameraService
将 AvailableCameras
暴露为 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”按钮。
当您厌倦了观看自己时,只需关闭表单。回到 Visual Studio 后,查看 thrashOldCamera
方法(它也从 Form_Closing
和 btnStop_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();
}
问题
从代码可以看出 - 实现非常简单且干净(不像其他一些使用 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 窗口(用于设置亮度、对比度等)一样。
- 使用 DirectShow 设置网络摄像头属性 论坛帖子
- EasyWebCam 项目 - 大部分情况下很糟糕,但它确实有一个 Video Format 窗口。我反编译了该项目中的 WebCam_Capture.dll,发现所有内容都使用 PInvoke 实现 - 这意味着它对我们的方法无用。所以,如果有人能使用 C++ 和 DirectShow 调用相同的窗口 - 请通过扩展现有的
CameraMethods
类来帮忙。
结论
我希望您离开这篇文章时感到欣慰和快乐,知道即使您只是一个简单的 C# 开发人员,您现在也可以轻松地与您的网络摄像头通信。
如果您有任何要补充的,请随意发挥 - 我会关注评论区。任何求助(即使这个简单的解决方法对您来说还不够简单)、建议(您使用不同的方法做了同样的事情)、批评(您使用不同的方法做了同样的事情,而且您不喜欢这个)或提供帮助的邀请……都欢迎。
历史
- 2015-06-06 - 合并了 Static Flux 的 广泛的拉取请求
- 2013-01-01 - 添加了指向 Jake Dreww 修改的源代码的链接[^]。仍在寻找解决分辨率设置问题的有效方法。一些线索
- Sklett 和 Dan C.[[^]
- bntr[^]
- 2010-10-07 - 文章的初始版本。