C# 如何在非UI线程中编辑WritableBitmap.BackBuffer?

C# 如何在非UI线程中编辑WritableBitmap.BackBuffer?,c#,.net,wpf,C#,.net,Wpf,我的应用程序运行CPU密集型算法来编辑放置在WPF窗口中的图像。我需要在一个背景线程的版本完成。但是,尝试在非UI线程中编辑可写位图的BackBuffer会引发InvalidOperationException private WriteableBitmap writeableBitmap; private void button1_Click(object sender, RoutedEventArgs e) { // Create WritableB

我的应用程序运行CPU密集型算法来编辑放置在WPF窗口中的图像。我需要在一个背景线程的版本完成。但是,尝试在非UI线程中编辑可写位图的BackBuffer会引发InvalidOperationException

    private WriteableBitmap writeableBitmap;

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        // Create WritableBitmap in UI thread.
        this.writeableBitmap = new WriteableBitmap(10, 10, 96, 96, PixelFormats.Bgr24, null);
        this.image1.Source = this.writeableBitmap;

        // Run code in non UI thread.
        new Thread(
            () =>
            {
                // 'Edit' bitmap in non UI thread.
                this.writeableBitmap.Lock(); // Exception: The calling thread cannot access this object because a different thread owns it.

                // ... At this place the CPU is highly loaded, we edit this.writeableBitmap.BackBuffer.

                this.writeableBitmap.Unlock();
            }).Start();
    }
我已经阅读了几十本手册,所有这些手册都告诉我在UI线程中执行BackBuffer版本(即


如何在非UI线程中编辑WritableBitmap.BackBuffer,而不进行任何无用的缓冲区复制/克隆?

在WPF中,跨线程调用是使用Dispatcher类完成的

在非UI线程中,需要获取创建WritableBitmap的线程的调度程序实例

然后在该调度器上调用Invoke(如果希望异步,则调用BeginInvoke)


Invoke然后调用一个委托函数,其中BackBuffer是编辑的

除了Klaus78所说的,我建议采用以下方法:

  • 通过在线程中的单独缓冲区(例如,
    字节[]
    )上执行异步“位图编辑”代码。不要每次需要执行异步操作时都创建新线程。这就是ThreadPool的用途

  • 在WriteableBitmap的调度程序中复制编辑的缓冲区。无需锁定/解锁

  • 例如:

    private byte[] buffer = new buffer[...];
    
    private void UpdateBuffer()
    {
        ThreadPool.QueueUserWorkItem(
            o =>
            {
                // write data to buffer...
                Dispatcher.BeginInvoke((Action)(() => writeableBitmap.WritePixels(..., buffer, ...)));
            });
    }
    

    正如克莱门斯所说,这是不可能的

    你有三个选择:

    1) 按照克莱门斯的建议,在缓冲区中进行编辑,并在完成后进行blit

    2) 在非常小的块中进行编辑,并在GUI线程上以很好的优先级安排它们。如果您将工作块保持足够小,GUI将保持响应,但显然这会使编辑代码复杂化

    3) 组合1和2。在另一个线程中编辑小块,然后在每个块完成时进行blit。这可以保持GUI的响应速度,而无需使用内存作为完整的后台缓冲区。

    在后台线程中写入后台缓冲区。只需要在UI线程上执行某些更新前和更新后操作。因此,当后台线程执行实际更新时,UI线程可以自由执行其他操作:

            //Put this code in a method that is called from the background thread
            long pBackBuffer = 0, backBufferStride = 0;
            Application.Current.Dispatcher.Invoke(() =>
            {//lock bitmap in ui thread
                _bitmap.Lock();
                pBackBuffer = (long)_bitmap.BackBuffer;//Make pointer available to background thread
                backBufferStride = Bitmap.BackBufferStride;
            });
            //Back to the worker thread
            unsafe
            {
                //Carry out updates to the backbuffer here
                foreach (var update in updates)
                {
                    long bufferWithOffset = pBackBuffer + GetBufferOffset(update.X, update.Y, backBufferStride);
                    *((int*)bufferWithOffset) = update.Color;
                }
            }
            Application.Current.Dispatcher.Invoke(() =>
            {//UI thread does post update operations
                _bitmap.AddDirtyRect(new System.Windows.Int32Rect(0, 0, width, height));
                _bitmap.Unlock();
            });
    

    我实施了以下内容,基于:

    在视图模型中,有这样一个属性,它绑定到XAML中的图像源:

    private WriteableBitmap cameraImage;
    private IntPtr cameraBitmapPtr;
    public WriteableBitmap CameraImage
    {
        get { return cameraImage; }
        set
        {
            cameraImage = value;
            cameraBitmapPtr = cameraImage.BackBuffer;
            NotifyPropertyChanged();
        }
    }
    
    使用属性意味着,如果可写位图发生更改(例如,由于分辨率的原因),它将在视图中更新,并且还将构造一个新的IntPtr

    在适当的时候构建图像:

    CameraImage = new WriteableBitmap(2448, 2048, 0, 0, PixelFormats.Bgr24, null);
    
    在更新线程中,复制新图像,例如使用:

    [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
    public static extern void CopyMemory(IntPtr Destination, IntPtr Source, uint Length);
    
    你会的

    CopyMemory(cameraImagePtr, newImagePtr, 2448 * 2048 * 3);
    
    这可能有更好的功能

    在同一线程中,在复制之后:

    parent.Dispatcher.Invoke(new Action(() =>
    {
        cameraImage.Lock();
        cameraImage.AddDirtyRect(new Int32Rect(0, 0, cameraImage.PixelWidth, cameraImage.PixelHeight));
        cameraImage.Unlock();
    }), DispatcherPriority.Render);
    

    其中
    parent
    是带有图像的控件/窗口。

    我在我的问题中发布了
    new Thread()
    ,只是想暗示操作应该在其他(随机)线程中完成。在我的问题中,我还声明我不想创建任何缓冲区,但我忘了提到我的位图是huuuge。这就是为什么你的方法有点不符合我的需要。:)是的,但是我恐怕你的问题的答案是:你不能。我认为BackBuffer是专门为后台编辑操作而存在的,但是微软的决定不同。无论如何谢谢你。我不会说“简直不能”,因为你实际上可以:)看我的代码回复。但委托函数将在哪个线程中执行?UI线程?线程池中的一些随机线程?还是什么?是的。第二种方法很难实现,而第一种方法很容易实现,因为它有一个快速而好的Blit函数。第三种方法当然是最好的,但是我没有时间对我们的应用程序进行那么多的增强。建议1没有奏效,因为
    Dispatcher.BeginInvoke(…writeableBitmap.WritePixels(…,buffer,…)花费太多时间。结果,应用程序出现了很多小故障。此外,在UI线程中执行CPU密集型任务看起来更具响应性。现在我将尝试使用两个常规位图重新实现可写位图。我认为这不是正确的方法。这两个调用的阻塞时间将超过写入缓冲区所需的时间。我完全同意像素缓冲区的更新是一个快速过程的情况。如果缓冲区更新涉及一些昂贵的计算(即成本超过两个调用),那么上述方法仍然是一种完全可行的方法。