[C#] 使用Accord.Net,实现相机画面采集,视频保存及裁剪视频区域,利用WriteableBitmap高效渲染
添加Nuget引用:Accord.Video.FFMPEG、Accord.Video.DirectShow;
发现电脑的视频采集设备,及获取视频采集设备的采集参数:
/// <summary> /// 枚举视频设备 /// </summary> /// <returns></returns> public static IEnumerable<VideoDevice> EnumerateVideoDevices () { var videoDevices = new FilterInfoCollection (FilterCategory.VideoInputDevice); // 筛选视频输入设备 foreach (var videoDevice in videoDevices) { var deviceName = videoDevice.Name; var videoCaptureDevice = new VideoCaptureDevice (videoDevice.MonikerString); yield return new VideoDevice { FriendlyName = videoDevice.Name, // 设备的友好名称 MonikerName = videoDevice.MonikerString, // 设备的唯一标识符,用于区分哪个设备 VideoCapabilities = videoCaptureDevice.VideoCapabilities.Select (q => new VideoCapabilities { FrameWidth = q.FrameSize.Width, // 帧宽 FrameHeight = q.FrameSize.Height, // 帧高 AverageFrameRate = q.AverageFrameRate // 平均帧率 }) }; } }
选择设备及采集参数之后,打开相机:
public VideoCapture (VideoDevice device, VideoCapabilities videoCapabilities) { this.device = device; this.videoCapabilities = videoCapabilities; Name = device.FriendlyName; // 相机名称 videoCaptureDevice = new VideoCaptureDevice (device.MonikerName); // 视频输入设备 var capabilities = videoCaptureDevice.VideoCapabilities.FirstOrDefault (q => q.FrameSize.Width == videoCapabilities.FrameWidth && q.FrameSize.Height == videoCapabilities.FrameHeight && q.AverageFrameRate == videoCapabilities.AverageFrameRate); if (capabilities != null) videoCaptureDevice.VideoResolution = capabilities; // 选择采集参数 videoCaptureDevice.NewFrame += OnNewFrame; // 监听视频帧回调 relativeRect = bmp_relative_rect = new Rect (new Size (1, 1)); // 设置完整裁剪区域 bmp_absolute_rect.Width = frameWidth = videoCapabilities.FrameWidth; // 帧宽 bmp_absolute_rect.Height = frameHeight = videoCapabilities.FrameHeight; // 帧高 } public Boolean Start (out String errMsg) { errMsg = null; if (!videoCaptureDevice.IsRunning) videoCaptureDevice.Start (); // 打开设备 return true; }
关闭相机:
public Boolean Stop (out String errMsg) { errMsg = null; if (videoCaptureDevice.IsRunning) videoCaptureDevice.Stop (); // 关闭设备 return true; }
拍摄时,要先有画面回调,即必须保证已有一帧图像,只要把最新的一帧图像保存成图片即可:
public Boolean TakePhoto (String photoFile, out String errMsg) { errMsg = null; if (writeableBitmap == null) { // 等待画面渲染 SpinWait.SpinUntil (() => { return writeableBitmap != null || !IsStarted; }); if (writeableBitmap == null || IsStarted) return false; } try { // 将WriteableBitmap保存成jpg var renderTargetBitmap = new RenderTargetBitmap (writeableBitmap.PixelWidth, writeableBitmap.PixelHeight, writeableBitmap.DpiX, writeableBitmap.DpiY, PixelFormats.Default); DrawingVisual drawingVisual = new DrawingVisual (); using (var dc = drawingVisual.RenderOpen ()) { dc.DrawImage (writeableBitmap, new Rect (0, 0, writeableBitmap.PixelWidth, writeableBitmap.PixelHeight)); } renderTargetBitmap.Render (drawingVisual); JpegBitmapEncoder bitmapEncoder = new JpegBitmapEncoder (); bitmapEncoder.Frames.Add (BitmapFrame.Create (renderTargetBitmap)); var folder = Path.GetDirectoryName (photoFile); if (!Directory.Exists (folder)) Directory.CreateDirectory (folder); using (var fs = File.OpenWrite (photoFile)) { bitmapEncoder.Save (fs); } return true; } catch (Exception ex) { errMsg = ex.GetBaseException ().Message; return false; } }
开始录像,使用VideoFileWriter保存视频,使用StopWatch计算每一帧的时间戳:
public Boolean BeginRecord (String videoFile, out String errMsg) { errMsg = null; this.videoFile = null; if (IsRecording) return true; try { var folder = Path.GetDirectoryName (videoFile); if (!Directory.Exists (folder)) Directory.CreateDirectory (folder); videoFileWriter = new VideoFileWriter (); videoFileWriter.Open (videoFile, bmp_absolute_rect.Width, bmp_absolute_rect.Height, videoCapabilities.AverageFrameRate, VideoCodec.MPEG4); // 帧率从采集参数获取,以MP4格式保存 if (videoFileWriter.IsOpen) { this.spf = 1000 / videoCapabilities.AverageFrameRate; // 计算一帧所需毫秒数 this.videoFile = videoFile; if (this.stopwatch == null) this.stopwatch = new Stopwatch (); // 初始化计时器,计算每一帧的时间错 } return IsRecording; } catch (Exception ex) { errMsg = ex.GetBaseException ().Message; return false; } }
停止录像:
public Boolean EndRecord (out String videoFile, out String errMsg) { errMsg = null; videoFile = null; if (!IsRecording) return true; videoFile = this.videoFile; this.videoFile = null; videoFileWriter.Close (); videoFileWriter.Dispose (); videoFileWriter = null; this.stopwatch.Reset (); return true; }
设置裁剪区域:
public Boolean SetRenderRect (Rect rect, out String errMsg) { errMsg = null; if (IsRecording) { errMsg = "录像期间不允许修改裁剪区域"; return false; } // 验证数据合理性 if (rect.X < 0 || rect.Y < 0 || rect.X > 1 || rect.Y > 1 || rect.X + rect.Width > 1 || rect.Y + rect.Height > 1) { errMsg = "裁剪区域超出有效范围"; return false; } this.relativeRect = rect; return true; }
视频文件写入的时间戳处理:
private void OnNewFrame (Object sender, NewFrameEventArgs e) { var bmp = e.Frame; var bmpData = bmp.LockBits (bmp_absolute_rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb); if (IsRecording) { if (!stopwatch.IsRunning) { stopwatch.Restart (); // 启动计时器 frameIndex = 0; videoFileWriter.WriteVideoFrame (bmpData); // 写入第一帧 } else { var frame_index = (UInt32) (stopwatch.ElapsedMilliseconds / spf); // 计算当前帧是第几帧 if (frameIndex != frame_index) { frameIndex = frame_index; videoFileWriter.WriteVideoFrame (bmpData, frameIndex); // 只有不同帧索引才写入,否则会引发异常 } } } bmp.UnlockBits (bmpData); bmp.Dispose (); }
使用WriteableBitmap渲染Bitmap,在第一帧时创建WriteableBitmap对象,之后将Bitmap像素数据写入WriteableBitmap的后台缓冲区,再监听程序渲染事件CompositionTarget.Rendering从后台缓冲区更新画面:
private WriteableBitmap _writeableBitmap; private WriteableBitmap writeableBitmap { get => this._writeableBitmap; set { if (this._writeableBitmap == value) return; if (this._writeableBitmap == null) CompositionTarget.Rendering += OnRender; else if (value == null) CompositionTarget.Rendering -= OnRender; this._writeableBitmap = value; this.ImageSourceChanged?.Invoke (value); } } private void OnNewFrame (Object sender, NewFrameEventArgs e) { var bmp = e.Frame; if (writeableBitmap == null || bmp.Width != frameWidth || bmp.Height != frameHeight || relativeRect != bmp_relative_rect) { // 创建新的WriteableBitmap frameWidth = bmp.Width; frameHeight = bmp.Height; frameRect = new System.Drawing.Rectangle (0, 0, bmp.Width, bmp.Height); bmp_relative_rect = relativeRect; bmp_absolute_rect.X = (Int32) (bmp.Width * relativeRect.X); bmp_absolute_rect.Y = (Int32) (bmp.Height * relativeRect.Y); bmp_absolute_rect.Width = (Int32) (bmp.Width * relativeRect.Width); bmp_absolute_rect.Height = (Int32) (bmp.Height * relativeRect.Height); context.Send (n => { writeableBitmap = new WriteableBitmap (bmp_absolute_rect.Width, bmp_absolute_rect.Height, 96, 96, PixelFormats.Bgr24, null); bmp_stride = writeableBitmap.BackBufferStride; bmp_length = bmp_stride * bmp_absolute_rect.Height; bmp_backBuffer = writeableBitmap.BackBuffer; if (IsRecording) { // 创建新的录像 if (EndRecord (out String videoFile, out _)) BeginRecord (videoFile, out _); } }, null); } var bmpData = bmp.LockBits (bmp_absolute_rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb); var bmpDataPtr = bmpData.Scan0; var bmpDataStride = bmpData.Stride; if (bmp_stride == bmpDataStride) Memcpy (bmp_backBuffer, bmpDataPtr, bmp_length); else { // 逐行复制 var targetPtr = bmp_backBuffer; var yPtr = bmpDataPtr; // 指向每一行的开始 var length = Math.Min (bmp_stride, bmpDataStride); for (var i = 0; i < bmpData.Height; i++) { Memcpy (targetPtr, yPtr, length); yPtr += bmpDataStride; targetPtr += bmp_stride; } } bmp.UnlockBits (bmpData); bmp.Dispose (); Interlocked.Exchange (ref newFrame, 1); } private void OnRender (Object sender, EventArgs e) { var curRenderingTime = ((RenderingEventArgs) e).RenderingTime; if (curRenderingTime == lastRenderingTime) return; lastRenderingTime = curRenderingTime; if (Interlocked.CompareExchange (ref newFrame, 0, 1) != 1) return; var bmp = this.writeableBitmap; bmp.Lock (); bmp.AddDirtyRect (new Int32Rect (0, 0, bmp.PixelWidth, bmp.PixelHeight)); bmp.Unlock (); }
项目代码已上传至Github:https://github.com/LowPlayer/CameraCapture.git