如何显示快速更新的图像而无需大量内存分配?

13
我在超声波机上有一个 WPF 应用程序,以每秒 30 帧的速度显示 C++ 生成的超声图像。
据我所知,在 WPF 中显示图像的正常流程是为您的图像创建 BitmapSource,并设置 Image 的 Source,然后它会失效并显示。
由于 BitmapSources 不实现 IDisposable,使用此方法强制我每秒创建 30 个 BitmapSources。对于一个 640x480 的 32bppArgb 格式图像,这大约是每秒分配 30MB 的内存,然后每 10 秒进行垃圾回收,导致可见的延迟。显然,这不是一个可接受的解决方案。
我的当前解决方案是:
在 C++ 中:我在托管的 C++ 中创建了一个 System.Drawing.Bitmap(WinForms 位图),通过指针执行 memcpy 来填充图片,使用 Graphics 对象执行一些额外的绘图操作,并在 ImageReceived 事件期间将其传递到 C#/WPF 中。
在 C# 中,Image.Source 设置为由 BitmapBuffer 生成的源,这是一种访问位图源原始数据的 hack 方法:请参阅 此链接。我使用 P/Invoke 调用 CopyMemory 将数据从 Bitmap.Scan0 复制到 BitmapBuffer。然后我使 Image 失效以更新屏幕,并 Dispose() Drawing.Bitmap 对象以释放内存。
虽然这种方法已经运作一段时间,但它似乎非常 hacky,我很难相信除了通过反射之外没有其他“适当”的方法来做到这一点。

问题:有更好的方法吗?

2个回答

13
如果您正在使用最新的WPF版本,请查看WriteableBitmap,您需要做更多的工作,但您将获得非常快速的更新。
快速搜索一下谷歌,您就可以找到一些示例。

你最终会使用更多的内存和 CPU 循环。由于所需的源是 GDI 位图,每次更新时都必须将其复制到 WriteableBitmap 中。上面的 AliasedBitmap 方法将自动反映在另一个位图中所做的更改。 - bsneeze
你所消耗的额外内存仅限于后备缓冲区;但优点在于你可以避免画面撕裂。就CPU周期而言,在我看来,内存复制几乎是相同的,你需要将位图数据从应用程序传输到显示器 - 而且在这方面,CopyPixels逻辑几乎是相同的。 - Scott
3
回顾过去,我相信多年后,我最终更加倾向于这种解决方案。我保留了一个单独的WriteableBitmap,并从超声机的图像缓冲区中使用Buffer.BlockCopy将数据复制到BackBuffer中。因此,随着时间的推移没有内存使用(没有任何分配),即使是较慢的双核处理器,也可以获得可靠的60FPS性能。 - Will Eddins
2
重要提示:如果您使用上述MSDN链接中的示例,请确保更改行“int pBackBuffer =(int)writeableBitmap.BackBuffer;”以使用(long)。否则,在64位系统中会崩溃。 - Justas

9

这里是我为我的项目编写的一些代码,用于在WPF BitmapSource和GDI Bitmap之间进行别名(共享内存)。

显然,你需要根据自己的需求进行调整,最终可能会感觉不那么“hacky”。

class AliasedBitmapSource : BitmapSource {
    private Bitmap source;
    public AliasedBitmapSource(Bitmap source) {
        this.source = source;
        this.pixelHeight = source.Height;
        this.pixelWidth = source.Width;
        this.dpiX = source.HorizontalResolution;
        this.dpiY = source.VerticalResolution;
    }

    public override event EventHandler DownloadCompleted;
    public override event EventHandler<ExceptionEventArgs> DownloadFailed;
    public override event EventHandler<ExceptionEventArgs> DecodeFailed;

    protected override Freezable CreateInstanceCore() {
        throw new NotImplementedException();
    }

    private readonly double dpiX;
    public override double DpiX {
        get {
            return dpiX;
        }
    }

    private readonly double dpiY;
    public override double DpiY {
        get {
            return dpiY;
        }
    }

    private readonly int pixelHeight;
    public override int PixelHeight {
        get {
            return pixelHeight;
        }
    }

    private readonly int pixelWidth;
    public override int PixelWidth {
        get {
            return pixelWidth;
        }
    }

    public override System.Windows.Media.PixelFormat Format {
        get {
            return PixelFormats.Bgra32;
        }
    }

    public override BitmapPalette Palette {
        get {
            return null;
        }
    }

    public unsafe override void CopyPixels(Int32Rect sourceRect, Array pixels, int stride, int offset) {
        BitmapData sourceData = source.LockBits(
        sourceRect.ToRectangle(),
        ImageLockMode.ReadWrite,
        System.Drawing.Imaging.PixelFormat.Format32bppArgb);

        fixed (byte* _ptr = &((byte[])pixels)[0]) {
            byte* dstptr = _ptr;
            byte* srcptr = (byte*)sourceData.Scan0;

            for (int i = 0; i < pixels.Length; ++i) {
                *dstptr = *srcptr;
                ++dstptr;
                ++srcptr;
            }
        }

        source.UnlockBits(sourceData);
    }
}

public static class Extensions {
    public static Rectangle ToRectangle(this Int32Rect me) {
        return new Rectangle(
        me.X,
        me.Y,
        me.Width,
        me.Height);
    }

    public static Int32Rect ToInt32Rect(this Rectangle me) {
        return new Int32Rect(
        me.X,
        me.Y,
        me.Width,
        me.Height);
    }
}

*我所说的“写”是指“匆忙拼凑而成”


使用Marshal.Copy而不是不安全的代码甚至更快,而且不需要不安全的块。 - Andreas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接