Подтвердить что ты не робот

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

Я пытаюсь научить себя С# и слышал из разных источников, что функции get и 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;
}
4b9b3361

Ответ 1

Немедленно применимый код

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.Format32bppPArgb. Если изображение не нужно менять перед тем, как его нарисовать, работу можно выполнить до начала умножения, нарисованного в буфере PixelFormat.Format32bppArgb и далее используемого оттуда.

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

Использование byte вместо int для необработанных пиксельных данных

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

Bits = new Int32[width * height];

К этому:

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

Когда используются байты, в этом порядке используется формат Alpha/Red/Green/Blue. Каждый пиксель занимает 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.

Ответ 2

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

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

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)
  {

  }
}

Ответ 3

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

Ответ 4

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

  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 - это битмап-переменная.