C# USB 摄像头 OpenCV 视频picBox呈现,抓拍图像保存呈现。
1、winform 应用程序,两个picturebox空间,一个用于视频呈现,一个用于抓拍呈现。
2、引用包OpenCvSharp4、OpenCvSharp4.Extensions、OpenCvSharp4.runtime.win等。
1 // 定义一个部分类 Form2,继承自 Form 类,用于创建一个 Windows 窗体应用程序的窗口 2 public partial class Form2 : Form 3 { 4 // 修改成员变量 5 // 声明一个 volatile 修饰的 Bitmap 类型变量,用于存储最新的视频帧图像 6 // volatile 关键字确保该变量在多线程环境下的可见性,避免线程缓存导致的数据不一致问题 7 private volatile Bitmap _latestFrameBitmap; 8 // 声明一个用于线程同步的对象,用于对 _latestFrameBitmap 的访问进行加锁操作 9 private readonly object _bitmapLock = new object(); 10 // 声明一个 VideoCapture 类型的变量,用于捕获摄像头的视频流 11 private VideoCapture _capture; 12 // 声明一个 CancellationTokenSource 类型的变量,用于取消异步操作 13 private CancellationTokenSource _cts; 14 15 // 窗体的构造函数,在创建 Form2 实例时自动调用 16 public Form2() 17 { 18 // 调用 Windows 窗体设计器生成的初始化方法,初始化窗体的控件和布局 19 InitializeComponent(); 20 } 21 22 // 窗体加载事件处理方法,当窗体加载完成后自动触发 23 private async void Form2_Load(object sender, EventArgs e) 24 { 25 // 创建一个 VideoCapture 实例,参数 0 表示使用默认的摄像头设备 26 _capture = new VideoCapture(0); 27 // 检查摄像头是否成功打开 28 if (!_capture.IsOpened()) 29 { 30 // 如果摄像头无法打开,弹出消息框提示用户 31 MessageBox.Show("无法打开摄像头!"); 32 // 直接返回,不再执行后续的摄像头捕获操作 33 return; 34 } 35 36 // 创建一个 CancellationTokenSource 实例,用于取消异步操作 37 _cts = new CancellationTokenSource(); 38 try 39 { 40 // 调用异步方法 StartCapturingAsync 开始捕获摄像头的视频流,并传入取消令牌 41 await StartCapturingAsync(_cts.Token); 42 } 43 catch (OperationCanceledException) 44 { 45 // 捕获 OperationCanceledException 异常,表示操作被正常取消,不做额外处理 46 } 47 catch (Exception ex) 48 { 49 // 捕获其他异常,弹出消息框显示异常信息 50 MessageBox.Show($"捕获出错: {ex.Message}"); 51 } 52 } 53 54 // 异步方法,用于开始捕获摄像头的视频流 55 private async Task StartCapturingAsync(CancellationToken token) 56 { 57 // 使用 using 语句创建一个 Mat 对象,用于存储从摄像头读取的视频帧 58 // Mat 是 OpenCvSharp 库中用于表示图像矩阵的类,using 语句确保在使用完后自动释放资源 59 using (var frame = new Mat()) 60 { 61 // 进入一个循环,只要取消令牌未被触发,就持续捕获视频帧 62 while (!token.IsCancellationRequested) 63 { 64 // 从摄像头读取一帧视频数据,并存储到 frame 对象中 65 _capture.Read(frame); 66 // 检查读取的帧是否为空,如果为空则跳过本次循环,继续读取下一帧 67 if (frame.Empty()) continue; 68 69 // 将 OpenCvSharp 的 Mat 对象转换为 .NET 的 Bitmap 对象,以便在 Windows 窗体中显示 70 var newBitmap = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(frame); 71 72 // 更新最新帧 73 // 使用 lock 语句对 _bitmapLock 对象加锁,确保在多线程环境下对 _latestFrameBitmap 的访问是线程安全的 74 lock (_bitmapLock) 75 { 76 // 保存旧的 _latestFrameBitmap 对象 77 var old = _latestFrameBitmap; 78 // 将新的 Bitmap 对象赋值给 _latestFrameBitmap 79 _latestFrameBitmap = newBitmap; 80 // 如果旧的 _latestFrameBitmap 对象不为空,则释放其资源 81 old?.Dispose(); 82 } 83 84 // 调用异步方法 UpdateCameraPreviewAsync 更新摄像头预览界面 85 await UpdateCameraPreviewAsync(newBitmap); 86 // 异步延迟 30 毫秒,控制视频帧的捕获频率 87 await Task.Delay(30, token); 88 } 89 } 90 } 91 92 // 异步方法,用于更新摄像头预览界面 93 private async Task UpdateCameraPreviewAsync(Bitmap bitmap) 94 { 95 // 检查 picCamera 控件是否已经被释放,如果已经释放则释放传入的 Bitmap 对象并返回 96 if (picCamera.IsDisposed) 97 { 98 bitmap.Dispose(); 99 return; 100 } 101 102 try 103 { 104 // 检查当前线程是否需要通过 Invoke 方法在 UI 线程上执行操作 105 if (picCamera.InvokeRequired) 106 { 107 // 如果需要,则使用 BeginInvoke 方法在 UI 线程上调用 UpdateCamera 方法 108 picCamera.BeginInvoke(new Action(() => UpdateCamera(bitmap))); 109 } 110 else 111 { 112 // 如果不需要,则直接调用 UpdateCamera 方法 113 UpdateCamera(bitmap); 114 } 115 } 116 catch (ObjectDisposedException) 117 { 118 // 捕获 ObjectDisposedException 异常,释放传入的 Bitmap 对象 119 bitmap.Dispose(); 120 } 121 } 122 123 // 方法,用于更新摄像头预览界面 124 private void UpdateCamera(Bitmap newBitmap) 125 { 126 // 检查 picCamera 控件是否已经被释放,如果已经释放则释放传入的 Bitmap 对象并返回 127 if (picCamera.IsDisposed) 128 { 129 newBitmap.Dispose(); 130 return; 131 } 132 133 // 保存 picCamera 控件当前显示的图像 134 var old = picCamera.Image; 135 // 将新的 Bitmap 对象赋值给 picCamera 控件的 Image 属性,更新显示的图像 136 picCamera.Image = newBitmap; 137 // 如果旧的图像不为空,则释放其资源 138 old?.Dispose(); 139 } 140 141 // 优化后的抓拍方法,当点击抓拍按钮时触发 142 private async void catchBtn_Click(object sender, EventArgs e) 143 { 144 try 145 { 146 // 声明一个 Bitmap 类型的变量,用于存储抓拍的图像 147 Bitmap snapshot = null; 148 149 // 安全获取当前帧 150 // 使用 lock 语句对 _bitmapLock 对象加锁,确保在多线程环境下对 _latestFrameBitmap 的访问是线程安全的 151 lock (_bitmapLock) 152 { 153 // 检查 _latestFrameBitmap 是否不为空 154 if (_latestFrameBitmap != null) 155 { 156 // 如果不为空,则克隆一份 _latestFrameBitmap 对象赋值给 snapshot 变量 157 snapshot = (Bitmap)_latestFrameBitmap.Clone(); 158 } 159 } 160 161 // 检查 snapshot 是否为空,如果为空则弹出消息框提示用户当前没有可用的视频帧 162 if (snapshot == null) 163 { 164 MessageBox.Show("当前没有可用的视频帧"); 165 return; 166 } 167 168 // 异步保存防止界面卡顿 169 // 调用 Task.Run 方法在后台线程中执行 SaveSnapshot 方法,保存抓拍的图像 170 await Task.Run(() => SaveSnapshot(snapshot)); 171 } 172 catch (Exception ex) 173 { 174 // 捕获其他异常,弹出消息框显示异常信息 175 MessageBox.Show($"抓拍失败: {ex.Message}"); 176 } 177 } 178 179 // 方法,用于保存抓拍的图像 180 private void SaveSnapshot(Bitmap bitmap) 181 { 182 try 183 { 184 // 调用 GenerateUniqueFileName 方法生成一个唯一的文件名 185 var fileName = GenerateUniqueFileName(); 186 // 使用 using 语句确保在使用完 bitmap 对象后自动释放资源 187 using (bitmap) 188 { 189 // 将 bitmap 对象保存为 JPEG 格式的文件 190 bitmap.Save(fileName, ImageFormat.Jpeg); 191 192 // 显示预览(需要克隆新实例) 193 // 克隆一份 bitmap 对象用于在界面上显示预览 194 var previewBitmap = (Bitmap)bitmap.Clone(); 195 196 // 使用 BeginInvoke 方法在 UI 线程上执行更新预览界面和显示保存成功消息框的操作 197 BeginInvoke(new Action(() => 198 { 199 // 调用 UpdateSnapshotPreview 方法更新抓拍图像的预览界面 200 UpdateSnapshotPreview(previewBitmap); 201 // 弹出消息框显示图像保存的路径 202 MessageBox.Show($"图片已保存到:\n{fileName}"); 203 })); 204 } 205 } 206 catch (Exception ex) 207 { 208 // 捕获其他异常,使用 BeginInvoke 方法在 UI 线程上弹出消息框显示异常信息 209 BeginInvoke(new Action(() => 210 { 211 MessageBox.Show($"保存失败: {ex.Message}"); 212 })); 213 } 214 } 215 216 // 新增预览更新方法,用于更新抓拍图像的预览界面 217 private void UpdateSnapshotPreview(Bitmap newBitmap) 218 { 219 // 检查 pictureBoxSnapshot 控件是否已经被释放,如果已经释放则释放传入的 Bitmap 对象并返回 220 if (pictureBoxSnapshot.IsDisposed) 221 { 222 newBitmap.Dispose(); 223 return; 224 } 225 226 // 处理跨线程访问 227 // 检查当前线程是否需要通过 Invoke 方法在 UI 线程上执行操作 228 if (pictureBoxSnapshot.InvokeRequired) 229 { 230 // 如果需要,则使用 BeginInvoke 方法在 UI 线程上递归调用 UpdateSnapshotPreview 方法 231 pictureBoxSnapshot.BeginInvoke(new Action(() => UpdateSnapshotPreview(newBitmap))); 232 return; 233 } 234 235 // 更新控件并释放旧资源 236 // 保存 pictureBoxSnapshot 控件当前显示的图像 237 var old = pictureBoxSnapshot.Image; 238 // 将新的 Bitmap 对象赋值给 pictureBoxSnapshot 控件的 Image 属性,更新显示的图像 239 pictureBoxSnapshot.Image = newBitmap; 240 // 如果旧的图像不为空,则释放其资源 241 old?.Dispose(); 242 } 243 244 // 方法,用于生成一个唯一的文件名 245 private string GenerateUniqueFileName() 246 { 247 // 获取用户的图片文件夹路径 248 var docs = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures); 249 // 获取当前时间并格式化为指定的字符串格式 250 var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmssfff"); 251 // 组合图片文件夹路径、文件名前缀和时间戳,生成一个唯一的文件名 252 return Path.Combine(docs, $"Snapshot_{timestamp}.jpg"); 253 } 254 255 // 窗体关闭事件处理方法,当窗体关闭时自动触发 256 private void Form2_FormClosing(object sender, FormClosingEventArgs e) 257 { 258 // 取消异步操作 259 _cts?.Cancel(); 260 // 释放 CancellationTokenSource 对象的资源 261 _cts?.Dispose(); 262 263 // 释放所有资源 264 // 释放 VideoCapture 对象的资源 265 _capture?.Dispose(); 266 // 使用 lock 语句对 _bitmapLock 对象加锁,确保在多线程环境下对 _latestFrameBitmap 的访问是线程安全的 267 lock (_bitmapLock) 268 { 269 // 释放 _latestFrameBitmap 对象的资源 270 _latestFrameBitmap?.Dispose(); 271 } 272 273 // 清理预览图 274 // 检查 picCamera 控件的 Image 属性是否不为空 275 if (picCamera.Image != null) 276 { 277 // 保存 picCamera 控件当前显示的图像 278 var img = picCamera.Image; 279 // 将 picCamera 控件的 Image 属性设置为 null 280 picCamera.Image = null; 281 // 释放旧的图像资源 282 img.Dispose(); 283 } 284 285 // 新增快照预览清理 286 // 检查 pictureBoxSnapshot 控件的 Image 属性是否不为空 287 if (pictureBoxSnapshot.Image != null) 288 { 289 // 保存 pictureBoxSnapshot 控件当前显示的图像 290 var img = pictureBoxSnapshot.Image; 291 // 将 pictureBoxSnapshot 控件的 Image 属性设置为 null 292 pictureBoxSnapshot.Image = null; 293 // 释放旧的图像资源 294 img.Dispose(); 295 } 296 } 297 }