System.Drawing.Bitmap画像を使った高速な画像処理プログラミングについて

はじめに

.Net Framework の System.Drawing.Bitmapを使うと、jpegpngなどの画像データを
簡単に扱うことができます。しかし、このクラスに用意されている、各画素にアクセスするメソッド SetPixel はあまり高速ではありません。
閾値処理、エッジ検出など各種画像処理をこのメソッドを使って行おうとすると、
画像データが大きい場合、結構処理に時間がかかります。

<処理が遅い例>

// 閾値128で2値化する処理
Bitmap bmp = new Bitmap("hoge.png" ) ;
const int TH = 128; // 閾値
for (int j = 0; j < bmp.Height; ++j){
  for (int i = 0; i < bmp.Width; ++i){
    Color c = bmp.GetPixel(i,j) ;
    byte r = c.R < TH ? 0 : 255, g = c.G < TH ? 0 : 255, b = c.B < TH ? 0 : 255;
    bmp.SetPixel(i,j, Color.FromArgb(r,g,b)) ;
  }
}
bmp.Save("hogera.png" );

 

これを解決する手段を2つほど見つけましたので書いておきます。
 

方法1: LockBits

 bmp.LockBitsで手に入るBitmapDataオブジェクトを介して、画像データ配列を直接
取得してしまう方法です。この方法は、CodeProjectというサイトに載っていました。
http://www.codeproject.com/cs/media/csharpfilters.asp
ただし、この方法では画像データ配列をアンマネージドなアドレスから取得する必要があるので、
System.Runtime.InteropServices.Marshalクラスのメソッドのお世話になるか、
Win32APIを呼び出す、またはunsafeモードにする必要があります。

 

方法2: BMP形式に変換して直接編集

画像ファイルをフォーマットごとMemoryStreamに書き込んで、それを自力で
書き換える方法です。このアイデアは iTextSharpのソースから得ました。
圧縮されているjpegpngフォーマットを直接自力で書き換えるのはかなりしんどい
ので、非圧縮形式であるBMP形式に変換します。

 

<方法2による改善例>

Bitmap bmp_ = new Bitmap("hoge.png" );
bmp = new Bitmap(bmp_.Width, bmp_.Height, PixelFormat.Format24bppRgb) ;
Graphics g = Graphics.FromImage(bmp) ;
g.DrawImageUnscaled(bmp_,0,0) ;
g.Dispose() ;
bmp_.Dispose() ;
MemoryStream ms = new MemoryStream() ;

 

// ここでmsに 24bppなMicrosoft Windows BMPフォーマットが
// 書き込まれる。
bmp.Save(ms, ImageFormat.Bmp) ;

 

// BMP ファイル先頭から10バイト-13バイトの4バイトに画像データ
// 本体の先頭アドレスが書かれている
ms.Seek(10, SeekOrigin.Begin) ;

 

int startRaw = readint32(ms) ;
ms.Seek(startRaw, SeekOrigin.Begin) ;

 

// BMPフォーマットは横方向のデータを4バイト境界に揃える仕様
int stride = ((bmp.Width * 3 + 3) / 4) * 4;
byte data = new byte[stride * bmp.Height]; // data に全画像データを格納する
ms.Read(data, 0, data.Length) ;

 

// ここでdataに対して処理を行う BGRBGRBGR... の順で各画素のデータが格納されている点に注意

 

// ここから先は data を Bitmapオブジェクトに書き出す処理。これまでと逆の処理をやるだけ
ms.Seek (startRaw, SeekOrigin.Begin) ;
ms.Write (data, 0, data.Length) ;
Bitmap bmpOut = new Bitmap(ms) ; // 処理後の画像がbmpOutに入る
ms.Dispose() ;

 

readint32は 4バイトのバイト列をリトルエンディアンの整数として読み込むメソッドです。

int readint32(MemoryStream ms){
  byte b = new byte[4];
  ms.Read(b, 0, 4);
  return (int)b[0] + (int)b[1] * 0x100 + (int)b[2] * 0x10000 + (int)b[3] * 0x1000000;
}

 
BMPフォーマットは非圧縮形式とはいえそこそこクセのあるフォーマットなので、
手順はそれなりに煩雑になってしまってますが、この方法ならば全部マネージドで
済む上に高速に処理できます。

 

(注意:当方の環境では、bmpOut.Saveメソッドを呼んでpngファイルを書き出そうとしたときに例外を発生しました。PixelFormat.Format24bppRgb決め打ちにしたのが原因でしょうか?さらに↓のような処理を加えたあと、bmpOut2.Saveとしたら大丈夫でした)
bmpOut2 = new Bitmap(bmpOut.Width, bmpOut.Height);
Graphics g2 = Graphics.FromImage(bmpOut2);
g2.DrawImageUnscaled(bmpOut, 0, 0);
// この時点でbmpOut2にはbmpOutと同じ画像データが入っているはず。
g2.Dispose();

 

ベンチマークなどは気が向いたら後ほど…ということで。

 

 


関連ページ

もっちー書庫 自作ソフトウェアやOSSライブラリの調べごと等を公開しています。
http://ja.osdn.net/users/mocchi_2012/pf/mocchi_stack_room/wiki/FrontPage