【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%的降低。
相关推荐
q***82915 小时前
如何使用C#与SQL Server数据库进行交互
数据库·c#·交互
源之缘-OFD先行者6 小时前
10 万雷达点迹零卡顿回放:WPF + Vortice.Direct2D 多线程渲染实战
wpf
程序猿追7 小时前
异腾910B NPU实战:vLLM模型性能优化深度指南
性能优化
老前端的功夫7 小时前
Vue2中key的深度解析:Diff算法的性能优化之道
前端·javascript·vue.js·算法·性能优化
hixiong1238 小时前
C# OpenCVSharp实现Hand Pose Estimation Mediapipe
开发语言·opencv·ai·c#·手势识别
baivfhpwxf20238 小时前
SQL Server 服务端如何在其他电脑连接
c#
Dm_dotnet9 小时前
WPF/C#:使用Microsoft Agent Framework框架创建一个带有审批功能的终端Agent
c#
Dm_dotnet9 小时前
WPF/C#:使用Stylet中的IWindowManager用于显示等待窗体、对话框与消息框
c#
Dm_dotnet9 小时前
OpenCVSharp:ArUco 标记检测与透视变换
opencv
Jackson@ML9 小时前
360度看C#编程语言
开发语言·c#