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 }

 

posted @ 2025-03-05 10:15  摩诘  阅读(484)  评论(0)    收藏  举报