0

Context

I have a linescanner which I programmed to send over a couple of lines at a time (eg: after every 10 lines it sends a packet with those 10 lines of pixels). I need the WPF app to display not only these couple of lines but also ones before that. (eg: 50 packets of each 10 lines which would make the total image 500 pixels long). For this I use a circular buffer of those packets.

These packets are constant in dimensions untill the user changes a setting. So if the user changes a setting the new ones could for example have twice the width in pixels. The user can also change the amount of packets on screen and the lines per packet.

Firstly I tried to make this work with a naive approach which worked but lacked performance. The naive approach was just recalculating the entire bitmap for every packet in the circular buffer and then binding that bitmap in WPF.

Now I wanted a better approach so and I thought WriteableBitmap was the solution. In the case that a new packet arrives and the user has not changed any settings I can edit the Backbuffer very efficiently. I can move the eg:49 newest packets up which would delete the oldest packet and move the rest. I can then also write the new packet to the absolute bottom of the image. All of this would be a low level MoveMemory() which could be a lot faster then recalculating the entire image. I would still resort to recalculating the bitmap based on the circularbuffer in the cases where the optimization isn't possible.

Issue

That is where my problem is, I cannot make a WriteableBitmap with new dimensions after one has been construced. This makes everything break when I want to reinitialize the WriteableBitmap for example when a user would change a setting to decrease the amount of packets on screen (or even in the very beginning when the circular buffer gets filled and the dimensions change every time a new packet arrives untill the buffer is full).

The specific error I get is: System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'

Is it at all possible to overwrite a current WriteableBitmap with an entirely new WriteableBitmap object to change its dimensions?

  • If not: Are there ways to achieve what I want without writeableBitmap?
  • If yes: What code causes me to not be able to overwrite the WriteableBitmap?

Here is my current approach:

LiveView.xaml.cs contains:

private readonly ImagePipelineVM viewModel;

    private WriteableBitmap liveViewBitmap;
    private IntPtr backBufferPtr;
    private int backBufferStride;
    private int liveViewBitmapHeight;
    private int liveViewBitmapWidth;
    public WriteableBitmap LiveViewBitmap
    {
        get
        {
            return liveViewBitmap;
        }
        set
        {
            liveViewBitmap = value;
            backBufferPtr = liveViewBitmap.BackBuffer;
            backBufferStride = liveViewBitmap.BackBufferStride;
            liveViewBitmapHeight = liveViewBitmap.PixelHeight;
            liveViewBitmapWidth = liveViewBitmap.PixelWidth;
        }
    }

/*
... Irrelevant code omitted
*/


