Эффективный способ рисования HTML Canvas изображения пикселями с помощью Blazor (на стороне клиента) - PullRequest
1 голос
/ 20 апреля 2020

В настоящее время у меня просто есть класс C#, который представляет цвет, что-то вроде этого:

public class Color
{
        public double Red { get; }
        public double Green { get; }
        public double Blue { get; }

        public Color(double red, double green, double blue)
        {
            Red = red;
            Green = green;
            Blue = blue;
        }
}

и javascript метод для рисования изображения:

window.canvas = {
    render: (canvas, width, height, colors) => {
        canvas.width = width;
        canvas.height = height;

        let context = canvas.getContext("2d");
        let imageData = context.getImageData(0, 0, canvas.width, canvas.height);
        let data = imageData.data;

        let length = width * height;
        for (let i = 0; i < length; i++) {
            let dataIndex = i * 4;
            data[dataIndex] = colors[i].red;
            data[dataIndex + 1] = colors[i].green;
            data[dataIndex + 2] = colors[i].blue;
            data[dataIndex + 3] = 255;
        }

        context.putImageData(imageData, 0, 0);
    }
}

Я вызываю этот метод из Blazor (где data - это массив Color):

await JSRuntime.InvokeAsync<object>("canvas.render", new object[] { CanvasElement, canvas.Width, canvas.Height, data });

Я генерирую изображение программно, используя C#, с размерами 900 x 550 = 495000 пикселей. Я должен отлаживать вызов javascript с помощью веб-браузера и вижу, что параметры отправлены правильно. Изображение отображается правильно. Однако для заполнения параметров javascript из Blazor требуется несколько минут.

Существует ли эффективный способ рисования изображения Canvas попиксельно с помощью Blazor (на стороне клиента)?

Ответы [ 2 ]

1 голос
/ 20 апреля 2020

Я рад знать, что вы пытались в javascript медленно, потому что я думал о том, чтобы попробовать это.

У меня есть пакет Nuget с именем DataJuggler.PixelDatabase, и я делаю то же самое как вы, но в C# коде, тогда я просто сохраняю изображение под новым именем файла.

https://github.com/DataJuggler/PixelDatabase

В настоящее время я его рефакторинг, потому что моя первая версия использовала 7 гигабайт памяти, когда я пытался хранить список с более чем 20 миллионами элементов.

Этот класс здесь делает то же самое, что и ваш JavaScript. Кто-то опубликовал это здесь несколько лет go, и я sh Я сохранил их информацию, чтобы отдать им должное.

#region using statements

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

#endregion

namespace DataJuggler.PixelDatabase
{

    #region class DirectBitmap
    /// <summary>
    /// This class is used as a faster alternative to GetPixel and SetPixel
    /// </summary>
    public class DirectBitmap : IDisposable
    {

        #region Constructor
        /// <summary>
        /// Create a new instance of a 'DirectBitmap' object.
        /// </summary>
        public DirectBitmap(int width, int height)
        {
            Width = width;
            Height = height;
            Bits = new Int32[width * height];
            BitsHandle = GCHandle.Alloc(Bits, GCHandleType.Pinned);
            Bitmap = new Bitmap(width, height, width * 4, PixelFormat.Format32bppPArgb, BitsHandle.AddrOfPinnedObject());
        }
        #endregion

        #region Methods

            #region Dispose()
            /// <summary>
            /// method Dispose
            /// </summary>
            public void Dispose()
            {
                if (Disposed) return;
                Disposed = true;
                Bitmap.Dispose();
                BitsHandle.Free();
            }
            #endregion

            #region GetPixel(int x, int y)
            /// <summary>
            /// method Get Pixel
            /// </summary>
            public Color GetPixel(int x, int y)
            {
                int index = x + (y * Width);
                int col = Bits[index];
                Color result = Color.FromArgb(col);

                return result;
            }
            #endregion

            #region SetPixel(int x, int y, Color color)
            /// <summary>
            /// method Set Pixel
            /// </summary>
            public void SetPixel(int x, int y, Color color)
            {
                int index = x + (y * Width);
                int col = color.ToArgb();

                Bits[index] = col;
            }
            #endregion

        #endregion

        #region Properties

            #region Bitmap
            /// <summary>
            /// method [Enter Method Description]
            /// </summary>
            public Bitmap Bitmap { get; private set; }
            #endregion

            #region Bits
            /// <summary>
            /// method [Enter Method Description]
            /// </summary>
            public Int32[] Bits { get; private set; }
            #endregion

