【问题标题】:Is there any way to directly map a raw array to an image in WPF?有没有办法直接将原始数组映射到 WPF 中的图像?
【发布时间】:2019-11-15 22:12:56
【问题描述】:

我编写了一个简单的路径跟踪器,希望能够在我的 GUI 中显示实时预览。目前,我正在使用一些 hacky 解决方案:

应用程序基于 .NET Core 3。 GUI 是 .NET Core 3 WPF。 GUI 是它自己的项目,而实际的渲染器是一个单独的类库。目前,我在 GUI 中使用 Image 对象,其源绑定到 Renderer.ImageBuffer,这是一个位图。我添加了一些 INotifyPropertyChanged 代码,让 GUI 知道它应该以用户设置的间隔刷新。我正在使用此转换器在此处显示一些内容:

public class ConvertBitmapToBitmapImage : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is System.Drawing.Bitmap rawBitmap)) return null;
        using var memory = new MemoryStream();
        rawBitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Bmp);
        memory.Position = 0;
        var bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.StreamSource = memory;
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        bitmapImage.EndInit();

        return bitmapImage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

但是,当我在渲染器中启用更新代码时,将原始位图数据转换为 GUI 友好的 BitmapImage 格式会产生大量开销,并且会大大降低渲染速度。据我所知,没有办法直接从后端访问 BitmapImage 对象类型,而不使类库成为 Core 3 中的 WPF 项目本身。有没有更好的方法可以做到这一点,或者更合适的控制用于将原始值映射到 GUI 中可查看的内容?

【问题讨论】:

  • 为什么源属性是 System.Drawing.Bitmap?当您问“有什么方法可以将原始数组直接映射到 WPF 中的图像?”时,答案是:当然,只需将 Image 的 Source 属性绑定到 byte[] 类型的属性即可。内置类型转换使其开箱即用。
  • 呵呵,不开玩笑。雅每天都学习新东西。我得试一试。它期望字节数组采用什么格式才能使其正常工作?只是图像中像素总数大小的平面数组?
  • 当前解决方案的帧速率是多少?您确定是渲染需要额外的时间吗?
  • 如果您希望在 CPU 使用率最低的情况下获得最高性能(60fps,最终更高),您可以构建一个 Directx 12 资产,将其注入 WPF 视图框或其他东西中,并拥有例如 cuda- directx12 互操作。您的延迟将低于微秒。
  • @MatthewHeimlich 字节数组将包含一个编码的图像帧,例如PNG 或 JPEG。除非事先知道位图的宽度、高度和像素格式,否则绑定原始像素缓冲区会很困难。

标签: c# wpf .net-core


【解决方案1】:

您有时不得不在 WPF 应用程序中与System.Drawing.Bitmap“互操作”,这是不争的事实。您使用BitmapImage 是因为您想要BitmapSource,对吗?你很幸运! .NET Core 3 支持WriteableBitmap

您在 IValueConverter 实例中的转换使这非常棘手。无论如何,我们的想法是只保留一个WriteableBitmap 并使用WritePixels properly 来复制数据,而无需重新创建位图的开销。 WPF 将在每次渲染过程中更新,因此您一定会得到您的结果。我建议找到一种方法将WriteableBitmap“填充”到您的视图模型中并直接传递它,而不需要转换器。

您可能(可能)偶尔需要重新创建源,因为我怀疑您可能希望允许调整应用程序的大小。每当调整 GUI 大小时,您都需要将 newWriteableBitmap 调整为新大小。如果您正在开发 DPI 感知应用程序,您还必须确保正确设置 DPI。幸运的是,WriteableBitmap 可以为您管理 some of this(注意构造函数参数)。

更新

