zoukankan      html  css  js  c++  java
  • [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

  • 相关阅读:
    《剑指offer》第六十八题(树中两个结点的最低公共祖先)
    《剑指offer》第六十七题(把字符串转换成整数)
    《剑指offer》第六十六题(构建乘积数组)
    《剑指offer》第六十五题(不用加减乘除做加法)
    ECShop 2.7.2版本,数据库表
    织梦在导航栏下拉菜单中调用当前栏目子类的方法
    让dedecms autoindex,itemindex 从0到1开始的办法!
    dedeCMS列表页中如何给前几条文章加单独样式?
    dedecms标签调用大全
    完美解决ecshop与jquery冲突兼容
  • 原文地址:https://www.cnblogs.com/pumbaa/p/14252755.html
Copyright © 2011-2022 走看看