ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#接入USB摄像头:DirectShow与AForge实战指南,解决枚举、帧率与稳定性问题

C#接入USB摄像头:DirectShow与AForge实战指南,解决枚举、帧率与稳定性问题 简介一套基于C#与.NET框架的USB摄像头控制示例项目Nighteop Camera面向需要实现摄像头实时预览、夜视模式及外部USB设备通信的桌面端开发者既可作为入门C#硬件编程的练手素材也能作为二次开发的工程基础。压缩包共46个文件整体约307KB以6个.cs源码文件、10个dll类库和3个exe可执行程序为主体同时包含.sln/.csproj工程配置、.pdb调试符号、.resx/.resources界面资源等便于直接打开Visual Studio工程查看运行效果或定位问题。项目覆盖多个技术点基于LibUsbDotNet与SharpUSBLib进行USB设备访问使用MediaCapture或AForge.NET完成摄像头视频捕获并通过调整曝光、增益或外部红外LED等方式实现夜视模式。代码中还有摄像头类封装、事件处理、异步操作以及错误处理示例能帮助读者理解C#环境下从设备枚举、通信交互到界面集成的完整链路。压缩包已有2332人学习/下载适合有一定.NET基础、希望快速掌握USB摄像头与硬件交互的开发者参考。1. 用 C# 接 USB 摄像头这套方案解决什么问题做 C# 上位机的人迟早都要接一次 USB 摄像头。工位调试软件要抓拍、检测外观、存档到 MES客户从抽屉里掏出一个免驱摄像头第一反应是找官方 SDK结果官网连驱动页都找不到。其实这套“C# USB摄像头”玩法很统一Windows 对符合 UVC 标准的摄像头提供通用驱动.NET 里通过 DirectShow 就能把画面拿到手。网上流传的 Camera.rar通常就是把设备枚举、预览、抓拍、录像组装好的完整工程解压出来照着读代码、改分辨率、接自己的回调一小时就能跑通。这篇文章适合你在 WinForm 或 WPF 里写上位机、不想被厂家 SDK 绑架、又希望代码能复用到下一台摄像头上的场景。下面所有参数和坑都按最常见的 UVC 摄像头来写。2. 选型与最小实现DirectShow 是 C# 接 USB 摄像头的最稳入口2.1 为什么优先选 DirectShow 而不是 MediaFoundationDirectShow 是微软在 ActiveMovie 时代定下来的采集框架到现在 Windows 的 WDM/AVStream 驱动模型仍然对它提供完整支持。USB 摄像头走 UVC 协议插进系统会挂到 usbvideo.sys 上DirectShow 的 Video Capture Filter 几乎不用额外驱动就能出流。MediaFoundation 是后推的框架接口更干净、也支持硬件解码但在这类设备上有实际问题是厂商测试投入少。很多低端免驱摄像头在 MediaFoundation 下要么枚举不到默认设备要么只能出低分辨率退回 DirectShow 才一切正常。做工业上位机我要的是稳定和可复现不要把采集链交给一个黑匣子。除非项目明确要求 UWP、新式 WinUI或者要直接做硬件编码推流否则首选 DirectShow。C# 直接调 DirectShow 要写一堆 COM 互操作所以常见做法是依赖第三方组件。免费开源组件里AForge.NET 的 AForge.Video.DirectShow 流传最广Accord.NET 是它的延续版本。有人说它好几年没更新但它封装的底层接口至今变化极小跑起来反而稳定。真正要留意的是它的目标框架停留在 .NET Framework 时代新 SDK 项目引用它可能触发 System.Drawing.Common 兼容警告。我一般先按 AForge 跑通整个逻辑让枚举、回调、分辨率这些概念都出现在代码里再决定要不要换成 OpenCvSharp 的 VideoCapture。下面这张表是选型时最常比的几点。对比项DirectShowMediaFoundation系统要求Windows XP 至今Windows 7 开始可用UVC 摄像头兼容几乎所有免驱摄像头部分老旧机型对齐不完整像素格式暴露YUY2、MJPG 直接可读部分机型只暴露一种.NET 托管封装AForge/Accord 成熟自己要写投影资料少调试工具链GraphStudioNext 可看图工具不直观2.2 用 AForge/Accord 三行代码完成枚举和预览从零开始最快的路径是拿到 Camera.rar 这种打包工程后先找到其中读取摄像头的核心文件再看它用的是哪个采集组件。大多数包用的都是下面这套结构FilterInfoCollection 枚举设备VideoCaptureDevice 打开设备NewFrame 事件把帧送出来。using AForge.Video; using AForge.Video.DirectShow; // 1. 枚举所有 USB 摄像头这一步拿到的是 DirectShow 设备集合 FilterInfoCollection videoDevices new FilterInfoCollection(FilterCategory.VideoInputDevice); if (videoDevices.Count 0) { Console.WriteLine(没有找到摄像头先检查连接和通用驱动); return; } // 2. 用 MonikerString 而不是序号选中设备 VideoCaptureDevice camera new VideoCaptureDevice(videoDevices[0].MonikerString); // 3. 注册帧回调预览画面从这里出来 camera.NewFrame (sender, eventArgs) { // eventArgs.Frame 是采集线程内部的 Bitmap必须 Clone 后另作他用 Bitmap copy (Bitmap)eventArgs.Frame.Clone(); pictureBox1.Image?.Dispose(); pictureBox1.Image copy; }; camera.Start();代码说明FilterCategory.VideoInputDevice 是 DirectShow 给视频输入设备定义的过滤类别它枚举的是系统里所有可用的采集 Filter。MonikerString 是每一个设备实例的稳定标识比数组下标可靠。NewFrame 回调运行在后台采集线程不是 UI 线程所以直接改 PictureBox 有线程问题我这里的写法是简化演示实际项目里要么用 Invoke要么把图片扔进队列由 UI 定时取。Start 方法启动采集后立刻返回画面通过回调异步到达别在 Start 后面同步等帧。停止采集时一定要成对调用camera.SignalToStop(); camera.WaitForStop(); camera null;SignalToStop 通知采集线程退出WaitForStop 阻塞等待它真正结束。直接调 Stop 在某些设备上会卡死这是老驱动留下的习惯见后面避坑章节。2.3 不依赖第三方组件时自己写 DirectShow 互操作是底线如果公司对开源组件有合规要求或者目标机不能连网还原 NuGet 包就得自己封 DirectShow。这件事没有想象中难整个采集链核心就是创建 Filter Graph往里面加一个视频采集 Filter再把输出接到渲染或抓帧环节。下面是极简互操作声明能跑通“创建对象”这一步// 自己声明 DirectShow 需要的两个 COM 接口IFilterGraph 和 IMediaControl // 实际项目还要 IGraphBuilder、ICaptureGraphBuilder2、IBaseFilter、IPin、IAMStreamConfig [ComImport, Guid(56a86891-0ad4-11ce-b03a-0020af0ba770)] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IGraphBuilder { } [ComImport, Guid(56a86893-0ad4-11ce-b03a-0020af0ba770)] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IMediaControl { } // 通过 CLSID_FilterGraph 创建 Graph 对象 Type graphType Type.GetTypeFromCLSID(new Guid(e436ebb3-524f-11ce-9f53-0020af0ba770)); IGraphBuilder graph (IGraphBuilder)Activator.CreateInstance(graphType);这段代码里的 Guid 不是乱码它们分别是 IGraphBuilder、IMediaControl 和 CLSID_FilterGraph 的 COM 标识。如果你解压 Camera.rar 后看到源码里有一长串 Guid 声明别改它们照着原样保留。自己封装的成本主要在 IAMStreamConfig 和 IPin 这两个接口上它们负责读分辨率列表、设置视频格式大概多写两三百行 interop。好处是项目不依赖 AForge 的更新节奏坏处是调试难度上了一个台阶所以我的建议是先 AForge 跑通再按需替换。3. UVC 枚举与会话参数区分多摄像头、分辨率、帧率的配置逻辑3.1 用 SetupAPI 拿设备路径构造物理 ID多路摄像头工位最常见的翻车点是代码写死用 videoDevices[0]。某天开机后系统的枚举顺序变了另一台同型号设备排到前面软件连错相机。DirectShow 只能告诉你设备集合不告诉你设备的物理位置。要稳定区分设备需要去查 Windows 设备接口信息这一步由 SetupAPI 完成。using System; using System.Collections.Generic; using System.Runtime.InteropServices; public static class UsbCameraEnum { private const int DIGCF_PRESENT 0x2; private const int DIGCF_DEVICEINTERFACE 0x10; [StructLayout(LayoutKind.Sequential)] public struct SP_DEVICE_INTERFACE_DATA { public int Size; public Guid InterfaceClassGuid; public int Flags; public IntPtr Reserved; } // devices 设备接口类KSCATEGORY_VIDEO_CAMERA private static readonly Guid VideoCameraClass new Guid(E5323777-F976-4f5b-9B55-B94699C46E44); [DllImport(setupapi.dll, SetLastError true, CharSet CharSet.Auto)] private static extern IntPtr SetupDiGetClassDevs( ref Guid classGuid, IntPtr enumerator, IntPtr hwndParent, int flags); [DllImport(setupapi.dll, SetLastError true)] private static extern bool SetupDiEnumDeviceInterfaces( IntPtr deviceInfoSet, IntPtr deviceInfoData, ref Guid interfaceClassGuid, int memberIndex, out SP_DEVICE_INTERFACE_DATA deviceInterfaceData); [DllImport(setupapi.dll, SetLastError true, CharSet CharSet.Auto)] private static extern bool SetupDiGetDeviceInterfaceDetail( IntPtr deviceInfoSet, ref SP_DEVICE_INTERFACE_DATA deviceInterfaceData, IntPtr deviceInterfaceDetailData, int deviceInterfaceDetailDataSize, out int requiredSize, IntPtr deviceInfoData); [DllImport(setupapi.dll)] private static extern bool SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet); public static Liststring EnumerateCameraPaths() { var result new Liststring(); IntPtr devs SetupDiGetClassDevs(ref VideoCameraClass, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); for (int index 0; ; index) { var ifData new SP_DEVICE_INTERFACE_DATA(); ifData.Size Marshal.SizeOf(ifData); if (!SetupDiEnumDeviceInterfaces(devs, IntPtr.Zero, ref VideoCameraClass, index, out ifData)) { break; // 返回 false 表示枚举结束 } int requiredSize 0; SetupDiGetDeviceInterfaceDetail(devs, ref ifData, IntPtr.Zero, 0, out requiredSize, IntPtr.Zero); if (requiredSize 0) continue; IntPtr pDetail Marshal.AllocHGlobal(requiredSize); try { // 变长结构体的第一个 DWORD 表示结构体自身大小 // Unicode 模式下填 4 字节即可。 Marshal.WriteInt32(pDetail, 4); if (SetupDiGetDeviceInterfaceDetail(devs, ref ifData, pDetail, requiredSize, out requiredSize, IntPtr.Zero)) { // DevicePath 从结构体偏移 4 个字节开始 string devicePath Marshal.PtrToStringAuto(IntPtr.Add(pDetail, 4)); if (!string.IsNullOrEmpty(devicePath)) result.Add(devicePath); } } finally { Marshal.FreeHGlobal(pDetail); } } SetupDiDestroyDeviceInfoList(devs); return result; } }常见 DevicePath 长这样\\?\usb#vid_046dpid_0825#8c0...mi_00#...。里面的 vid_ 是厂商 IDpid_ 是产品型号mi_ 是设备功能号后面还有实例序列号。同型号多台相机的区别就在序列号和 USB 端口路径上。拿到 DevicePath 后可以和 DirectShow 的 MonikerString 做映射之后无论系统怎么重排枚举顺序软件都能找到正确的那一路。提示如果现场只有一台摄像头匹配 VIDPIDMI 就够了。多台同型号时还要比对实例序列号或者父 USB 端口号后者需要再查 DEVPKEY_Device_Parent代码量不多但必须在设备插入状态下读取。3.2 分辨率与帧率怎么选理解 VideoCapabilities 和像素格式UVC 摄像头不是任意分辨率都能出图。传感器内部做裁切和缩放后对 PC 暴露的是一组固定预设每种预设包含分辨率、帧率、像素格式三个参数。在 AForge 里对应 VideoCapabilities 集合var camera new VideoCaptureDevice(videoDevices[0].MonikerString); foreach (var cap in camera.VideoCapabilities) { Console.WriteLine( ${cap.FrameSize.Width}x{cap.FrameSize.Height}, ${cap.AverageFrameRate}fps, {cap.BitCount}bit); } // 找一档不低于 1280 宽、且帧率最高的配置 var choose camera.VideoCapabilities .Where(c c.FrameSize.Width 1280) .OrderByDescending(c c.AverageFrameRate) .FirstOrDefault(); if (choose ! null) { camera.VideoResolution choose; }注意 AverageFrameRate 是设备标称值不是实测值。同一分辨率下帧率档位可能写 5/15/30实际能不能跑满还取决于 USB 带宽、CPU 解码能力和驱动实现。BitCount 则对应像素格式采集时最常遇到的是下面三种格式特点带宽占用CPU 开销适用场景YUY2/YUYV无压缩色彩还原准高很低近距离图像处理、测量MJPG帧内 JPEG 压缩低解码消耗 CPU1080P 以上高帧率H.264帧间压缩延迟略高最低依赖显卡硬解长时间录像、推流带宽不够时同分辨率下切到 MJPG 往往能把帧率提上去。代价是解码多耗 CPU所以采集端和业务处理端的 CPU 预算要一起算。3.3 帧回调分离抓帧、入队、处理分三段NewFrame 回调天然运行在采集线程上这是整个方案里最容易被写坏的地方。新手喜欢把识别、保存、甚至是网络上传直接写进回调画面立刻变卡。正确思路是回调只负责拿帧处理放到另外的工作线程。using System.Collections.Concurrent; var queue new ConcurrentQueueBitmap(); var camera new VideoCaptureDevice(videoDevices[0].MonikerString); camera.NewFrame (sender, e) { var clone (Bitmap)e.Frame.Clone(); // 队列积压超过 5 帧丢最旧的保证实时性 if (queue.Count 5 queue.TryDequeue(out var dropped)) dropped.Dispose(); queue.Enqueue(clone); }; // 处理线程 Task.Run(() { while (processRunning) { if (queue.TryDequeue(out var frame)) { ProcessFrame(frame); // 识别、测量都在这里做 frame.Dispose(); } else { Thread.Sleep(5); } } });队列深度 5 是实时性和延时的折中太深会让画面延迟越来越明显太浅扛不住处理端的一次抖动。如果你的项目后面要接 AI 识别可以把 ConcurrentQueue 换成 Channel 的 BoundedChannel用内置背压控制积压效果更干净。这里锁定的原则是采集线程绝不能等业务线程。4. USB 摄像头 C# 开发高频避坑现象、原因、解决4.1 初始化失败摄像头被占用现象camera.Start() 不报错但 NewFrame 从未触发或者直接抛-2147467259。原因摄像头被另一个进程占住。微信、浏览器、远程会议软件都会在后台占用摄像头最常见的其实是上一个测试程序没退出。UVC 摄像头同一时间只允许一个会话独占抓帧。解决先关掉可能占用的软件代码里停止摄像头必须成对调用 SignalToStop 和 WaitForStop程序退出时把 camera 置 null再调用一次 GC 也能缩短释放时间。初始化失败时把 Win32 错误码打到日志不要只显示“打开失败”。4.2 画面偏绿花屏像素格式和解码不匹配现象图像大面积发绿边缘有彩色条纹人脸像底片。原因摄像头输出 YUY2 或 YUYV 格式显示层却按 RGB32 解释。很多第三方组件对部分新设备的默认格式协商不准或者你在代码里手动指定了分辨率但没有同步指定像素格式。解决先打印 VideoCapabilities 里的 BitCount 和 FrameSize确定设备真正输出的格式如果设备支持 MJPG优先切到 MJPG 再看颜色是否正常自己封 DirectShow 时用 IAMStreamConfig 读取 VIDEOINFOHEADER 里的 biCompression 字段按格式做转换。4.3 硬刚帧回调导致预览卡顿现象任务管理器 CPU 不高但预览窗口一卡一卡严重时整个 UI 失去响应。原因NewFrame 在采集线程里逐帧触发你在里面做识别、保存、上传等于把 USB 驱动的取帧节奏拖慢设备端开始丢帧。另一个原因是没有跨线程更新 UI直接在回调里改 PictureBox。解决回调里只做 Clone 和入队耗时操作全部移到独立线程UI 更新通过 Control.BeginInvoke 投递到主线程。可以用帧计数和入队时间戳验证卡顿发生在采集段还是处理段。4.4 同样的摄像头在 USB 3.0 口反而掉帧现象从 USB 2.0 口换到 USB 3.0 口后帧率下降、偶发掉线说出去别人都不信。原因一半是玄学一半是硬件工程。部分主板 USB 3.0 控制器和芯片组共用中断负载上来后等时出现问题劣质线缆超过 3 米也可能造成传输错误还有些设备的 UVC 带宽协商在 xHCI 控制器上表现奇怪。解决先换回 USB 2.0 口对比排除驱动问题换一根短一点的带屏蔽线在设备管理器里找到对应 USB 根集线器把“允许计算机关闭此设备以节约电源”关掉。不要一上来就改代码。4.5 拔插一次后连错摄像头现象程序第一次运行正常拔掉相机重插后读到的第一台设备变成另一个型号。原因DirectShow 的枚举顺序由总线和驱动加载顺序决定不是固定不变的索引完全不可靠。解决用第 3.1 节的 SetupAPI 枚举 DevicePath通过 VID、PID、MI 锁定设备。匹配逻辑写成独立方法减少业务代码里的字符串比较private static bool IsTargetCamera(string devicePath) { return devicePath.Contains(vid_0c45) devicePath.Contains(pid_6366) devicePath.Contains(mi_00); }这里 vid_0c45 和 pid_6366 要替换成你设备铭牌上的实际值。多台同型号设备时再比对 DevicePath 里的序列号字段。坚决不要在代码里用 VideoCaptureDevice 的索引做长期绑定索引只适合临时测试。5. 交付前的最后一步帧率验证、内存与打包注意事项5.1 用计时器量帧率不要用肉眼看判断摄像头是否达标最直接的办法是统计单位时间内的回调次数。这个数字反映的是“应用层实际收到帧的频率”比设备标称帧率更有说服力。int frameCount 0; var stopwatch Stopwatch.StartNew(); camera.NewFrame (sender, e) { frameCount; if (stopwatch.ElapsedMilliseconds 1000) { Console.WriteLine($过去 1 秒收到 {frameCount} 帧); stopwatch.Restart(); frameCount 0; } }; camera.Start(); Console.WriteLine(按 Q 键退出...); while (Console.ReadKey().Key ! ConsoleKey.Q) ; camera.SignalToStop(); camera.WaitForStop();这个测试代码里没有做图像处理纯粹测采集链路。如果实测帧率低于设备标称优先检查是否开了高分辨率、像素格式是不是 YUY2、机器上有没有其他程序抢带宽。测的时候把 UI 缩到最小避免渲染影响结果。5.2 打包与验收运行库、位宽、长期稳定性打包交付时目标框架和运行库是踩坑重灾区。老工程包里的代码常写的是 .NET Framework 3.5 或 4.0新机器不一定带对应运行时编译部署时会报“缺少 .NET Framework”。这种情况我一般保持目标框架为 .NET Framework 4.8它在 Win10/Win11 上默认可用回归成本最低。如果用 .NET 6 以上重写需要单独处理 System.Drawing.Common 在非 Windows 环境的兼容问题好在工控机基本都是 Windows这个坑容易绕过去。发布配置建议固定 x64不要用 AnyCPU。DirectShow 的 COM 互操作在 x64 下没有 32 位进程的 WoW64 重定向问题运行也更稳定。如果代码里用到了 C 编写的原生封装目标机必须装对应版本的 Visual C Redistributable缺了它程序可能启动即崩溃。我自己做这套东西的习惯是采集层单独一个工程与业务逻辑完全分离设备标志只认 DevicePath不认索引每次切换分辨率后必须实测帧率再验收。遇到玄学报错先拔摄像头、看设备管理器里有没有警告号再回来查程序。很多时候问题出在供电、线材和驱动上不全是代码的事。希望帮到你。本文还有配套的精品资源点击获取
返回列表