C# - более быстрые альтернативы SetPixel и GetPixel для растровых изображений для приложения Windows Forms

Я пытаюсь научить себя C# и слышал из разных источников, что функции получают и setpixel может быть ужасно медленным. Каковы некоторые из альтернатив, и действительно ли улучшение производительности настолько существенно? Заранее спасибо!

Часть моего кода для справки:

public static Bitmap Paint(Bitmap _b, Color f)
{
  Bitmap b = new Bitmap(_b);
  for (int x = 0; x < b.Width; x++) 
  {
    for (int y = 0; y < b.Height; y++) 
    {
      Color c = b.GetPixel(x, y);
      b.SetPixel(x, y, Color.FromArgb(c.A, f.R, f.G, f.B));
    }
  }
  return b;
}

5 ответов

Решение

Немедленно используемый код

public class DirectBitmap : IDisposable
{
    public Bitmap Bitmap { get; private set; }
    public Int32[] Bits { get; private set; }
    public bool Disposed { get; private set; }
    public int Height { get; private set; }
    public int Width { get; private set; }

    protected GCHandle BitsHandle { get; private set; }

    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());
    }

    public void SetPixel(int x, int y, Color colour)
    {
        int index = x + (y * Width);
        int col = colour.ToArgb();

        Bits[index] = col;
    }

    public Color GetPixel(int x, int y)
    {
        int index = x + (y * Width);
        int col = Bits[index];
        Color result = Color.FromArgb(col);

        return result;
    }

    public void Dispose()
    {
        if (Disposed) return;
        Disposed = true;
        Bitmap.Dispose();
        BitsHandle.Free();
    }
}

Там нет необходимости для LockBits или же SetPixel, Используйте вышеуказанный класс для прямого доступа к растровым данным.

С помощью этого класса можно установить необработанные растровые данные как 32-разрядные данные. Обратите внимание, что это PARGB, который является предварительно умноженным альфа. См. Alpha Compositing в Википедии для получения дополнительной информации о том, как это работает, и примеров в статье MSDN для BLENDFUNCTION, чтобы узнать, как правильно рассчитать альфа.

Если предварительное умножение может усложнить вещи, используйте PixelFormat.Format32bppArgb вместо. Падение производительности происходит, когда оно отрисовывается, потому что оно внутренне преобразуется в PixelFormat.Format32bppPArgb, Если изображение не должно изменяться до рисования, работа может быть выполнена до предварительного умножения, нарисованного в PixelFormat.Format32bppArgb буфер, и далее используется оттуда.

Доступ к стандарту Bitmap Участники выставляются через Bitmap имущество. Доступ к растровым данным осуществляется напрямую с помощью Bits имущество.

С помощью byte вместо int для необработанных данных пикселей

Изменить оба экземпляра Int32 в byteи затем измените эту строку:

Bits = new Int32[width * height];

К этому:

Bits = new byte[width * height * 4];

Когда используются байты, формат Альфа / Красный / Зеленый / Синий в этом порядке. Каждый пиксель занимает 4 байта данных, по одному на каждый канал. Функции GetPixel и SetPixel должны быть соответственно переработаны или удалены.

Преимущества использования вышеуказанного класса

  • Выделение памяти для простого манипулирования данными не требуется; изменения, внесенные в необработанные данные, немедленно применяются к растровому изображению.
  • Нет дополнительных объектов для управления. Это реализует IDisposable как Bitmap,
  • Не требует unsafe блок.

Соображения

  • Закрепленная память не может быть перемещена. Это обязательный побочный эффект для того, чтобы такой доступ к памяти работал. Это снижает эффективность работы сборщика мусора ( статья MSDN). Делайте это только с растровыми изображениями, где требуется производительность, и обязательно Dispose их, когда вы закончите, чтобы память можно было закрепить.

Доступ через Graphics объект

Поскольку Bitmap свойство на самом деле.NET Bitmap объект, это просто для выполнения операций с использованием Graphics учебный класс.

var dbm = new DirectBitmap(200, 200);
using (var g = Graphics.FromImage(dbm.Bitmap))
{
    g.DrawRectangle(Pens.Black, new Rectangle(50, 50, 100, 100));
}

Сравнение производительности

Вопрос касается производительности, вот таблица, которая должна показать относительную производительность между тремя различными методами, предложенными в ответах. Это было сделано с помощью приложения на основе.NET Standard 2 и NUnit.

* Time to fill the entire bitmap with red pixels *
- Not including the time to create and dispose the bitmap
- Best out of 100 runs taken
- Lower is better
- Time is measured in Stopwatch ticks to emphasize magnitude rather than actual time elapsed
- Tests were performed on an Intel Core i7-4790 based workstation

              Bitmap size
