☰
C#调用摄像头拍照:AForge.NET轻量方案实战与避坑指南
2026/10/12 18:04:21 网站建设 项目流程

1. 为什么在C#里调用摄像头,我最终选了AForge

1.1 从一次实际需求说起

前阵子帮一个做产线质检的朋友处理一个小工具,需求很朴素:工位上有一台普通USB摄像头,操作员按一下按钮,软件自动抓一张当前画面的照片,存到指定目录,同时把工单号、时间戳一起写进文件名。听起来是不是特别简单?但真动手的时候,问题就来了——项目是WinForm的,.NET Framework 4.7.2,客户机器上不允许装额外的商业SDK,也不希望引入体积庞大的第三方库。

我一开始试过几种方案。WPF的MediaCapture在WinForm里用起来别扭,而且对老版本系统的兼容性一般;DirectShow自己封装FilterGraph,代码量大得离谱,调试成本高;OpenCVSharp功能强,但为了拍一张照片引入上百MB的依赖,实在不划算。最后绕了一圈,还是回到了AForge.NET这个老牌库上。

AForge.NET是一个开源的C#图像处理和计算机视觉框架,里面的AForge.Video和AForge.Video.DirectShow两个程序集,专门用来做摄像头的枚举、预览和帧捕获。它底层封装的是DirectShow,但把那些复杂的COM接口调用都藏起来了,对外暴露的API非常干净。对于“调用摄像头拍照”这种需求,它几乎是量身定做的。

1.2 AForge方案的核心优势与适用边界

先说优势。第一,轻量。AForge.Video相关的几个DLL加起来也就几百KB,跟OpenCVSharp那种动辄上百MB的库完全不是一个量级。第二,API简单。枚举设备、启动预览、订阅NewFrame事件、抓帧保存,核心逻辑不到五十行代码就能跑通。第三,兼容性好。它基于DirectShow,在Windows 7到Windows 11上都能正常工作,对老设备的支持也到位。

但我也得把话说清楚,AForge不是万能的。它已经很多年没有大版本更新了,社区活跃度不高,遇到一些新摄像头或者特殊编码格式可能会翻车。另外它本身不提供录像编码功能,只能抓帧,如果你要录视频还得自己接FFmpeg之类的工具。所以我的判断是:如果你的需求就是“预览+抓拍单帧”,AForge是最省事的选择;如果要做实时视频处理、人脸识别、录像编码,那还是老老实实上OpenCVSharp或者MediaFoundation。

1.3 整体设计思路

这个项目的结构我拆成了三层。最底层是设备管理层,负责枚举摄像头、选择设备、启动和停止视频源;中间是预览层,把摄像头的实时画面渲染到PictureBox上;最上层是抓拍层,在用户点击按钮时从当前帧复制一份Bitmap出来,做保存和后续处理。

为什么要分层?因为摄像头操作有个坑:视频源启动后是在独立线程里回调NewFrame事件的,如果你直接在事件里操作UI控件,会抛跨线程异常。分层之后,预览层负责用Invoke把画面丢回UI线程,抓拍层则用一个标志位来控制“下一帧到来时执行抓拍”,这样逻辑清晰,也不容易出bug。

2. 环境准备与依赖配置的实操细节

2.1 安装AForge相关的NuGet包

现在装AForge最省事的方式就是走NuGet。在Visual Studio里右键项目,选择“管理NuGet程序包”,搜索AForge,你会看到几个相关的包。这里要注意,别只装一个AForge就完事了,实际需要的是这三个:

包名作用是否必需
AForge核心库,提供基础类型和图像处理必需
AForge.Video视频源抽象层,定义IVideoSource接口必需
AForge.Video.DirectShowDirectShow实现,枚举和操作摄像头必需

如果你用的是Package Manager Console,直接敲这三条命令就行:

Install-Package AForge Install-Package AForge.Video Install-Package AForge.Video.DirectShow

装完之后检查一下项目的引用列表,应该能看到AForge.dll、AForge.Video.dll、AForge.Video.DirectShow.dll三个引用。有时候NuGet会自动带上AForge.Math和AForge.Imaging,这两个在这个项目里用不到,但留着也不碍事。

