端くれプログラマの備忘録 C#,画像処理 [C#] ビットマップにピクセル単位で高速にアクセスするには (GetPixel/SetPixel vs BitmapData 速度比較)

[C#] ビットマップにピクセル単位で高速にアクセスするには (GetPixel/SetPixel vs BitmapData 速度比較)

Bitmapクラスにはピクセル単位のアクセス関数 SetPixel/GetPixel が用意されているけど、ネットを見るとこれら関数はあまり速くないらしい。これら関数を使う代わりに、ビットマップデータをアンマネージ配列にコピーした 上で処理する方法が推奨されているみたい。画像処理ソフトを書く上で処理速度は重要だ。両者でどれぐらいの速度差があるのか調べてみた。

フルカラー画像を読み込んだ後、ピクセル単位にグレイスケールに変換するのに要した時間をミリ秒単位で計測。グレイスケールへの変換には、YCrCb変換のY値の計算式を使った。

grey = (0.299 * red + 0.587 * green + 0.114 * blue)
Stopwatch sw = new Stopwatch();
sw.Start();
// 処理を行う
sw.Stop();
Debug.WriteLine("Elapsed time (ms) = " + sw.ElepasedMilliseconds);

方法1: SetPixel/GetPixelを使う

Bitmap bitmap = new Bitmap("C:\\Temp\\lena.jpg");
for (int y = 0; y < bitmap.Height; y++)
{
    for (int x = 0; x < bitmap.Width; x++)
    {
        Color color = bitmap.GetPixel(x, y);
        int grey = (int)(0.299 * color.R + 0.587 * color.G + 0.114 * color.B);
        bitmap.SetPixel(x, y, Color.FromArgb(grey, grey, grey));
    }
}

方法2: ビットマップデータをアンマネージ配列にコピーしてから処理する

Bitmap bitmap = new Bitmap("C:\\Temp\\lena.jpg");
BitmapData data = bitmap.LockBits(
    new Rectangle(0, 0, bitmap.Width, bitmap.Height),
    ImageLockMode.ReadWrite,
    PixelFormat.Format32bppArgb);
byte[] buf = new byte[bitmap.Width * bitmap.Height * 4];
Marshal.Copy(data.Scan0, buf, 0, buf.Length);
for (int i = 0; i < buf.Length; )
{
    byte grey = (byte)(0.299 * buf[i] + 0.587 * buf[i+1] + 0.114 * buf[i+2]);
    buf[i++] = grey;
    buf[i++] = grey;
    buf[i++] = grey;
    i++;
}
Marshal.Copy(buf, 0, data.Scan0, buf.Length);
bitmap.UnlockBits(data);

方法3: ビットマップをシステムメモリにロックして直アクセスする (バイト単位)

方法2のMarshal.Copyのコストが掛かっているかも?という懸念から。

Bitmap bitmap = new Bitmap("C:\\Temp\\lena.jpg");
BitmapData data = bitmap.LockBits(
    new Rectangle(0, 0, bitmap.Width, bitmap.Height),
    ImageLockMode.ReadWrite,
    PixelFormat.Format32bppArgb);
int bytes = bitmap.Width * bitmap.Height * 4;
for (int i = 0; i < bytes; i += 4)
{
    byte r = Marshal.ReadByte(data.Scan0, i);
    byte g = Marshal.ReadByte(data.Scan0, i+1);
    byte b = Marshal.ReadByte(data.Scan0, i+2);
    byte grey = (byte)(0.299 * r + 0.587 * g + 0.114 * b);
    Marshal.WriteByte(data.Scan0, i, grey);
    Marshal.WriteByte(data.Scan0, i+1, grey);
    Marshal.WriteByte(data.Scan0, i+2, grey);
}
bitmap.UnlockBits(data);

方法4: ビットマップをシステムメモリにロックして直アクセスする (ピクセル単位)

方法3では1ピクセルごとにMarshal.ReadByte/WriteByteが3回ずつ呼ばれるので、これら関数のオーバーヘッドが掛かっているかも?という懸念から。

Bitmap bitmap = new Bitmap("C:\\Temp\\lena.jpg");
BitmapData data = bitmap.LockBits(
    new Rectangle(0, 0, bitmap.Width, bitmap.Height),
    ImageLockMode.ReadWrite,
    PixelFormat.Format32bppArgb);
int bytes = bitmap.Width * bitmap.Height * 4;
for (int i = 0; i < bytes; i += 4)
{
    Int32 value = Marshal.ReadInt32(data.Scan0, i);
    byte r = (byte)(value & 0xff);
    byte g = (byte)((value >> 8) & 0xff);
    byte b = (byte)((value >> 16) & 0xff);
    byte grey = (byte)(0.299 * r + 0.587 * g + 0.114 * b);
    value = (grey << 16) | (grey << 8) | grey;
    Marshal.WriteInt32(data.Scan0, i, value);
}
bitmap.UnlockBits(data);

テストに使ったデータ

画像1 lena.jpg (400×400ピクセル)
画像2 ff_x_e1_004.JPG (4896×3264ピクセル) – 富士フィルムサイトより拝借

フジノンレンズ XF18-55mmF2.8-4 R LM OIS : サンプル画像 | 富士フイルム
http://fujifilm.jp/personal/digitalcamera/x/fujinon_lens_xf18_55mmf28_4_r_lm_ois/sample_images/

テスト結果

処理時間は以下のようになった。方法3と4は、アンマネージ配列をアロケートしてデータをコピーするコストを考えた代替方法だったのだけど、画像が大きくなってもそのコストは大したことはなかったので気にする必要はなさそう。

# 処理内容 画像1(400×400ピクセル) 画像2(4896×3264ピクセル)
方法1 GetPixel/SetPixel関数を使用 597ms 28,187ms
方法2 アンマネージ配列にコピーした上で処理 10ms 268ms
方法3 システムメモリにロックして直アクセス (バイト単位) 19ms 523ms
方法4 システムメモリにロックして直アクセス (ピクセル単位) 15ms 400ms

環境: Windows 7 Ultimate SP1 (64bit), Intel Core i7 3.4GHz, RAM 16GB, Visual Studio 2012