            #region BitsHandle
            /// <summary>
            /// This is a ptr to the garbage collector
            /// </summary>
            protected GCHandle BitsHandle { get; private set; }
            #endregion

            #region Disposed
            /// <summary>
            /// method [Enter Method Description]
            /// </summary>
            public bool Disposed { get; private set; }
            #endregion

            #region Height
            /// <summary>
            /// method [Enter Method Description]
            /// </summary>
            public int Height { get; private set; }
            #endregion

            #region Width
            /// <summary>
            /// method [Enter Method Description]
            /// </summary>
            public int Width { get; private set; }
            #endregion

        #endregion

    }
    #endregion

}

Затем в этом классе приведен пример загрузки DirectBitmap. Кое-что из этого указано c для моего приложения, но вы можете взять то, что вам нужно.

#region using statements

using DataJuggler.UltimateHelper.Core;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

#endregion

namespace DataJuggler.PixelDatabase
{

    #region class PixelDatabaseLoader
    /// <summary>
    /// This class is used to load PixelDatabases and their DirectBitmaps
    /// </summary>
    public class PixelDatabaseLoader
    {

        #region Methods

            #region LoadPixelDatabase(Image original, StatusUpdate updateCallback)
            /// <summary>
            /// This method is used to load a PixelDatabase and its DirectBitmap object.
            /// </summary>
            /// <param name="bitmap"></param>
            /// <returns></returns>
            public static PixelDatabase LoadPixelDatabase(Image original, StatusUpdate updateCallback)
            {
                // initial valule
                PixelDatabase pixelDatabase = null;

                try
                {
                    // convert to a bitmap
                    Bitmap bitmap = (Bitmap) original;

                    pixelDatabase = LoadPixelDatabase(bitmap, updateCallback);
                }
                catch (Exception error)
                {
                    // write to console for now
                    DebugHelper.WriteDebugError("LoadPixelDatabase", "PixelDatabaseLoader", error);
                }
                finally
                {

                }

                // return value
                return pixelDatabase;
            }
            #endregion

            #region LoadPixelDatabase(string imagePath, StatusUpdate updateCallback)
            /// <summary>
            /// This method is used to load a PixelDatabase and its DirectBitmap object from an imagePath
            /// </summary>
            /// <param name="bitmap"></param>
            /// <returns></returns>
            public static PixelDatabase LoadPixelDatabase(string imagePath, StatusUpdate updateCallback)
            {
                // initial valule
                PixelDatabase pixelDatabase = null;

                try
                {
                    // if we have an imagePath
                    if (TextHelper.Exists(imagePath))
                    { 
                        // create the Bitmap
                        using (Bitmap bitmap = (Bitmap) Bitmap.FromFile(imagePath))
                        {
                            // load the pixelDatabase
                            pixelDatabase = LoadPixelDatabase(bitmap, updateCallback);
                        }
                    }   
                }
                catch (Exception error)
                {
                    // write to console for now
                    DebugHelper.WriteDebugError("LoadPixelDatabase", "PixelDatabaseLoader", error);
                }
                finally
                {

                }

                // return value
                return pixelDatabase;
            }
            #endregion

