Артефакты на Avalonia WriteableBitmap BitmapContext - PullRequest
1 голос
/ 01 августа 2020

Я пытаюсь написать WriteableBitmapEx BitmapContext для класса Avalonia WrieableBitmap. Код успешно устанавливает пиксели нужного мне цвета и получает их, но BitmapContext смешивает некоторые пиксели с цветом, который я не установил. Например, я указал, что он должен установить 4 раза LightSalmon цвет, а остальные должны быть #000000.

BitmapContext выглядит так:

public enum ReadWriteMode
    {
        /// <summary>
        /// On Dispose of a BitmapContext, do not Invalidate
        /// </summary>
        ReadOnly,

        /// <summary>
        /// On Dispose of a BitmapContext, invalidate the bitmap
        /// </summary>
        ReadWrite
    }

    unsafe
    public class BitmapContext : IDisposable
    {
        private readonly WriteableBitmap _writeableBitmap;
        private readonly ReadWriteMode _mode;

        private readonly int _pixelWidth;
        private readonly int _pixelHeight;

        private readonly static IDictionary<WriteableBitmap, int> UpdateCountByBmp = new ConcurrentDictionary<WriteableBitmap, int>();
        private readonly static IDictionary<WriteableBitmap, int[]> PixelCacheByBmp = new ConcurrentDictionary<WriteableBitmap, int[]>();
        private int length;
        private int[] pixels;

        /// <summary>
        /// The Bitmap
        /// </summary>
        public WriteableBitmap WriteableBitmap { get { return _writeableBitmap; } }

        /// <summary>
        /// Width of the bitmap
        /// </summary>
        public int Width { get { return _writeableBitmap.PixelSize.Width; } }

        /// <summary>
        /// Height of the bitmap
        /// </summary>
        public int Height { get { return _writeableBitmap.PixelSize.Height; } }
        /// <summary>
        /// Creates an instance of a BitmapContext, with default mode = ReadWrite
        /// </summary>
        /// <param name="writeableBitmap"></param>
        public BitmapContext(WriteableBitmap writeableBitmap)
            : this(writeableBitmap, ReadWriteMode.ReadWrite)
        {
        }

        /// <summary>
        /// Creates an instance of a BitmapContext, with specified ReadWriteMode
        /// </summary>
        /// <param name="writeableBitmap"></param>
        /// <param name="mode"></param>
        public BitmapContext(WriteableBitmap writeableBitmap, ReadWriteMode mode)
        {
            _writeableBitmap = writeableBitmap;
            _mode = mode;

            _pixelWidth = _writeableBitmap.PixelSize.Width;
            _pixelHeight = _writeableBitmap.PixelSize.Height;

            // Ensure the bitmap is in the dictionary of mapped Instances
            if (!UpdateCountByBmp.ContainsKey(_writeableBitmap))
            {
                // Set UpdateCount to 1 for this bitmap
                UpdateCountByBmp.Add(_writeableBitmap, 1);
                length = _writeableBitmap.PixelSize.Width * _writeableBitmap.PixelSize.Height;
                pixels = new int[length];
                CopyPixels();
                PixelCacheByBmp.Add(_writeableBitmap, pixels);
            }
            else
            {
                // For previously contextualized bitmaps increment the update count
                IncrementRefCount(_writeableBitmap);
                pixels = PixelCacheByBmp[_writeableBitmap];
                length = pixels.Length;
            }
        }

        private void CopyPixels()
        {
            using (var bmp = _writeableBitmap.Lock())
            {
                byte[] data = new byte[length];
                Marshal.Copy(bmp.Address, data, 0, length);
                fixed (byte* srcPtr = data)
                {
                    fixed (int* dstPtr = pixels)
                    {
                        for (var i = 0; i < length; i++)
                        {
                            dstPtr[i] = (srcPtr[i * 4 + 3] << 24)
                                        | (srcPtr[i * 4 + 2] << 16)
                                        | (srcPtr[i * 4 + 1] << 8)
                                        | srcPtr[i * 4 + 0];
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets the Pixels array
        /// </summary>
        public int[] Pixels { get { return pixels; } }

        /// <summary>
        /// Gets the length of the Pixels array
        /// </summary>
        public int Length { get { return length; } }

        /// <summary>
        /// Performs a Copy operation from source BitmapContext to destination BitmapContext
        /// </summary>
        /// <remarks>Equivalent to calling Buffer.BlockCopy in Silverlight, or native memcpy in WPF</remarks>
        public static void BlockCopy(BitmapContext src, int srcOffset, BitmapContext dest, int destOffset, int count)
        {
            Buffer.BlockCopy(src.Pixels, srcOffset, dest.Pixels, destOffset, count);
        }

        /// <summary>
        /// Performs a Copy operation from source Array to destination BitmapContext
        /// </summary>
        /// <remarks>Equivalent to calling Buffer.BlockCopy in Silverlight, or native memcpy in WPF</remarks>
        public static void BlockCopy(Array src, int srcOffset, BitmapContext dest, int destOffset, int count)
        {
            Buffer.BlockCopy(src, srcOffset, dest.Pixels, destOffset, count);
        }

        /// <summary>
        /// Performs a Copy operation from source BitmapContext to destination Array
        /// </summary>
        /// <remarks>Equivalent to calling Buffer.BlockCopy in Silverlight, or native memcpy in WPF</remarks>
        public static void BlockCopy(BitmapContext src, int srcOffset, Array dest, int destOffset, int count)
        {
            Buffer.BlockCopy(src.Pixels, srcOffset, dest, destOffset, count);
        }

        /// <summary>
        /// Clears the BitmapContext, filling the underlying bitmap with zeros
        /// </summary>
        public void Clear()
        {
            var pixels = Pixels;
            Array.Clear(pixels, 0, pixels.Length);
        }

        /// <summary>
        /// Disposes this instance if the underlying platform needs that.
        /// </summary>
        public void Dispose()
        {
            // Decrement the update count. If it hits zero
            if (DecrementRefCount(_writeableBitmap) == 0)
            {
                // Remove this bitmap from the update map
                UpdateCountByBmp.Remove(_writeableBitmap);
                PixelCacheByBmp.Remove(_writeableBitmap);

                // Copy data back
                if (_mode == ReadWriteMode.ReadWrite)
                {
                    using var data = _writeableBitmap.Lock();
                    using var stream = new UnmanagedMemoryStream((byte*)data.Address, length, length * 4, FileAccess.ReadWrite);
                    var buffer = new byte[length * 4];
                    fixed (int* srcPtr = pixels)
                    {
                        var b = 0;
                        for (var i = 0; i < length; i++, b += 4)
                        {
                            var p = srcPtr[i];
                            buffer[b + 3] = (byte) ((p >> 24) & 0xff);
                            buffer[b + 2] = (byte) ((p >> 16) & 0xff);
                            buffer[b + 1] = (byte) ((p >> 8) & 0xff);
                            buffer[b + 0] = (byte) (p & 0xff);
                        }

                        stream.Write(buffer, 0, length * 4);
                    }
                }
            }
        }

        private static void IncrementRefCount(WriteableBitmap target)
        {
            UpdateCountByBmp[target]++;
        }

        private static int DecrementRefCount(WriteableBitmap target)
        {
            int current;
            if (!UpdateCountByBmp.TryGetValue(target, out current))
            {
                return -1;
            }
            current--;
            UpdateCountByBmp[target] = current;
            return current;
        }
    }

Настройка и получение пикселей выглядит следующим образом:


public static int ConvertColor(Color color)
        {
            var col = 0;

            if (color.A != 0)
            {
                var a = color.A + 1;
                col = (color.A << 24)
                      | ((byte)((color.R * a) >> 8) << 16)
                      | ((byte)((color.G * a) >> 8) << 8)
                      | (byte)((color.B * a) >> 8);
            }

            return col;
        }

        public static void SetPixel(this WriteableBitmap bmp, int x, int y, Color color)
        {
            using (var context = bmp.GetBitmapContext())
            {
                context.Pixels[y * context.Width + x] = ConvertColor(color);
            }
        }

        public static Color GetPixel(this WriteableBitmap bmp, int x, int y)
        {
            using (var context = bmp.GetBitmapContext(ReadWriteMode.ReadOnly))
            {
                var c = context.Pixels[y * context.Width + x];
                var a = (byte)(c >> 24);

                // Prevent division by zero
                int ai = a;
                if (ai == 0)
                {
                    ai = 1;
                }

                // Scale inverse alpha to use cheap integer mul bit shift
                ai = ((255 << 8) / ai);
                return Color.FromArgb(a,
                    (byte)((((c >> 16) & 0xFF) * ai) >> 8),
                    (byte)((((c >> 8) & 0xFF) * ai) >> 8),
                    (byte)(((c & 0xFF) * ai) >> 8));
            }
        }

И пример:

WriteableBitmap _bitmap = new WriteableBitmap(new PixelSize(5, 5), new Vector(96, 96));
            using (_bitmap.GetBitmapContext())
            {
                _bitmap.SetPixel(0, 0, Color.FromRgb(255, 160, 122)); //Should be LightSalmon
                _bitmap.SetPixel(0, 4, Color.FromRgb(255, 160, 122)); //Should be LightSalmon
                _bitmap.SetPixel(4, 0, Color.FromRgb(255, 160, 122)); //Should be LightSalmon
                _bitmap.SetPixel(4, 4, Color.FromRgb(255, 160, 122)); //Should be LightSalmon


                for (int y = 0; y < 4; y++)
                {
                    for (int x = 0; x < 4; x++)
                    {
                        Console.WriteLine(_bitmap.GetPixel(x, y));
                    }
                }
            }

Вывод:

LightSalmon
#00000000
#00000000
#00000000
#00000000
#00000000
#00000000
#00000000
#9aa118ed
#0000815b
#000000f4
#00970094
#00970095
#00ad009f
#009f0098
#0090008e

Где LightSalmon - цвет, который я установил

Также стоит обратите внимание, что когда я не оборачиваю GetPixel l oop в контексте, а размер растрового изображения составляет, например, 15x15, он выдает

Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at WriteableBitmapEx.Models.BitmapContext.CopyPixels()

1 Ответ

0 голосов
/ 02 августа 2020

Хорошо, я нашел исправление. Я неправильно скопировал данные в CopyPixels. Пришлось поменять

byte[] data = new byte[length];
Marshal.Copy(bmp.Address, data, 0, length);

на

byte[] data = new byte[length * 4];
Marshal.Copy(bmp.Address, data, 0, length * 4);
...