画像の輝度値(画素値)を取得/設定するのに、.NETでは SetPixel と GetPixel というメソッドが用意されていますが、処理が遅かったり、モノクロの8ビット画像でSetPixelを実行しようとすると
型 ‘System.InvalidOperationException’ のハンドルされていない例外が System.Drawing.dll で発生しました
追加情報:SetPixel は、インデックス付きピクセル形式のイメージに対してサポートされていません。
というエラーメッセージが出て、実質的に使い物になりません。
そこで、LockBits~UnlockBitsというメソッドを使い、ビットマップのポインタ(Scan0)から輝度値の値を参照/設定するのが定番となっています。
(unsafeコードではScan0をポインタにキャストして使う事もできます。)
以下に3x3の移動平均フィルタ処理を行った例を示します。
var bmp = new Bitmap(@"C:\Temp\Mandrill.bmp");
// Bitmapをロック
System.Drawing.Imaging.BitmapData bmpData =
bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite,
bmp.PixelFormat
);
// メモリの幅のバイト数を取得
var stride = Math.Abs(bmpData.Stride);
// チャンネル数を取得(8, 24, 32bitのBitmapのみを想定)
var channel = Bitmap.GetPixelFormatSize(bmp.PixelFormat) / 8;
// 元の画像データ用配列
var srcData = new byte[stride * bmpData.Height];
// 処理後の画像データ用配列
var dstData = new byte[stride * bmpData.Height];
// Bitmapデータを配列へコピー
System.Runtime.InteropServices.Marshal.Copy(
bmpData.Scan0,
srcData,
0,
srcData.Length
);
int index;
// 移動平均処理
for (int j = 1; j < bmpData.Height - 1; j++)
{
for (int i = channel; i < (bmpData.Width - 1) * channel; i++)
{
index = i + j * stride;
dstData[index]
= (byte)((
srcData[index - channel - stride] + srcData[index - stride] + srcData[index + channel - stride]
+ srcData[index - channel ] + srcData[index ] + srcData[index + channel ]
+ srcData[index - channel + stride] + srcData[index + stride] + srcData[index + channel + stride]
) / 9);
}
}
// 配列をBitmapデータへコピー
System.Runtime.InteropServices.Marshal.Copy(
dstData,
0,
bmpData.Scan0,
dstData.Length
);
// アンロック
bmp.UnlockBits(bmpData);
bmp.Save(@"C:\Temp\Mandrill_Smooth.bmp", System.Drawing.Imaging.ImageFormat.Bmp);
(処理結果)
処理前画像 | 処理後画像 |
上記のコードはBitmapのフォーマットが8ビット(1チャンネル、Format8bppIndexed)、24ビット(3チャンネル、Format24bppRgb)、32ビット(4チャンネル、Format32bppArgb)のみを対象としていますが、OpenCVとかで使われるチャンネルという概念を持ち込む事で、上記のコードだけでモノクロとカラーの両対応となっています。
Scan0で示されたメモリに画像データがどのように格納されているか?についでは下記ページを合わせて参照下さい。
←画像処理のためのC#テクニックへ戻る