由于中所周不知的原因,WPF 中想要快速的更新图像的显示速率一直以来都是一大难题。在本文中,我将分享一些我对于 WPF 领域的经验和见解。虽然我并不是这方面的专家,但是希望通过我的分享,能够为大家提供一些有用的信息和思考角度。
WriteableBitmap
继承至 System.Windows.Media.Imaging.BitmapSource
“巨硬” 官方介绍: ?WriteableBitmap 类
WriteableBitmap
使用 类可按帧更新和呈现位图。 这对于生成算法内容(如分形图像)和数据可视化(如音乐可视化工具)非常有用。
类
WriteableBitmap
使用两个缓冲区。 后台缓冲区 在系统内存中分配,并累积当前未显示的内容。 前端缓冲区 在系统内存中分配,并包含当前显示的内容。 呈现系统将前缓冲区复制到视频内存中以供显示。
两个线程使用这些缓冲区。 用户界面 (UI) 线程生成 UI,但不会将其呈现在屏幕上。 UI 线程响应用户输入、计时器和其他事件。 一个应用程序可以有多个 UI 线程。 呈现线程编写和呈现来自 UI 线程的更改。 每个应用程序只有一个呈现线程。
UI 线程将内容写入后台缓冲区。 呈现线程从前缓冲区读取内容并将其复制到视频内存。 使用更改的矩形区域跟踪对后台缓冲区所做的更改。
调用其中
WritePixels
一个重载以自动更新和显示后台缓冲区中的内容。
为了更好地控制更新,并且要对后台缓冲区进行多线程访问,请使用以下工作流:
Lock
调用 方法以保留更新的后台缓冲区。- 通过访问 属性获取指向后台缓冲区的
BackBuffer
指针。- 将更改写入后台缓冲区。 锁定时
WriteableBitmap
,其他线程可能会将更改写入后台缓冲区。AddDirtyRect
调用 方法以指示已更改的区域。Unlock
调用 方法以释放后台缓冲区并允许在屏幕上演示。
将更新发送到呈现线程时,呈现线程会将更改后的矩形从后缓冲区复制到前缓冲区。 呈现系统控制此交换以避免死锁和重绘项目。
在调用 WriteableBitmap
的 AddDirtyRect
方法的时候,实际上是调用 MILSwDoubleBufferedBitmap.AddDirtyRect
,这是 WPF 专门为 WriteableBitmap
而提供的非托管代码的双缓冲位图的实现。
在 WriteableBitmap
内部数组修改完毕之后,需要调用 Unlock
来解锁内部缓冲区的访问,这时会提交所有的修改。
WriteableBitmap
的性能瓶颈源于对脏区的重新渲染。
WriteableBitmap
的性能瓶颈。
Windows API
或者 .NET API
来拷贝内存数据。特殊的应用场景,可以适当调整下自己写代码的策略:
WriteableBitmap
脏区的刷新率。WriteableBitmap
有较低的渲染延迟,则考虑减小脏区。测试 Demo 使用 OpenCvSharp 将视频帧读取出来,将视频帧图像数据通过 WriteableBitmap
渲染到界面的 Image
控件。
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 代码
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();
}
}
测试结果,经供参考,更精准的性能测试请使用专业工具。