private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        if (sender is ImagePipelineVM)
        {
            switch (args.PropertyName)
            {
                case nameof(ImagePipelineVM.Livepackets):
                    NewLivePacket(viewModel.ImagePipelineManagersClientModel.Models[1]);// Todo not hardcode 1
                    break;
            }
        }
    }

    private void NewLivePacket(ImagePipelineManagerModelAPI imagePipelineManager)
    {
        // No valid image
        if (imagePipelineManager.NewestPacket.Bitmap == null)
        {
            return;
        }

        // First construction
        if (LiveViewBitmap == null)
        {
            LiveViewBitmap = new WriteableBitmap(Convert(
                new Bitmap(imagePipelineManager.NewestPacket.Bitmap.Width, imagePipelineManager.LinesPerPacket * imagePipelineManager.PacketsOnScreen)));
            LiveViewImage.Source = LiveViewBitmap;
        }

        // Newest packet has different dimensions
        if (imagePipelineManager.NewestPacket.Bitmap.Width != liveViewBitmapWidth || // New width
            viewModel.Livepackets.Size < imagePipelineManager.PacketsOnScreen || // Buffer is filling
            imagePipelineManager.NewestPacket.Bitmap.Height != viewModel.Livepackets[1].Bitmap.Height) // Buffer is full but new height
        {
            // Complete recalculation needed, dimensions have changed
            var orderedImages = viewModel.Livepackets.Select(x => x.Bitmap);
            Bitmap newImage = MergeImages(orderedImages);
            BitmapSource newImageSrc = Convert(newImage);
            LiveViewBitmap.Dispatcher.Invoke(() =>
            {
                if (LiveViewBitmap.CanFreeze)
                {
                    LiveViewBitmap.Freeze();
                    LiveViewBitmap = new WriteableBitmap(newImageSrc); // This line throws the exception
                }
            });
            Application.Current.Dispatcher.Invoke(() =>
            {
                WidthTB.Text = newImage.Width.ToString();
                HeightTB.Text = newImage.Height.ToString();
            });
        }
        // Newest packet has the same dimensions as all others -> efficient method available
        else
        {
            // Move some memory around and add the newest packet to the bottom of the image                
            AddOneAndMoveOver(liveViewBitmapHeight, liveViewBitmapWidth, backBufferPtr, backBufferStride, imagePipelineManager.NewestPacket.Bitmap);
            Application.Current.Dispatcher.Invoke(() =>
            {
                if (LiveViewBitmap.CanFreeze)
                {
                    LiveViewBitmap.Lock();
                    LiveViewBitmap.AddDirtyRect(new Int32Rect(0, 0, liveViewBitmap.PixelWidth, liveViewBitmap.PixelHeight));
                    LiveViewBitmap.Unlock();
                }
            }); // Back to the worker thread                                
        }

        DateTime dateTime = new DateTime(imagePipelineManager.DateTime);
        viewModel.FrontendDelay = (int)Math.Abs((DateTime.Now - dateTime).TotalMilliseconds);
    }

    /// <summary>
    /// Merge a collection of bitmaps vertically into one long bitmap
    /// </summary>
    private static Bitmap MergeImages(IEnumerable<Bitmap> images)
    {
        // Code not relevant, it glues a collection of bitmaps vertically
    }

    public static BitmapSource Convert(System.Drawing.Bitmap bitmap)
    {
        // Code not relevant, converts bitmap to bitmapsource
    }

    [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
    private static extern void MoveMemory(IntPtr dest, IntPtr src, int size);
    [System.Runtime.InteropServices.DllImport("gdi32.dll")]
    private static extern bool DeleteObject(IntPtr hObject);

    /// <summary>
    /// Edits the memory of the WriteableBitmap using MoveMemory().
    /// Adds new block to the bottom of the bitmap and moves the rest over except for the oldest blockwhich gets deleted.
    /// </summary>
    public static void AddOneAndMoveOver(int height, int width, IntPtr backBufferPtr, int backBufferStride, Bitmap newBlock)
    {
        int x = 0; // Topleft of crop region
        int y = newBlock.Height; // Topleft of crop region, skip the oldest data
        int linesToCopy = height - newBlock.Height;

        for (var line = 0; line < linesToCopy; line++)
        {
            var srcOffset = ((y + line) * width + x) * 4;
            var dstOffset = line * backBufferStride;

            // Copy pixels using fast MoveMemory(destination, source, size)
            // Moves one line of pixels
            MoveMemory(backBufferPtr + dstOffset, backBufferPtr + srcOffset, backBufferStride);
        }

        // Now add the new block to the bottom of the writeablebitmap            
        IntPtr newBlockInMemory = newBlock.GetHbitmap();
        MoveMemory(backBufferPtr + (backBufferStride * linesToCopy), newBlockInMemory, backBufferStride * newBlock.Height);
        DeleteObject(newBlockInMemory);
    }

LiveView.xaml contains:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0" Text="Live View" Foreground="Black" Margin="10,10,10,10"/>
    <DockPanel Grid.Row="1" Width="300" HorizontalAlignment="Left">
        <TextBlock Text="Width" HorizontalAlignment="Left"/>
        <TextBox HorizontalAlignment="Right" Name="WidthTB"/>
    </DockPanel>
    <DockPanel Grid.Row="2" Width="300" HorizontalAlignment="Left">
        <TextBlock Text="Height" HorizontalAlignment="Left"/>
        <TextBox HorizontalAlignment="Right" Name="HeightTB"/>
    </DockPanel>
    <DockPanel Grid.Row="3" Width="auto" HorizontalAlignment="Left">
        <Image Margin="10,10,10,10" Name="LiveViewImage" Stretch="Uniform" Visibility="Visible" HorizontalAlignment="Left"/>
    </DockPanel>
</Grid>

Related:

  • You have to create a new one when setting changed. Instead of WriteableBitmap you may perhaps use BitmapSource.Create. – Clemens Aug 26 '21 at 15:19

0 Answers0