[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

posted @ 2021-01-08 18:04  孤独成派  阅读(2624)  评论(2编辑  收藏  举报