我运行了这段代码(和你的类似)

    //Load a bitmap in memory from file. Size is 1896 x 745, 
    System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(@"...");

    //Start a stopwatch to measure performance.
    Stopwatch watch = new System.Diagnostics.Stopwatch();
    watch.Start();

    //Perform 500 times.
    for (int i = 0; i < 500; i++)
    {
        using (var memory = new MemoryStream())
        {
            bmp.Save(memory, System.Drawing.Imaging.ImageFormat.Bmp);
            memory.Position = 0;

            var bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.StreamSource = memory;
            bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
            bitmapImage.EndInit();

            //testImage is an Image control in the UI.
            testImage.Source = bitmapImage;
        }
    }

    watch.Stop();
    System.Diagnostics.Debug.WriteLine(watch.ElapsedMilliseconds + " ms.");

    //Just one test run of this takes about 7000 ms!!!

这需要大约 7 秒才能运行。

然后,我运行这段代码:

    //Load a bitmap in memory from file. Size is 1896 x 745, 
    System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(@"...");

    //Create the BitmapSource (a WriteableBitmap). 96 is the dpi setting.
    WriteableBitmap writeableBMP =
        new WriteableBitmap(1896, 745, 96, 96, PixelFormats.Pbgra32, null);

    //Start a stopwatch to measure performance.
    Stopwatch watch = new System.Diagnostics.Stopwatch();
    watch.Start();

    //Perform 500 times.
    for (int i = 0; i < 500; i++)
    {
        System.Drawing.Rectangle rect = new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height);

        //Lock the bits to copy from the bitmap to the image source.
        BitmapData data =
            bmp.LockBits(rect, ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

        //Prepare the buffer size (System.Drawing.Bitmap specific)
        int bufferSize = rect.Height * data.Stride;

        //Prepare the write rectangle, of course this is the entire image.
        Int32Rect writeRectangle = new Int32Rect(0, 0, 1896, 745);

        //Write the pixels
        writeableBMP.WritePixels(writeRectangle, data.Scan0, bufferSize, data.Stride, 0, 0);

        //Unlock the bits, to prepare for another cycle.
        bmp.UnlockBits(data);

        testImage.Source = writeableBMP;   
    }

    watch.Stop();
    System.Diagnostics.Debug.WriteLine(watch.ElapsedMilliseconds + " ms.");

    //Multiple test runs of this take ~300 ms!!!

第二段代码,尽管其辉煌的草率,在约 300 毫秒内执行。它在每一步中复制整个位图。对于“几乎”最大化的屏幕 UI,1896 x 745 位图应该是相对平均的。

我建议使用WriteableBitmap 将在我的机器上与最初发布的代码实现相同结果的运行时间提高了大约 ~7000/300 > ~20 倍。您是否可以像我发布的那样尝试这个,如果它对您来说运行得更好,请告诉我?与您遇到完全相同的问题(当然切换到WriteableBitmap),我很难相信它only improves your running time by 1-2%。您可能需要分享更多关于您的 UI 渲染代码的细节,以便我可以尝试定位其他潜在的瓶颈。

当然,WPF 渲染管道的其余部分预计会有一些开销,因为我只测量代码的关键部分(相对粗略,不少于),但总的来说,WriteableBitmap 接近在您放弃 WPF 中的软件渲染以用于任何其他渲染引擎(例如硬件)之前,“尽其所能”。

(到目前为止,我已收到 1 次反对票和 1 次删除此答案的投票)

【讨论】:

  • 花了很多额外的工作来使一切都变得美好和线程安全,但我最终让您的解决方案在渲染时间上与没有预览的渲染相比只有 1-2% 的差异。将研究上述字节数组方法的工作原理。
  • 嗯.. 您是否有机会手动更新您的 GUI?你是如何刷新你的 GUI 的?另外,您的图像的分辨率是多少?如果将 WriteableBitmap 直接绑定到 ImageSource 属性,则应该很容易获得可容忍的帧速率,除非您在非常慢的系统上运行它,或者您的图像非常大。
  • @MatthewHeimlich 我已经用一些测试代码更新了我的答案。我得到了超过 20 倍的改进。您的代码中是否可能存在其他瓶颈?如果您分享更多详细信息,也许我们可以追踪到这一点。
猜你喜欢
  • 1970-01-01
  • 2018-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-06
  • 1970-01-01
  • 2019-11-20
  • 2022-01-04
相关资源
最近更新 更多