C# 4.0でRGB24bitの画像をGrayscale 8bitに変換する必要ができたのですが、調べ方が悪いのか.NET Frameworkに該当する機能は見当たらず、自力変換しているケースがちらほら。
というわけで、以下のように画像データをbyte[]配列に放り込み実験。
JpegBitmapDecoder jpegDecoder = new JpegBitmapDecoder(
new Uri("4288x2848.jpg"),
BitmapCreateOptions.PreservePixelFormat,
BitmapCacheOption.OnLoad);
BitmapSource InputBitmap = jpegDecoder.Frames[0];
int InputStride = InputBitmap.PixelWidth * 3;
if ((InputBitmap.PixelWidth * 3) % 4 > 0) InputStride += 4 - (InputBitmap.PixelWidth * 3) % 4;
byte[] InputData = new byte[InputStride * InputBitmap.PixelHeight];
InputBitmap.CopyPixels(InputData, InputStride, 0);
int OutputStride = InputBitmap.PixelWidth;
if( InputBitmap.PixelWidth % 4 > 0) OutputStride += 4 - InputBitmap.PixelWidth % 4;
byte[] OutputData = new byte[OutputStride * InputBitmap.PixelHeight];
ここではWPF系のBitmapSourceを使ってますが本質は画像データをbyte[]配列に入れることなので、System.Drawing.Bitmapを使う場合は、Bitmap.LockBitsとMarshal.Copyを駆使して下さいませ。
で、安直に以下の二重ループで変換すると、4288x2848ドットの画像を変換するのに約960msec (@Core i7 920)。今回の要件でこの速度はちと不味い。
for (int y = 0; y < InputBitmap.PixelHeight; y++)
{
for (int x = 0; x < InputBitmap.PixelWidth; x++)
{
double intensity =
0.29891 * InputData[y * InputStride + x * 3] +
0.58661 * InputData[y * InputStride + x * 3 + 1] +
0.11448 * InputData[y * InputStride + x * 3 + 2];
OutputData[y * OutputStride + x] = (byte)intensity;
}
}
で、半日こねくり回してたどり着いたのが以下のコード。同条件で約50msec。
Parallel.For(0, InputImage.PixelHeight, y =>
{
for (int InputOffset = y * InputStride, OutputOffset = y * OutputStride; OutputOffset < (y + 1) * OutputStride; )
{
int intensity =
313430 * InputData[InputOffset++] +
615105 * InputData[InputOffset++] +
120041 * InputData[InputOffset++];
OutputData[OutputOffset++] = (byte)(intensity >> 20);
}
});
- byte[]配列のインデックスを和積(x+y*stride)でなくint値を増加させると、倍速くらいに
- intensityをdoubleじゃなくintで計算するのは10%程度の効果。8bitの結果を得るのに20bitで計算するのは明らかに過剰ですが、別にintに収まっているうちは何bitにしても速度変わらないし…
- 最も効いたのがParallel.For。Core i7 920が4コア+HTなので、4~5倍速はあるかなと思ったのですが、それをはるかに超える上がり方。これがCore2 Duo 2.4GHz上だと
20倍速以上7~8倍速程度になるようなのですが、理由がわからない(^^;)
-
もちろん上記コードが最速とは思っておらず、unsafeやC++を使えばもっと速くできそうですが、Parallel.For使った結果で目的は達してしまったのでやめた次第。
今回はParallel.Forがうれしい誤算だったわけですが、逆にシングルスレッド処理は何をやっているんだという疑問が…
[参考] [.NET]いまさら?Parallel.Forを使ってみた(その1) (GDD Blog 2010/9/25)
[2012/4/26 追記] 同じプログラムを改めてCore2 Duoで走らせると両者の速度差は約7.4倍。それでも2コアじゃ説明付かない速度差ですが。
[2012/4/27 追記] 以下のUnsafeコードだとさらに10%速い(44~45msec@Core i7 920、Parallel.For部分のみの計測)。ここまでする必要があるかどうかはケースバイケースかな。
IntPtr srcData = Marshal.AllocHGlobal(InputStride * InputImage.PixelHeight);
IntPtr dstData = Marshal.AllocHGlobal(OutputStride * InputImage.PixelHeight);
try
{
InputImage.CopyPixels(System.Windows.Int32Rect.Empty, srcData, InputStride * InputImage.PixelHeight, InputStride);
Parallel.For(0, InputImage.PixelHeight, y =>
{
unsafe
{
for (byte* pInput = (byte*)srcData + y * InputStride, pOutput = (byte*)dstData + y * OutputStride;
pOutput < (byte*)dstData + (y + 1) * OutputStride; )
{
int intensity =
313430 * *pInput++ +
615105 * *pInput++ +
120041 * *pInput++;
*pOutput++ = (byte)(intensity >> 20);
}
}
});
}
finally
{
Marshal.FreeHGlobal(srcData);
Marshal.FreeHGlobal(dstData);
}
[2012/4/28 追記] Unsafe版のコードに余計なものが入っていたので整理
[2012/5/1 追記] Strideの計算を訂正
Comments