Method        4x4   16x16   64x64   256x256   1024x1024   4096x4096
DirectBitmap  <1    2       28      668       8219        178639
LockBits      2     3       33      670       9612        197115
SetPixel      45    371     5920    97477     1563171     25811013

* Test details *

- LockBits test: Bitmap.LockBits is only called once and the benchmark
                 includes Bitmap.UnlockBits. It is expected that this
                 is the absolute best case, adding more lock/unlock calls
                 will increase the time required to complete the operation.

Причина, по которой растровые операции так медленны в C#, связана с блокировкой и разблокировкой. Каждая операция будет выполнять блокировку требуемых битов, манипулировать битами, а затем разблокировать биты.

Вы можете значительно улучшить скорость, выполняя операции самостоятельно. Смотрите следующий пример.

using (var tile = new Bitmap(tilePart.Width, tilePart.Height))
{
  try
  {
      BitmapData srcData = sourceImage.LockBits(tilePart, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
      BitmapData dstData = tile.LockBits(new Rectangle(0, 0, tile.Width, tile.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);

      unsafe
      {
          byte* dstPointer = (byte*)dstData.Scan0;
          byte* srcPointer = (byte*)srcData.Scan0;

          for (int i = 0; i < tilePart.Height; i++)
          {
              for (int j = 0; j < tilePart.Width; j++)
              {
                  dstPointer[0] = srcPointer[0]; // Blue
                  dstPointer[1] = srcPointer[1]; // Green
                  dstPointer[2] = srcPointer[2]; // Red
                  dstPointer[3] = srcPointer[3]; // Alpha

                  srcPointer += BytesPerPixel;
                  dstPointer += BytesPerPixel;
              }
              srcPointer += srcStrideOffset + srcTileOffset;
              dstPointer += dstStrideOffset;
          }
      }

      tile.UnlockBits(dstData);
      aSourceImage.UnlockBits(srcData);

      tile.Save(path);
  }
  catch (InvalidOperationException e)
  {

  }
}

Это было какое-то время, но я нашел пример, который может быть полезным.

  BitmapData BtmpDt = a.LockBits(new Rectangle(0,0,btm.Width,btm.Height),ImageLockMode.ReadWrite,btm.PixelFormat);
  IntPtr pointer = BtmDt.Scan0;
  int size = Math.Abs(BtmDt.Stride)*btm.Height;
  byte[] pixels = new byte[size];
  Marshal.Copy(pointer,pixels,0, size);
  for (int b = 0; b < pixels.Length; b++) {
    pixels[b] = 255;// do something here 
  }
  Marshal.Copy(pixels,0,pointer, size);
  btm.UnlockBits(BtmDt);

Где btm - это битовая переменная.

Вы можете использовать метод Bitmap.LockBits. Также, если вы хотите использовать параллельное выполнение задач, вы можете использовать класс Parallel в пространстве имен System.Threading.Tasks. Следующие ссылки имеют некоторые образцы и объяснения.

Этот код должен быть распараллелен, при синхронном выполнении упускается огромный прирост производительности. Почти ни один современный микрочип не будет иметь менее 4 доступных потоков, а некоторые чипы будут иметь 40 доступных потоков.

Нет абсолютно никакой причины запускать этот первый цикл синхронно. Вы можете пройти как по ширине, так и по длине, используя много-много нитей.

              private void TakeApart_Fast(Bitmap processedBitmap)
        {
            
            BitmapData bitmapData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
            ConcurrentBag<byte> points = new ConcurrentBag<byte>();   
            unsafe
            {
                int bytesPerPixel = System.Drawing.Bitmap.GetPixelFormatSize(processedBitmap.PixelFormat) / 8;
                int heightInPixels = bitmapData.Height;
                int widthInBytes = bitmapData.Width * bytesPerPixel;
                _RedMin = byte.MaxValue;
                _RedMax = byte.MinValue;
                byte* PtrFirstPixel = (byte*)bitmapData.Scan0;
              
                Parallel.For(0, heightInPixels, y =>
                {
                    byte* currentLine = PtrFirstPixel + (y * bitmapData.Stride);
                    for (int x = 0; x < widthInBytes; x = x + bytesPerPixel)
                    {
                        // red
                        byte redPixel = currentLine[x + 2];
                        //save information with the concurrentbag
    
                    }
                });
                processedBitmap.UnlockBits(bitmapData);
            }
        }`
  

тест не будет иметь большого значения, потому что ответ на вопрос, насколько это ускорит процесс, на 100% зависит от того, какое оборудование вы используете, и что еще работает в фоновом режиме, все зависит от того, сколько свободных потоков доступно. Если вы используете это на графической карте серии 4000 с тысячами потоковых процессоров, вы можете выполнять итерацию по каждому столбцу изображения одновременно.

если вы используете его со старым четырехъядерным процессором, у вас может быть только 5 или 6 потоков, что все еще невероятно важно.

Другие вопросы по тегам