提示:AForge的NuGet包版本号停留在2.2.5已经很多年了,这是正常的,不用怀疑自己装错了。这个版本足够稳定,覆盖绝大多数USB摄像头场景。

2.2 项目配置的几个关键点

第一个要注意的是目标框架。AForge是基于.NET Framework开发的,虽然理论上.NET Core和.NET 5+也能通过兼容包引用,但实际测试下来在.NET Framework 4.5以上版本最稳。我一般建议用4.7.2或4.8,这两个版本在Win10和Win11上兼容性最好。

第二个是平台目标。AForge.Video.DirectShow底层调用的是32位的DirectShow组件,所以你的项目平台目标最好设成x86,或者Any CPU但取消“首选32位”的勾选。我踩过一次坑:项目设成x64,结果枚举摄像头的时候返回空列表,排查了半天才发现是平台不匹配。

第三个是UI框架的选择。WinForm和WPF都能用,但WinForm的PictureBox配合AForge最顺手,因为AForge输出的就是System.Drawing.Bitmap,直接赋值给PictureBox.Image就行。WPF的话需要额外做Bitmap到BitmapSource的转换,多一道工序。

2.3 摄像头设备的枚举逻辑

枚举摄像头用的是FilterInfoCollection类,构造的时候传入FilterCategory.VideoInputDevice。这个类会返回当前系统上所有可用的视频输入设备,每个设备是一个FilterInfo对象,包含Name和MonikerString两个关键属性。

using AForge.Video.DirectShow; FilterInfoCollection videoDevices; try { videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice); } catch (Exception ex) { MessageBox.Show("枚举摄像头失败:" + ex.Message); return; } if (videoDevices.Count == 0) { MessageBox.Show("没有检测到任何摄像头设备"); return; } foreach (FilterInfo device in videoDevices) { comboBoxDevices.Items.Add(device.Name); } comboBoxDevices.SelectedIndex = 0;

这里有个细节值得说一下:MonikerString是设备的唯一标识符,启动视频源的时候用的就是这个字符串,而不是设备名称。因为设备名称可能重复(比如插了两个同型号的摄像头),但MonikerString是唯一的。

注意:枚举操作最好放在try-catch里。某些情况下(比如摄像头驱动异常),构造函数会直接抛异常,不捕获的话程序就崩了。

3. 核心实现:从预览到抓拍的完整流程

3.1 启动视频源与实时预览

启动视频源的核心是VideoCaptureDevice类。构造的时候传入选中设备的MonikerString,然后订阅NewFrame事件,最后调用Start()方法。

