简介:C#调用USB摄像头进行视频录制与图像保存的完整Demo项目,面向使用WinForms或WPF开发桌面采集功能的C#程序员。项目基于Visual Studio解决方案,完整演示了设备枚举、实时预览、录像写入和单帧截图保存的实现链路;同时涉及DirectShow与Media Foundation两种采集框架的选用思路,并附有权限配置、编码格式与异常处理说明。包内含49个文件,以.cs源码、config配置、sln解决方案、exe可执行程序为主,辅以Resources资源、pdb调试符号等,整体仅78KB,结构精简,适合直接加载运行学习。已有1579人学习下载,可用于快速验证摄像头调用方案、参考封装UsbCamera核心逻辑,减少对旧版API调试的时间成本。压缩包保留Git版本目录信息,便于理解工程结构与开发过程。 先说个场景:最近做一套小型视觉检测上位机,需求很朴实——USB摄像头实时预览,操作员点一下“开始录制”,现场画面落盘保存,后续出问题能直接翻录像。这需求听着简单,实际搜索“C#调用USB摄像头 录制和保存”,网上答案不少,但照着抄很容易翻车:有的只能预览录不了,有的录出来全黑,有的程序一关摄像头就被占住,下一次打开直接失败。这篇把我实际趟过一遍的完整路线记录下来,从选型、环境、录制核心逻辑到各种诡异坑,一次说清楚。
适合谁看?刚接手上位机、需要用普通USB摄像头做采集录像的C#开发,以及准备把视频录制功能加进现有WinForm/WPF项目的人。如果你是做海康、大华这类工业相机的,这套思路可以先当参考,但更建议走厂商SDK,原因后面细讲。
1. 选型那点事:为什么我绕了一大圈又回到OpenCV系
1.1 四个主流方案的对比
我最初的想法很直接:找一个现成的C#库调用摄像头,再把帧数写入文件。翻了半天,主流路线无外乎这四种:
| 方案 | 设备枚举 | 视频录制 | 维护情况 | 上手难度 |
|---|---|---|---|---|
| AForge.NET | 支持,简单 | 需要自己接FFmpeg | 2013年左右停止维护 | 低 |
| DirectShowLib | 很强,底层开放 | 要自己写COM采集+编码链路 | 社区维护,资料偏老 | 高 |
| OpenCvSharp4 | 一般,需配合DirectShow枚举 | VideoCapture+VideoWriter一条龙 | 活跃,跟着OpenCV走 | 中 |
| MediaFoundation | 系统级,C#封装少 | 系统和CPU压力更小 | 微软平台原生 | 很高 |
AForge.NET是老牌方案,WinForm快速做Demo很好用,但项目已经停摆了,它帮你封装好的VideoSource类后续没人填坑,一旦遇到摄像头不兼容,就得自己翻源码。DirectShowLib更适合做设备枚举和管理,真正要录制时,从GraphBuilder到SampleGrabber再到编码器,一整套COM链路写起来非常耗时间。MediaFoundation在Windows上性能不错,支持系统H264硬件编码,但C#侧几乎没有稳定成熟的封装,要自己写COM互操作或找第三方库,对普通项目来说成本偏高。
1.2 我更看重的,是“能尽快稳定跑起来”
我的核心需求是:把UVC摄像头的画面录下来落盘,后面可能还要加图像处理逻辑。OpenCvSharp4.Windows包一条命令安装就能跑,VideoCapture负责采集帧,VideoWriter负责编码封装,中间拿到的Mat对象还能顺手做图像处理,录制、预览、逻辑扩展都能兼顾。
实际编码时,OpenCV把图像采集、格式转换、编码输出全部做了,对一个做上位机的人来说省心很多。项目维护也不需要跟着某些停摆库的 issue 走,OpenCV迭代这么多年,摄像头兼容性比大多数个人封装要好。
1.3 例外情况:什么时候别用OpenCV
如果你手里是海康、大华这类工业相机,除非它已经被系统识别成标准UVC摄像头,否则我建议用厂商自己的SDK。工业相机走GigE或USB3 Vision协议,厂商SDK里对分辨率枚举、触发模式、曝光控制、丢包重传都有专门处理,比用OpenCV暴力调底层要稳定太多。
很多做视觉定位的工程师还会把VisionMaster、Halcon这类视觉软件和上位机联调,这种场景下相机对接通常走厂商驱动或独立采集线程,再通过SDK接口把图像帧抛给算法库,而不是让OpenCV去抢设备。所以“C#调用USB摄像头录制保存”这套方案,本质上是面向普通USB摄像头、UVC摄像头(包括ESP32-S3模拟出来的USB摄像头)的通用路线——只要Windows把设备识别成摄像头,OpenCV就能用同一套逻辑采集。
2. 环境搭建与最小可用Demo
2.1 引包和运行时DLL
用NuGet安装两个包就够了:
Install-Package OpenCvSharp4 Install-Package OpenCvSharp4.Windows很多人只装了第一个OpenCvSharp4,运行时直接抛“找不到OpenCvSharpExtern.dll”。这是正常的——第一个包只是托管层,原生DLL在Windows运行时包里。装上OpenCvSharp4.Windows后,OpenCvSharpExtern.dll和相关原生库才会进入输出目录。
还有一个容易被忽略的点:VideoWriter写入MP4时依赖opencv_videoio_ffmpeg开头的DLL。如果发布时精简文件把这类ffmpeg插件剪掉了,程序不报编译错,但运行到保存视频时会失败。遇到编码相关报错,第一时间检查输出目录里是否存在opencv_videoio_ffmpeg*.dll。
2.2 打开摄像头,抓一帧验证链路
我习惯先写一个最小Demo,验证“摄像头能打开、帧能读出来”,再去做录制。代码如下:
using OpenCvSharp; using (var capture = new VideoCapture(0)) // 0表示系统默认摄像头 { if (!capture.IsOpened()) { Console.WriteLine("摄像头打开失败"); return; } capture.Set(VideoCaptureProperties.FrameWidth, 1280); capture.Set(VideoCaptureProperties.FrameHeight, 720); capture.Set(VideoCaptureProperties.Fps, 30); using (Mat frame = new Mat()) { // 跳过前几帧,摄像头自动曝光需要时间 for (int i = 0; i < 10; i++) { capture.Read(frame); } if (!frame.Empty()) { Cv2.ImWrite("snapshot.jpg", frame); Console.WriteLine("拍照成功"); } } }注意Set(分辨率、帧率)必须在第一次Read前调用。有些摄像头驱动会忽略Set,失败后需要重新枚举支持的分辨率,这个坑第4章详细说。先用这张保存出来的jpg确认图像链路没问题,再继续做录制。
2.3 枚举设备名:不要死磕0号摄像头
只写VideoCapture(0)的问题在于:0号不一定是你的USB摄像头。笔记本自带摄像头、OBS虚拟摄像头、别的USB摄像头都可能占着0号,插拔顺序一变,设备号就乱了。
枚举设备名用DirectShowLib这个库比较方便:
using DirectShowLib; DsDevice[] devices = DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice); for (int i = 0; i < devices.Length; i++) { Console.WriteLine($"{i}: {devices[i].Name}"); }然后做一个下拉框让用户选设备,代码里用选中项索引去打开。很多带多个摄像头的设备,出厂摄像头是0号,USB摄像头是1号,热插拔后顺序还会变,靠固定索引迟早踩雷。不过DirectShowLib这些年更新不频繁,在.NET 6/8上使用时如果遇到兼容问题,可以用开源的DirectShowLib替代实现,或者只把它当枚举工具,采集录制仍走OpenCvSharp。
3. 录制保存:从“能抓帧”到“录一个能看的视频”
3.1 VideoWriter关键参数
OpenCvSharp里写入视频的核心类就是VideoWriter。构造参数有几个关键点:
using (var writer = new VideoWriter( @"D:\record\video.mp4", // 文件路径 FourCC.FromString("mp4v"), // 编码格式 30, // 帧率,单位fps new Size(1280, 720))) // 画面尺寸,必须和采集帧一致 { // 循环读帧并写入 }FourCC编码格式这个参数最容易出问题,我的实测建议是:
| FourCC字符串 | 文件后缀 | 兼容性 | 文件体积 |
|---|---|---|---|
| mp4v | .mp4 | 兼容性最好,一般播放器都能开 | 中等 |
| MJPG | .avi | 兼容性好,编码快 | 很大 |
| XVID | .avi | 兼容性尚可 | 中等偏大 |
| avc1/H264 | .mp4 | 依赖OpenCV构建是否包含H264编码器,不一定成功 | 较小 |
第一次做录制功能,优先用mp4v配.mp4,成功率最高。H264的编码器在Windows上的OpenCV发布版里支持情况不稳定,折腾半天最后可能发现是OpenCV的ffmpeg库没带相关编码器,不值得在这个阶段浪费时间。
3.2 写入循环和实际帧率补偿
很多“能写文件但视频时间轴不对”的问题,根源都是帧率参数写错了。
摄像头设置成30fps,不代表实际就能跑到30fps。USB摄像头受光照、分辨率、USB带宽影响,实际可能只有十几帧。如果VideoWriter还按30fps封装,录出来的视频相当于把实际时间压缩了,播放时会看到明显的快进效果。
我用的办法比较务实:打开摄像头后先预热1秒,统计实际读取帧数,用统计值去创建VideoWriter。
using (var temp = new Mat()) { DateTime start = DateTime.UtcNow; int count = 0; while ((DateTime.UtcNow - start).TotalMilliseconds < 1000) { if (capture.Read(temp) && !temp.Empty()) { count++; } } // 这1秒统计到的count就是相近的实际帧率 } // 用实际帧率创建写入器 using (var writer = new VideoWriter( @"D:\record\video.mp4", FourCC.FromString("mp4v"), count > 0 ? count : 15, new Size(1280, 720))) { // 正式录制循环 }这套逻辑简单可行,因为普通追溯场景允许一秒级误差,没必要为时间戳精度去上FFmpeg管道。如果你确实需要逐帧精确时间戳,就得放弃VideoWriter,改用FFmpeg命令行管道输入原始帧,那是另一个更复杂的工程,一般只有在做特殊检测记录时才会用到。
3.3 采集线程与写入线程分离
单线程一边读帧一边写文件,最大的风险是写文件卡一下、采集就丢好几帧。项目里录制连续性要求高的话,我建议把采集和写入拆成两个线程,中间用队列缓冲。
using System.Collections.Concurrent; BlockingCollection<Mat> frameQueue = new BlockingCollection<Mat>(boundedCapacity: 30); // 采集线程 Task producer = Task.Run(() => { while (isRecording && capture.Read(mat)) { Mat copy = mat.Clone(); // 必须拷贝,否则后续被覆盖 if (!frameQueue.TryAdd(copy, 500)) // 队列满就丢帧,避免越积越多 { copy.Dispose(); } } frameQueue.CompleteAdding(); }); // 写入线程 Task consumer = Task.Run(() => { foreach (Mat m in frameQueue.GetConsumingEnumerable()) { writer.Write(m); m.Dispose(); // 用完立即释放,否则内存持续上涨 } });注意Mat是引用类型,直接塞进队列后面会被下一帧覆盖,所以必须Clone一份。队列设一个上限,满了用TryAdd,而不是Add,否则录制长时间卡顿会把内存撑爆。写入线程消费完要Dispose,不然内存占用只增不减。
4. 实测中踩过的坑
4.1 录出来的视频全黑
全黑是最常见的录制问题。排查链路是这样:先看预览画面,如果预览正常只是录像全黑,问题90%出在编码器或像素格式不匹配。
我第一次用avc1编码写MP4,写入时没有任何报错,看完文件全黑,也没有画面。换成mp4v后正常了。如果你的代码里writer创建失败其实不会立刻抛异常,最好写完一个文件后判断writer.IsOpened(),确认编码器是否真正初始化成功。
还有一个容易被忽略的原因:摄像头自动曝光。刚打开摄像头的第一两秒画面可能接近全黑,这时候写入文件,开头部分就会有一段黑屏。所以正式录制前多读几帧丢弃,或者延迟启动写入,能明显改善。
4.2 设置分辨率无效
Set(1280, 720)后Read出来还是640×480,这种问题我遇到好几次。原因通常是摄像头硬件本身不支持这个分辨率,但驱动没有报错,只是默默忽略。
最好先枚举摄像头支持的分辨率,UVC摄像头可以通过DirectShow的IAMStreamConfig接口拿到,但代码比较啰嗦。一个简单的替代办法是打开摄像头后,用capture.Get(VideoCaptureProperties.FrameWidth)和FrameHeight回读一下实际值,确认是否设置成功。
如果分辨率没变,一是降低目标分辨率,改720P甚至640×480试试;二是用摄像头厂商的专用配置工具把分辨率固化到设备里,再让OpenCV去读。设完分辨率后记得用Read读一帧,用frame.Cols和frame.Rows确认实际尺寸,再去创建VideoWriter——否则writer尺寸和实际帧尺寸不一致,写出来的文件一样会坏。
4.3 摄像头占用与热插拔
摄像头被其他程序(比如另一个上位机、系统相机App)占用时,OpenCV的Open不一定报异常,但Read会一直失败或超时。我的做法是打开后立即读几帧作为连通性测试,连不上就提示用户关闭其他占用程序。
热插拔更麻烦。摄像头拔掉再插上,原VideoCapture对象可能已经无效了,要不要自动重连是个产品决策,但底层逻辑建议是:设备列表变化后,主动Release旧对象,重新枚举并按用户选择的名称重新打开。
简单热插拔检测可以用System.Management监听设备变更事件,也可以退而求其次,在UI线程每2秒轮询一次DsDevice设备数量,检测到数量变化后触发重连逻辑。
4.4 USB带宽与多路并发
多路USB摄像头同录是带宽大户。USB 2.0理论带宽480Mbps,实际有效传输也就30MB/s左右,两路1080P@30fps同时跑,一天下来必然掉帧。
实测心得:
- 优先插USB 3.0接口,UVC协议在USB 3.0下能跑更高带宽。
- 多路摄像头不要挤在同一个USB Hub上,尽量分散到不同USB控制器。
- 如果多路还要高清,稳妥做法是720P@15fps,再不行就换硬件采集卡或工业相机。
4.5 资源释放问题
录制完成后是不是只调writer.Release就完事了?不够。capture.Release、writer.Release、每一帧Mat的Dispose都要处理干净。否则最直接的表现是:第二次打开摄像头时设备被占用,或者内存持续上涨。
程序如果被无条件杀掉(任务管理器强杀、断电),摄像头驱动没来得及释放,摄像头灯会一直亮着。这时候不要重启上位机,先把进程杀掉,然后用设备管理器或系统重启让驱动复位,能省去很多“明明没程序调用摄像头但就是打不开”的排查时间。
5. 上位机场景的进阶调整
5.1 按时间戳分段录像
录制文件不能无限大,工业追溯场景通常按时间切片。最简单的方法就是每秒检查一下当前录像时长,超过阈值就关掉旧writer、创建新writer。文件命名直接用时间戳:
string path = $"record_{DateTime.Now:yyyyMMdd_HHmmss}.mp4";分段录像还有一个好处:如果中途断电导致文件没正常关闭,坏掉的文件只是当前这一小段,前面已经落盘的文件还能正常回放。单个文件越大,损坏损失越大。
5.2 定时拍照
有些追溯场景不只要视频,还要关键节点截图。在采集循环里加一个时间判断就行:
if ((DateTime.Now - lastSnapTime).TotalSeconds >= snapInterval) { Cv2.ImWrite($"snap_{DateTime.Now:yyyyMMdd_HHmmss}.jpg", frame); lastSnapTime = DateTime.Now; }定时拍照不用太频繁,太密集会频繁触发磁盘写入,影响采集线程稳定。
5.3 工业USB相机的选择建议
普通USB摄像头最大的优势是便宜、接入简单、UVC协议通用,但你别指望它做精密测量。画面畸变、色彩还原、低照度噪点、帧率稳定性都很难和工业相机比。
如果是视觉定位、缺陷检测这类精度要求高的项目,预算允许还是上工业USB相机。海康、大华这类品牌都有自己SDK,配合VisionMaster、Halcon这类视觉软件时,通常要把相机图像帧通过SDK取出后直接交给算法模块,而不是绕一层OpenCV再传——多绕一层,性能和多路并发都容易出问题。
最后说点个人体会:“C#调用USB摄像头录制保存”这件事,代码本身不难,难的是把“录制”当成一个长期稳定运行的功能去对待。我现在的习惯是固定的三步走:先花五分钟确认设备枚举和分辨率回读正确,再预热统计实际帧率,最后才上多线程写入模型。这些步骤省掉,后面十有八九要返工。你要是正好在配这个功能,把第二、三、四章的检查清单过一遍,能省一晚上调试时间。
本文还有配套的精品资源,点击获取