简介:这份资源面向使用 VB.NET 开发桌面应用的初学者与中级开发者,聚焦笔记本摄像头调用与图像采集场景,借助 AForge.Video.DirectShow.dll 封装 DirectShow 接口,解决在 .NET 平台下枚举摄像头、实时预览、抓拍保存等常见需求。压缩包共 53 个文件,约 874KB,包含 6 个 vb 源码文件、7 个 dll 依赖库、3 个 exe 可执行程序,以及 vbproj、sln、config、resx 等工程与配置文件,另有少量 pdb、xml、txt 辅助说明,整体结构完整,可直接打开运行。资源围绕 VideoCaptureDevice 类展开,涵盖设备枚举、NewFrame 事件回调、分辨率设置、位图抓拍与 JPEG 保存等关键环节,并演示了停止捕获与资源释放的写法。目前已有 1406 人学习下载,适合希望快速跑通摄像头拍照流程、理解 AForge 视频采集机制的开发者参考,也可在此基础上扩展录像、水印或图像处理功能。
1. 摄像头捕捉这件事,为什么老项目还在用 AForge
如果你手上维护的是一套 VB.NET 写的桌面系统,客户突然提一句「能不能加个摄像头拍照,把照片存到档案里」,你大概率会先搜到 AForge.Video.DirectShow.dll 这个方案。它不是什么新东西,但在 WinForms 桌面端做摄像头捕捉及拍照,它依然是能最快跑通的一条路:引用一个 DLL,拖一个 VideoSourcePlayer 控件,几十行代码就能出画面,再补一个拍照按钮就能落盘。
这套方案解决的是「本地 USB 摄像头 / 采集卡 → 实时预览 → 单帧抓拍 → 保存为图片」这条最短链路,适合工控上位机、门禁登记、档案录入、质检留证这类场景。它不适合做高帧率视频编码、不适合多路并发推流,也不适合跨平台。你要清楚它的边界:它就是个 DirectShow 的托管封装,帮你把设备枚举、预览渲染、帧回调这几件事做掉。下面按「能跑起来 → 参数怎么调 → 坑在哪 → 怎么做得更稳」的顺序讲透。
2. 环境准备与设备枚举:先把摄像头认出来
2.1 引用与控件到底装了什么
AForge 这套东西拆成几个程序集,做摄像头捕捉你至少要引两个:AForge.dll 是基础库,AForge.Video.dll 定义 IVideoSource 抽象,AForge.Video.DirectShow.dll 才是真正对接 DirectShow 的实现,里面两个核心类:FilterInfoCollection 负责枚举设备,VideoCaptureDevice 负责打开某一路设备并持续吐帧。UI 上的 VideoSourcePlayer 控件在 AForge.Controls.dll 里,它内部起一个后台线程去拉帧再刷到控件表面。
常见做法是用 NuGet 装,但老项目里更常见的是直接引 DLL 文件。这里有个血泪经验:AForge.Controls.dll 的 VideoSourcePlayer 在 64 位下偶尔会出渲染问题,如果你的项目是 AnyCPU,建议显式锁 x86 或 x64 其中一个,别让它自己飘。我一般锁 x86,因为很多老工控机的采集卡驱动只有 32 位版本。
2.2 枚举设备的最小代码
Imports AForge.Video.DirectShow Public Class CameraHelper ' 保存枚举到的设备集合,后续按索引打开 Private _devices As FilterInfoCollection ''' <summary> ''' 枚举本机所有视频输入设备 ''' </summary> Public Function EnumDevices() As List(Of String) Dim names As New List(Of String)() ' VideoInputDevice 是 DirectShow 里视频采集设备的类别 GUID 封装 _devices = New FilterInfoCollection(FilterCategory.VideoInputDevice) For Each info As FilterInfo In _devices ' info.Name 是设备显示名,info.MonikerString 是唯一标识 names.Add(info.Name) Next Return names End Function End Class逻辑说明:FilterCategory.VideoInputDevice 告诉 DirectShow「我只关心视频输入这一类」,系统会把 USB 摄像头、虚拟摄像头、部分采集卡都列出来。MonikerString 是设备的唯一标识串,比 Name 可靠——因为两台同型号摄像头 Name 可能一模一样,用 Name 去打开会翻车。参数上没什么可调的,枚举本身是只读操作。失败时看什么:如果返回 0 个设备,先确认摄像头在系统「相机」应用里能不能用,再确认项目位数和驱动位数是否匹配,这是最常见的两个原因。
2.3 打开设备与预览
Imports AForge.Video Imports AForge.Video.DirectShow Private _videoSource As VideoCaptureDevice Public Sub StartPreview(deviceIndex As Integer, player As AForge.Controls.VideoSourcePlayer) If _devices Is Nothing OrElse _devices.Count = 0 Then Return ' 用 MonikerString 打开,避免同名设备歧义 _videoSource = New VideoCaptureDevice(_devices(deviceIndex).MonikerString) ' 设置期望的分辨率,见 2.4 SetDesiredResolution(_videoSource, 1280, 720) player.VideoSource = _videoSource player.Start() End Sub Public Sub StopPreview() If _videoSource IsNot Nothing AndAlso _videoSource.IsRunning Then ' SignalToStop 是异步停止,必须等 WaitForStop,否则下次打开会占用设备 _videoSource.SignalToStop() _videoSource.WaitForStop() End If End Sub逻辑说明:VideoSourcePlayer 的 Start 内部会调用 VideoSource.Start,帧到达后通过事件回调刷新控件。StopPreview 里那个 WaitForStop 是关键,很多人只写 SignalToStop 就以为停了,结果第二次打开报「设备被占用」,这就是没等线程真正退出的典型翻车。参数上,SignalToStop 之后可以给一个超时保护,避免极端情况下卡死主线程。
2.4 分辨率与帧率的设置方式
VideoCaptureDevice 支持通过 VideoCapabilities 查询设备支持的所有格式,然后挑一个匹配的塞回去。
Private Sub SetDesiredResolution(dev As VideoCaptureDevice, wantW As Integer, wantH As Integer) Dim best As VideoCapabilities = Nothing For Each cap As VideoCapabilities In dev.VideoCapabilities ' 先按宽高精确匹配,再退而求其次找最接近的 If cap.FrameSize.Width = wantW AndAlso cap.FrameSize.Height = wantH Then best = cap Exit For End If Next If best IsNot Nothing Then dev.VideoResolution = best End If End Sub逻辑说明:VideoCapabilities 里每一项包含 FrameSize、AverageFrameRate、BitCount。注意不是所有摄像头都支持你想要的 1280x720,有些只给 640x480 和 1920x1080 两档。参数选择上,预览用 640x480 足够省 CPU,拍照时再切高分辨率是更聪明的做法,但切换分辨率要停流重开,会有短暂黑屏,这个体验要提前和产品说清楚。
3. 拍照落盘:从帧回调到 JPEG 文件
3.1 用 VideoSourcePlayer 的 GetCurrentVideoFrame
最省事的拍照方式就是直接问控件要当前帧。
Private Sub btnCapture_Click(sender As Object, e As EventArgs) Handles btnCapture.Click If videoPlayer.IsRunning = False Then Return ' GetCurrentVideoFrame 返回一个 Bitmap 副本,调用方负责释放 Dim frame As Bitmap = videoPlayer.GetCurrentVideoFrame() If frame Is Nothing Then Return Dim fileName As String = IO.Path.Combine("D:\Photos", DateTime.Now.ToString("yyyyMMdd_HHmmssfff") & ".jpg") ' 用 JPEG 编码器保存,质量 90 SaveJpeg(frame, fileName, 90) frame.Dispose() End Sub逻辑说明:GetCurrentVideoFrame 拿到的是一份克隆,不会影响预览线程,但如果你拍照频率很高(比如每秒好几次),这个克隆开销会累积。参数上文件名用毫秒时间戳避免重名。注意 frame 用完必须 Dispose,否则内存会缓慢上涨,跑一天下来就是几百 MB。
3.2 自己写 JPEG 保存,别用 Bitmap.Save 默认质量
Bitmap.Save 直接存 JPEG 会用默认质量,压缩痕迹明显,档案照片这种场景不合适。
Imports System.Drawing.Imaging Private Sub SaveJpeg(bmp As Bitmap, path As String, quality As Integer) ' 查找系统里的 JPEG 编码器 Dim codec As ImageCodecInfo = Nothing For Each c As ImageCodecInfo In ImageCodecInfo.GetImageEncoders() If c.FormatID = ImageFormat.Jpeg.Guid Then codec = c Exit For End If Next ' quality 取值 0-100,90 是清晰度和体积的平衡点 Dim ep As New EncoderParameters(1) ep.Param(0) = New EncoderParameter(Encoder.Quality, CLng(quality)) bmp.Save(path, codec, ep) ep.Dispose() End Sub逻辑说明:Encoder.Quality 是唯一需要调的参数,90 左右肉眼几乎无损,文件大小约为 100 质量的一半。如果你要存证,建议 95;如果只是缩略图预览,75 就够。失败时看什么:如果 Save 抛 GDI+ 异常,八成是目录不存在或没有写权限,先 Directory.CreateDirectory 再存。
3.3 帧回调方式:需要连续处理时用 NewFrame 事件
如果你不只是拍照,还要做实时分析(比如检测有没有人),就得挂 NewFrame 事件,而不是靠控件。
Private Sub OnNewFrame(sender As Object, eventArgs As NewFrameEventArgs) ' 这个回调在采集线程上,不要在这里碰 UI 控件 Dim frame As Bitmap = DirectCast(eventArgs.Frame.Clone(), Bitmap) ' 丢给后台处理,避免阻塞采集线程 Threading.ThreadPool.QueueUserWorkItem(Sub() ProcessFrame(frame)) End Sub Private Sub ProcessFrame(frame As Bitmap) Try ' 在这里做你的分析或存盘 Finally frame.Dispose() End Try End Sub逻辑说明:NewFrameEventArgs.Frame 是复用的缓冲区,必须 Clone 之后再跨线程使用,否则下一帧来了数据就被覆盖,你会看到画面撕裂或花屏。参数上,QueueUserWorkItem 的并发度要控制,如果处理比采集慢,队列会堆积,建议加一个信号量限制同时处理数。
4. 避坑与排查:那些让你加班到半夜的问题
4.1 关闭程序后摄像头灯还亮着
现象:点了窗口关闭按钮,程序退了,但摄像头指示灯不灭,任务管理器里进程还在。原因:VideoSourcePlayer 没 Stop,或者只调了 SignalToStop 没 WaitForStop,采集线程还在跑。解决:在 FormClosing 事件里先 player.SignalToStop(),再 player.WaitForStop(),最后 player.VideoSource = Nothing。顺序不能反,先置空再停会抛异常。
4.2 第二次打开报「设备已被占用」
现象:第一次预览正常,停止后再开就报错。原因:上一路 VideoCaptureDevice 没释放干净,DirectShow 的 filter graph 还挂着。解决:确保每次 Start 之前,旧的 _videoSource 已经 WaitForStop 并置 Nothing;不要复用同一个 VideoCaptureDevice 实例反复 Start/Stop,直接 new 一个新的更稳。
4.3 预览画面卡顿或延迟越来越大
现象:刚打开很流畅,跑十几分钟后画面延迟好几秒。原因:你在 NewFrame 回调里做了耗时操作,采集线程被拖住,帧在缓冲区堆积。解决:回调里只做 Clone,重活丢线程池;同时检查是不是在 UI 线程里同步等待了帧数据。参数上,可以给处理队列设一个上限,超过就丢帧,宁可丢帧也不要累积延迟。
4.4 换台电脑就找不到设备
现象:开发机好好的,部署到客户机器上枚举为空。原因:客户机没装摄像头驱动,或者项目是 x64 而驱动只有 x86。解决:先让客户用系统自带相机应用验证硬件,再核对项目目标平台。这个坑在工控现场特别常见,采集卡驱动往往只给 32 位。
4.5 保存的图片偶尔是黑屏或半张
现象:大部分照片正常,偶尔一张全黑或只有上半部分。原因:GetCurrentVideoFrame 拿帧的瞬间正好赶上缓冲区切换,或者分辨率切换过程中抓拍。解决:抓拍前判断 IsRunning,抓到的 frame 检查尺寸是否等于预期分辨率,不等于就丢弃重抓一次。这个属于玄学范畴,加个重试基本能压下去。
5. 进阶:让拍照这件事更可控的几个技巧
第一个技巧是「预览低分辨率、拍照高分辨率」的双流思路。前面提过切换分辨率要停流重开,体验不好。更稳的做法是:预览固定用 640x480,拍照时不停流,而是从当前帧里取,如果客户对清晰度要求高,就接受那一下黑屏,在 UI 上给个「正在切换」的提示。我一般会做一个配置项,让现场实施的人自己选。
第二个技巧是拍照前的稳定判断。手持或震动环境下,抓拍容易糊。可以在 NewFrame 回调里算相邻两帧的差异,差异小于阈值说明画面稳定,这时候自动抓拍成功率最高。差异计算用简单的像素抽样就行,不用上复杂的算法。
' 抽样比较两帧差异,返回 0-1 的差异度 Private Function FrameDiff(a As Bitmap, b As Bitmap) As Double Dim diff As Long = 0, samples As Integer = 0 ' 每隔 16 像素取一个点,够用且快 For y As Integer = 0 To a.Height - 1 Step 16 For x As Integer = 0 To a.Width - 1 Step 16 Dim ca As Color = a.GetPixel(x, y) Dim cb As Color = b.GetPixel(x, y) diff += Math.Abs(ca.R - cb.R) + Math.Abs(ca.G - cb.G) + Math.Abs(ca.B - cb.B) samples += 3 Next Next Return diff / (samples * 255.0) End Function逻辑说明:GetPixel 很慢,所以必须抽样,Step 16 在 640x480 上大约取 1200 个点,单次耗时可控。阈值一般设 0.02 到 0.05,具体看场景,室内固定机位可以更严。这个函数放在回调线程里跑,别放 UI 线程。
第三个技巧是文件命名和目录分片。照片一多,单目录几万个文件,资源管理器打开就卡死。按日期建子目录,文件名带毫秒时间戳和设备索引,既避免重名又方便检索。如果要做数据库关联,存相对路径而不是绝对路径,换机器部署时不用改数据。
最后一个习惯:所有涉及设备释放的地方,都写成 Try/Finally,把 SignalToStop、WaitForStop、Dispose 放进 Finally。我吃过太多次亏——异常一抛,释放代码被跳过,摄像头就锁死了,只能重启电脑。这个习惯不花什么成本,但能省掉很多现场救火的电话。希望帮到你。
本文还有配套的精品资源,点击获取