web-dev-qa-db-ja.com

C#-WindowsフォームアプリのビットマップのSetPixelおよびGetPixelのより高速な代替

私は自分で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;
}
36
purdoo

すぐに使用可能なコード

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

LockBitsSetPixelは必要ありません。上記のクラスを使用して、ビットマップデータに直接アクセスします。

このクラスを使用すると、生のビットマップデータを32ビットデータとして設定できます。事前乗算されたアルファであるPARGBであることに注意してください。 Alpha Compositing on Wikipedia この仕組みの詳細については BLENDFUNCTIONのMSDN記事の例 を参照して、アルファを適切に計算する方法を確認してください。

事前乗算によって物事が複雑になりすぎる場合は、代わりにPixelFormat.Format32bppArgbを使用してください。内部でPixelFormat.Format32bppPArgbに変換されるため、描画時にパフォーマンスヒットが発生します。画像を描画する前に変更する必要がない場合は、事前乗算の前に作業を行い、PixelFormat.Format32bppArgbバッファーに描画し、そこからさらに使用できます。

標準のBitmapメンバーへのアクセスは、Bitmapプロパティを介して公開されます。ビットマップデータには、Bitsプロパティを使用して直接アクセスします。

生のピクセルデータにbyteの代わりにintを使用

Int32の両方のインスタンスをbyteに変更してから、次の行を変更します。

Bits = new Int32[width * height];

これに:

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

バイトを使用する場合、形式はアルファ/赤/緑/青の順になります。各ピクセルは、各チャネルに1つずつ、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));
}

性能比較

質問ではパフォーマンスについて尋ねられるため、回答で提案されている3つの異なる方法間の相対的なパフォーマンスを示す表を次に示します。これは、.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.
80
SaxxonPike

ビットマップ操作が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)
  {

  }
}
16
Bort

しばらく経ちましたが、役に立つかもしれない例を見つけました。

  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はビットマップ変数です。

2
user9015994

Bitmap.LockBitsメソッドを使用できます。また、並列タスク実行を使用する場合は、System.Threading.Tasks名前空間のParallelクラスを使用できます。以下のリンクには、いくつかのサンプルと説明があります。

1
turgay