            #region LoadPixelDatabase(Bitmap original, StatusUpdate updateCallback)
            /// <summary>
            /// This method is used to load a PixelDatabase and its DirectBitmap object.
            /// </summary>
            /// <param name="bitmap"></param>
            /// <returns></returns>
            public static PixelDatabase LoadPixelDatabase(Bitmap original, StatusUpdate updateCallback)
            {
                // initial valule
                PixelDatabase pixelDatabase = null;

                // locals
                int max = 0;

                try
                {
                    // if we have an image
                    if (NullHelper.Exists(original))
                    { 
                        // create a new bitmap
                        using (Bitmap source = new Bitmap(original))
                        {
                             // Create a new instance of a 'PixelDatabase' object.
                            pixelDatabase = new PixelDatabase();

                            // Create a DirectBitmap
                            pixelDatabase.DirectBitmap = new DirectBitmap(source.Width, source.Height);

                            // Code To Lockbits
                            BitmapData bitmapData = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadWrite, source.PixelFormat);
                            IntPtr pointer = bitmapData.Scan0;
                            int size = Math.Abs(bitmapData.Stride) * source.Height;
                            byte[] pixels = new byte[size];
                            Marshal.Copy(pointer, pixels, 0, size);

                            // End Code To Lockbits

                            // Marshal.Copy(pixels,0,pointer, size);
                            source.UnlockBits(bitmapData);

                            // locals
                            Color color = Color.FromArgb(0, 0, 0);
                            int red = 0;
                            int green = 0;
                            int blue = 0;
                            int alpha = 0;

                            // variables to hold height and width
                            int width = source.Width;
                            int height = source.Height;
                            int x = -1;
                            int y = 0;

                            // if the UpdateCallback exists
                            if (NullHelper.Exists(updateCallback))
                            {
                                // Set the value for max
                                max = height * width;    

                                // Set the graph max
                                updateCallback("SetGraphMax", max);
                            }

                            // Iterating the pixel array, every 4th byte is a new pixel, much faster than GetPixel
                            for (int a = 0; a < pixels.Length; a = a + 4)
                            {
                                // increment the value for x
                                x++;

                                // every new column
                                if (x >= width)
                                {
                                    // reset x
                                    x = 0;

                                    // Increment the value for y
                                    y++;
                                }      

                                // get the values for r, g, and blue
                                blue = pixels[a];
                                green = pixels[a + 1];
                                red = pixels[a + 2];
                                alpha = pixels[a + 3];

                                // create a color
                                color = Color.FromArgb(alpha, red, green, blue);

                                // Set the pixel at this spot
                                pixelDatabase.DirectBitmap.SetPixel(x, y, color);
                            }
                        }

                        // Create the MaskManager 
                        pixelDatabase.MaskManager = new MaskManager();
                    }
                }
                catch (Exception error)
                {
                    // write to console for now
                    DebugHelper.WriteDebugError("LoadPixelDatabase", "PixelDatabaseLoader", error);
                }

                // return value
                return pixelDatabase;
            }
            #endregion

        #endregion

    }
    #endregion

}

Так что после того, как я загружаю свое изображение и применяю изменения, я обновляю сохранение следующим образом:

// get the bitmap
Bitmap bitmap = PixelDatabase.DirectBitmap.Bitmap;

// Get a fileInfo of the oldPath
FileInfo fileInfo = new FileInfo(FullImagePath);

// Get the index of the period
int index = fileInfo.Name.IndexOf(".");

// get the name
string name = fileInfo.Name.Substring(0, index);

// Get the directory
DirectoryInfo directory = fileInfo.Directory;

// get the directoryFullname
string fullPath = directory.FullName;

// newFileName
string newFileName = name + "." + Guid.NewGuid().ToString().Substring(0, 12) + ".png";

// Get the newPath
string newPath = Path.Combine(fullPath, newFileName);

// Save
bitmap.Save(newPath, ImageFormat.Png);

Это может загрузить и сохранить 20 мегабайт изображений за несколько секунд.

Мы должны объединиться, похоже, мы работаем над тем же. Я подумываю о написании Windows сервиса, который может напрямую обновлять файлы и убирать обработку изображений из Blazor.

0 голосов
/ 24 апреля 2020

Я попытался сгенерировать изображение, отформатированное как base64 PNG на стороне клиента, и назначить его как атрибут HTML img sr c:

  • С DirectBitmap: приложение аварийно завершает работу, когда создание Bitmap.
  • с SkiaSharp: сбой приложения при создании SKBitmap.
  • с ImageSharp: приложение генерирует изображение.

Код для создания изображения base64 (с ImageSharp):

public static string ToBase64Image(this Canvas canvas)
{
    string base64 = null;

    using (var memoryStream = new MemoryStream())
    {
        using (var image = new Image<Rgba32>(canvas.Width, canvas.Height))
        {
            for (var x = 0; x < canvas.Width; x++)
            {
                for (var y = 0; y < canvas.Height; y++)
                {
                    var c = image[1, 2];
                    image[x, y] = canvas[x, y].ToImageSharpColor();
                }
            }

            image.SaveAsPng(memoryStream);
            base64 = Convert.ToBase64String(memoryStream.ToArray());
        }
    }

    return $"data:image/png;base64,{base64}";
}

Компонент Blazor содержит:

<img src="@Base64Image" />

И свойство Base64Image заполняется ToBase64Image method.

В зависимости от сгенерированного изображения я получаю ошибку:

blazor.webassembly. js: 1 Ошибка: сборщик мусора не смог выделить 16384u байт памяти для большой части кучи.

Любопытно, что кажется, что это зависит не от размера изображения, а от сложности изображения. Может ли существовать какая-либо проблема с сборщиком мусора в клиентских приложениях Blazor, которая не позволяет коду работать?

В любом случае это всегда очень медленно.

...