using AForge.Video; using AForge.Video.DirectShow; private VideoCaptureDevice videoSource; private void StartPreview() { if (videoDevices.Count == 0) return; // 如果已有视频源在运行,先停止 if (videoSource != null && videoSource.IsRunning) { videoSource.SignalToStop(); videoSource.WaitForStop(); } var selectedDevice = videoDevices[comboBoxDevices.SelectedIndex]; videoSource = new VideoCaptureDevice(selectedDevice.MonikerString); videoSource.NewFrame += VideoSource_NewFrame; videoSource.Start(); } private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs) { // 注意:这里是在非UI线程回调的 Bitmap frame = (Bitmap)eventArgs.Frame.Clone(); if (pictureBoxPreview.InvokeRequired) { pictureBoxPreview.Invoke(new Action(() => { var old = pictureBoxPreview.Image; pictureBoxPreview.Image = frame; old?.Dispose(); })); } else { var old = pictureBoxPreview.Image; pictureBoxPreview.Image = frame; old?.Dispose(); } }

这段代码有几个关键点必须说清楚。第一,eventArgs.Frame是一个复用的Bitmap对象,AForge每次回调都会往同一个对象里写新数据。所以你绝对不能直接把Frame赋值给PictureBox,必须Clone一份出来,否则画面会闪烁甚至出现撕裂。第二,Clone出来的Bitmap要记得释放,不然内存会一路涨上去。我在PictureBox赋值前先把旧的Image Dispose掉,就是这个道理。

第三,InvokeRequired的判断不能省。NewFrame事件是在AForge的内部线程触发的,直接操作PictureBox会抛“跨线程操作无效”的异常。用Invoke把赋值操作丢回UI线程是标准做法。

3.2 抓拍单帧的实现方式

抓拍有两种思路。一种是在NewFrame事件里设一个标志位,下一帧到来时顺便保存;另一种是直接从PictureBox.Image里拿当前显示的帧。我推荐第一种,因为PictureBox里的图像可能因为缩放而失真,而NewFrame里的Frame是原始分辨率。

private bool captureRequested = false; private void btnCapture_Click(object sender, EventArgs e) { if (videoSource == null || !videoSource.IsRunning) { MessageBox.Show("请先启动摄像头预览"); return; } captureRequested = true; } private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs) { Bitmap frame = (Bitmap)eventArgs.Frame.Clone(); // 处理抓拍请求 if (captureRequested) { captureRequested = false; SaveFrame(frame); } // 更新预览(同上,略) } private void SaveFrame(Bitmap frame) { string dir = Path.Combine(Application.StartupPath, "Captures"); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); string fileName = $"IMG_{DateTime.Now:yyyyMMdd_HHmmss_fff}.jpg"; string fullPath = Path.Combine(dir, fileName); // 用JPEG编码器保存,质量设为90 var encoder = GetEncoder(ImageFormat.Jpeg); var encoderParams = new EncoderParameters(1); encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); frame.Save(fullPath, encoder, encoderParams); frame.Dispose(); } private ImageCodecInfo GetEncoder(ImageFormat format) { return ImageCodecInfo.GetImageDecoders() .FirstOrDefault(c => c.FormatID == format.Guid); }

这里用EncoderParameters来控制JPEG质量是有讲究的。Bitmap默认的Save方法保存JPEG时用的是75%质量,对于质检场景来说细节损失有点大。调到90%之后,文件大小增加不多,但清晰度明显提升。如果你需要无损保存,那就用PNG格式,但文件会大好几倍。

3.3 分辨率与帧率的设置

AForge允许你查询摄像头支持的所有视频能力,并从中选择合适的分辨率。VideoCaptureDevice有个VideoCapabilities属性,返回VideoCapabilities对象的数组,每个对象包含FrameSize、AverageFrameRate等信息。

private void SetupCameraCapabilities() { var caps = videoSource.VideoCapabilities; if (caps == null || caps.Length == 0) return; // 按分辨率从高到低排序,选一个合适的 var sorted = caps.OrderByDescending(c => c.FrameSize.Width * c.FrameSize.Height).ToArray(); foreach (var cap in sorted) { // 选一个不超过1920x1080的最高分辨率 if (cap.FrameSize.Width <= 1920 && cap.FrameSize.Height <= 1080) { videoSource.VideoResolution = cap; break; } } }

为什么要限制在1920x1080?因为很多工业摄像头标称支持4K,但实际USB带宽根本跑不满,强行设置高分辨率会导致帧率暴跌甚至画面卡死。选一个带宽能承受的分辨率,比盲目追求高像素实用得多。

实操心得:设置VideoResolution必须在Start()之前调用,Start之后再改是不生效的。这个顺序千万别搞反。

4. 踩坑记录与常见问题排查

4.1 摄像头被占用导致启动失败

这是最常见的问题。摄像头是独占设备,同一时间只能被一个程序使用。如果你的程序启动预览失败,先检查是不是有其他软件(比如系统自带的相机应用、会议软件)正在占用摄像头。

AForge在设备被占用时,Start()方法不会抛异常,但NewFrame事件永远不会触发,界面上就是一片黑。这种情况很难排查,因为没有任何错误提示。我的做法是在Start()之后加一个超时检测:如果3秒内没有收到任何帧,就提示用户检查摄像头占用情况。

private DateTime lastFrameTime = DateTime.MinValue; private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs) { lastFrameTime = DateTime.Now; // ... 其他处理 } // 在启动后启动一个定时器检查 private void CheckCameraAlive() { if (videoSource != null && videoSource.IsRunning) { if ((DateTime.Now - lastFrameTime).TotalSeconds > 3) { MessageBox.Show("摄像头已启动但未收到画面,请检查设备是否被其他程序占用"); } } }

4.2 内存泄漏与资源释放

AForge用不好最容易出的问题就是内存泄漏。每次NewFrame回调都Clone一个Bitmap,如果忘记Dispose,几分钟下来内存就能涨到几个GB。我总结了几条释放资源的规矩:

  • PictureBox.Image赋值前,先Dispose旧的Image
  • 抓拍保存完成后,立即Dispose抓拍用的Bitmap
  • 程序关闭时,调用videoSource.SignalToStop()和WaitForStop(),确保视频源完全停止
  • 在FormClosing事件里做清理,并且要判断videoSource是否为null
private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (videoSource != null && videoSource.IsRunning) { videoSource.SignalToStop(); videoSource.WaitForStop(); videoSource.NewFrame -= VideoSource_NewFrame; videoSource = null; } }

WaitForStop()会阻塞当前线程直到视频源完全停止,一般也就几百毫秒。如果不调用它直接关程序,有时候会残留后台线程,导致进程无法完全退出。

4.3 常见问题速查表

问题现象可能原因解决方法
枚举不到摄像头平台目标不匹配改为x86或取消首选32位
预览黑屏无画面设备被占用关闭其他使用摄像头的程序
画面卡顿严重分辨率设置过高降低到1280x720或更低
内存持续增长Bitmap未释放检查所有Clone处是否Dispose
关闭程序报错视频源未停止FormClosing里调用WaitForStop
抓拍图片模糊JPEG质量过低设置Encoder.Quality为90以上
跨线程异常直接操作UI控件用Invoke回UI线程

4.4 几个容易被忽略的细节

第一个是摄像头预热。有些USB摄像头刚启动的前几帧是黑的或者曝光不正常,如果你在启动后立刻抓拍,拿到的可能是一张废片。我的做法是启动后延迟500毫秒再允许抓拍,或者丢弃前10帧。

第二个是文件名冲突。如果你用秒级时间戳,快速连拍时可能生成同名文件。加上毫秒(fff)基本就能避免,但更稳妥的做法是再加一个自增序号。

第三个是异常处理。SaveFrame里的文件保存操作一定要包在try-catch里,因为磁盘满、权限不足、路径非法这些情况都可能导致保存失败。保存失败时给用户一个明确提示,比默默失败要好得多。

5. 功能扩展与个人经验总结

5.1 可以继续扩展的方向

这套基础框架搭好之后,往上加功能其实很顺手。比如加一个自动抓拍模式,每隔N秒抓一张,用于定时巡检;比如加一个运动检测,用AForge.Imaging里的帧差算法判断画面是否有变化,有变化才抓拍,能大幅减少无效图片;再比如加一个简单的图像预处理,抓拍后自动做灰度化或者对比度增强,方便后续的OCR识别。

AForge本身还带了AForge.Imaging库,里面有滤波、边缘检测、形态学操作等常用算法。如果你要做简单的视觉检测,不用引入OpenCV,AForge自带的功能够用了。当然复杂场景还是得上OpenCVSharp,这个要看具体需求。

5.2 我在实际使用中的几点体会

用了几年AForge做摄像头相关的工具,最大的感受是:它不完美,但在“简单抓拍”这个场景下,没有比它更省事的选择。你不需要理解DirectShow的FilterGraph,不需要处理COM接口的引用计数,几十行代码就能跑起来。

但也要认清它的局限。AForge的NewFrame回调频率是跟着摄像头走的,如果你在回调里做耗时操作(比如图像处理、文件IO),会阻塞后续帧的回调,导致预览卡顿。正确的做法是在回调里只做最轻量的操作——Clone一帧,丢给后台线程去处理。

另外,AForge对高分辨率高帧率的支持确实一般。我试过用4K摄像头跑30帧,CPU占用直接飙到50%以上,换成1080p就降到10%左右。所以如果你的场景对性能要求高,还是得考虑MediaFoundation或者直接上OpenCVSharp。

最后分享一个小技巧:如果你需要在多台机器上部署,建议把AForge的三个DLL直接放在程序目录下,不要依赖GAC。这样拷贝过去就能跑,不用在每台机器上装NuGet包。发布的时候用Release模式编译,Debug模式下AForge的性能会差不少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询