【WPF】使用 WriteableBitmap 提升 Image 性能

【WPF】使用 WriteableBitmap 提升 Image 性能

  • 前言
  • [WriteableBitmap 背景](#WriteableBitmap 背景)
  • [WriteableBitmap 渲染原理](#WriteableBitmap 渲染原理)
  • [WriteableBitmap 使用技巧](#WriteableBitmap 使用技巧)
  • 案例

前言

由于中所周不知的原因,WPF 中想要快速的更新图像的显示速率一直以来都是一大难题。在本文中,我将分享一些我对于 WPF 领域的经验和见解。虽然我并不是这方面的专家,但是希望通过我的分享,能够为大家提供一些有用的信息和思考角度。

WriteableBitmap 背景

WriteableBitmap 继承至 System.Windows.Media.Imaging.BitmapSource

"巨硬" 官方介绍:  WriteableBitmap 类

WriteableBitmap使用 类可按帧更新和呈现位图。 这对于生成算法内容(如分形图像)和数据可视化(如音乐可视化工具)非常有用。
WriteableBitmap 使用两个缓冲区后台缓冲区 在系统内存中分配,并累积当前未显示的内容。 前端缓冲区 在系统内存中分配,并包含当前显示的内容。 呈现系统将前缓冲区复制到视频内存中以供显示。
两个线程使用这些缓冲区。 用户界面 (UI) 线程生成 UI,但不会将其呈现在屏幕上。 UI 线程响应用户输入、计时器和其他事件。 一个应用程序可以有多个 UI 线程。 呈现线程编写和呈现来自 UI 线程的更改。 每个应用程序只有一个呈现线程。
UI 线程将内容写入后台缓冲区。 呈现线程从前缓冲区读取内容并将其复制到视频内存。 使用更改的矩形区域跟踪对后台缓冲区所做的更改。
调用其中 WritePixels 一个重载以自动更新和显示后台缓冲区中的内容。
为了更好地控制更新,并且要对后台缓冲区进行多线程访问,请使用以下工作流:

  1. Lock 调用 方法以保留更新的后台缓冲区。
  2. 通过访问 属性获取指向后台缓冲区的 BackBuffer 指针。
  3. 将更改写入后台缓冲区。 锁定时 WriteableBitmap ,其他线程可能会将更改写入后台缓冲区。
  4. AddDirtyRect 调用 方法以指示已更改的区域。
  5. Unlock 调用 方法以释放后台缓冲区并允许在屏幕上演示。
    将更新发送到呈现线程时,呈现线程会将更改后的矩形从后缓冲区复制到前缓冲区。 呈现系统控制此交换以避免死锁和重绘项目。

WriteableBitmap 渲染原理

  • 在调用 WriteableBitmapAddDirtyRect 方法的时候,实际上是调用 MILSwDoubleBufferedBitmap.AddDirtyRect,这是 WPF 专门为 WriteableBitmap 而提供的非托管代码的双缓冲位图的实现。

  • WriteableBitmap 内部数组修改完毕之后,需要调用 Unlock 来解锁内部缓冲区的访问,这时会提交所有的修改。

WriteableBitmap 使用技巧

  1. WriteableBitmap 的性能瓶颈源于对脏区的重新渲染。
    • 脏区为 0 或者不在可视化树渲染,则不消耗性能。
    • 只要有脏区,渲染过程就会开始成为性能瓶颈。
      • CPU 占用基础值就很高了。
      • 脏区越大,CPU 占用越高,但增幅不大。
  2. 内存拷贝不是 WriteableBitmap 的性能瓶颈。
    • 建议使用 Windows API 或者 .NET API 来拷贝内存数据。

特殊的应用场景,可以适当调整下自己写代码的策略:

  • 如果你希望有较大脏区的情况下降低 CPU 占用,可以考虑降低 WriteableBitmap 脏区的刷新率。
  • 如果你希望 WriteableBitmap 有较低的渲染延迟,则考虑减小脏区。

案例

测试 Demo 使用 OpenCvSharp 将视频帧读取出来,将视频帧图像数据通过 WriteableBitmap 渲染到界面的 Image 控件。

核心源码

  • 核心代码,利用双缓存区更新位图图像信息
csharp 复制代码
private void ShowImage()
{
    Bitmap.Lock();

    bitmap = frame.ToBitmap();

    bitmapData = bitmap.LockBits(new Rectangle(new System.Drawing.Point(0, 0), bitmap.Size),
        System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);

    Bitmap.WritePixels(rect, bitmapData.Scan0, bitmapData.Height * bitmapData.Stride, bitmapData.Stride, 0, 0);

    bitmap.UnlockBits(bitmapData);
    bitmap.Dispose();

    Bitmap.Unlock();
}

完整的 ViewModel 代码

csharp 复制代码
public class MainWindowViewModel : Prism.Mvvm.BindableBase
{
    #region 属性、变量、命令

    private WriteableBitmap _bitmap;
    /// <summary>
    /// UI绑定的资源对象
    /// </summary>                
    public WriteableBitmap Bitmap
    {
        get => _bitmap;
        set => SetProperty(ref _bitmap, value);
    }

    /// <summary>
    /// OpenCvSharp 视频捕获对象
    /// </summary>
    private static VideoCapture videoCapture;

    /// <summary>
    /// 视频帧
    /// </summary>
    private static Mat frame = new Mat();

    private static BitmapData bitmapData = new BitmapData();
	
	private static Bitmap bitmap;
    
    Int32Rect rect;

    static int width = 0, height = 0;

    /// <summary>
    /// 打开文件
    /// </summary>
    public DelegateCommand OpenFileCommand { get; set; }

    public DelegateCommand MNCommand { get; set; }

    #endregion

    public MainWindowViewModel()
    {
        videoCapture = new VideoCapture();

        OpenFileCommand = new DelegateCommand(OpenFile);
        MNCommand = new DelegateCommand(MN);
    }

    #region 私有方法

    private void OpenFile()
    {
        OpenFileDialog open = new OpenFileDialog()
        {
            Multiselect = false,
            Title = "请选择文件",
            Filter = "视频文件(*.mp4, *.wmv, *.mkv, *.flv)|*.mp4;*.wmv;*.mkv;*.flv|所有文件(*.*)|*.*"
        };

        if (open.ShowDialog() is true)
        {           
            ShowMove(open.FileName);
        }
    }

    /// <summary>
    /// 获取视频
    /// </summary>
    /// <param name="fileName">文件路径</param>
    private void ShowMove(string fileName)
    {
        videoCapture.Open(fileName, VideoCaptureAPIs.ANY);

        if (videoCapture.IsOpened())
        {
            var timer = (int)Math.Round(1000 / videoCapture.Fps) - 8;
            width = videoCapture.FrameWidth;
            height = videoCapture.FrameHeight;

            Bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
            rect = new Int32Rect(0, 0, Bitmap.PixelWidth, Bitmap.PixelHeight);

            while (true)
            {
                videoCapture.Read(frame);
                if (!frame.Empty())
                {
                    ShowImage();
                    Cv2.WaitKey(timer);
                }
            }
        }
    }

    private void ShowImage()
    {
        Bitmap.Lock();

        bitmap = frame.ToBitmap();

        bitmapData = bitmap.LockBits(new Rectangle(new System.Drawing.Point(0, 0), bitmap.Size),
            System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);

        Bitmap.WritePixels(rect, bitmapData.Scan0, bitmapData.Height * bitmapData.Stride, bitmapData.Stride, 0, 0);

        bitmap.UnlockBits(bitmapData);
        bitmap.Dispose();

        Bitmap.Unlock();
    }
}

测试结果

测试结果,经供参考,更精准的性能测试请使用专业工具。

  • VS Debug模式下的性能监测,以及Windows任务管理器中的资源占用,可以看出各项资源的使用是比较稳定的。
  • 发布之后独立运行资源的占用应该会有5%的降低。
相关推荐
bluefox19793 小时前
使用 Oracle.DataAccess.Client 驱动 和 OleDB 调用Oracle 函数的区别
开发语言·c#
弗锐土豆3 小时前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部
如若1233 小时前
利用 `OpenCV` 和 `Matplotlib` 库进行图像读取、颜色空间转换、掩膜创建、颜色替换
人工智能·opencv·matplotlib
鲤籽鲲5 小时前
C# MethodTimer.Fody 使用详解
开发语言·c#·mfc
工业3D_大熊5 小时前
3D可视化引擎HOOPS Luminate场景图详解:形状的创建、销毁与管理
java·c++·3d·docker·c#·制造·数据可视化
威桑5 小时前
CMake + mingw + opencv
人工智能·opencv·计算机视觉
yngsqq6 小时前
c#使用高版本8.0步骤
java·前端·c#
大白要努力!6 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
只怕自己不够好7 小时前
《OpenCV 图像基础操作全解析:从读取到像素处理与 ROI 应用》
人工智能·opencv·计算机视觉
嵌入式大圣7 小时前
嵌入式系统与OpenCV
人工智能·opencv·计算机视觉