処理時間の短縮

注意点

本節での処理時間計測は、Releaseビルドのデバッグなしで実行しています。

現状の把握

まずは現状のソースファイルですが、以下のようになっているかと思います。

            // 処理時間計測開始
            long cntStart = 0;
            QueryPerformanceCounter(ref cntStart);

            unsafe
            {
                // 出力ビットマップの領域確保
                this.grayscale = new Bitmap(this.bitmap.Width, this.bitmap.Height,
                    PixelFormat.Format8bppIndexed);

                // 入出力ビットマップをシステムメモリにロック
                BitmapData dataRgb = this.bitmap.LockBits(
                    new Rectangle(0, 0, bitmap.Width, bitmap.Height),
                    ImageLockMode.ReadOnly,
                    PixelFormat.Format24bppRgb);
                BitmapData dataGray = this.grayscale.LockBits(
                    new Rectangle(0, 0, this.grayscale.Width, this.grayscale.Height),
                    ImageLockMode.WriteOnly,
                    PixelFormat.Format8bppIndexed);
                byte* datas24bppRgb = (byte*)dataRgb.Scan0;
                byte* datas8bppGray = (byte*)dataGray.Scan0;

                // 4バイト境界の考慮(入力)
                int lineIn = this.bitmap.Width * 3;
                int lineInNew = (lineIn % 4 != 0) ? ((lineIn / 4 + 1) * 4) : lineIn;
                int lineInDiff = lineInNew - lineIn;
                int offsetIn = 0;

                // 4バイト境界の考慮(出力)
                int lineOut = this.bitmap.Width;
                int lineOutNew = (lineOut % 4 != 0) ? ((lineOut / 4 + 1) * 4) : lineOut;
                int lineOutDiff = lineOutNew - lineOut;
                int offsetOut = 0;

                // ポインタを介して、ピクセル毎にグレースケール化
                for (int y = 0; y < this.bitmap.Height; y++)
                {
                    for (int x = 0; x < this.bitmap.Width; x++)
                    {
                        // 4バイト境界の考慮
                        datas8bppGray[y * this.bitmap.Width + x + offsetOut] = (byte)(
                            ((int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn]) +
                            (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 1]) +
                            (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 2])) /
                            3);
                    }
                    // 4バイト境界の考慮
                    offsetIn += lineInDiff;
                    offsetOut += lineOutDiff;
                }

                // グレースケールのパレット情報の設定
                ColorPalette palette = this.grayscale.Palette;
                for (int i = 0; i < palette.Entries.Length; ++i)
                {
                    Color clr = Color.FromArgb(i, i, i);
                    palette.Entries[i] = clr;
                }
                this.grayscale.Palette = palette;

                // 入出力ビットマップのロックを解除
                this.bitmap.UnlockBits(dataRgb);
                this.grayscale.UnlockBits(dataGray);
            }

            // 処理時間計測終了
            long cntEnd = 0;
            QueryPerformanceCounter(ref cntEnd);
            long frq = 0;
            QueryPerformanceFrequency(ref frq);
            double time = ((double)cntEnd - (double)cntStart) / (double)frq;
            time *= 1000;
            labelTAT.Text = time.ToString("#0.0000") + "ms";

この状態で、1280×1024の画像をグレースケール化すると、(私の環境では)概ね1156ms程度かかります。計測点を、ネストしているfor文の前後に移動しても、さほど違いが見られなかったため、このネストしているfor文の処理時間が短縮できれば、全体の処理速度も速くなるはずです。

                // ポインタを介して、ピクセル毎にグレースケール化
                for (int y = 0; y < this.bitmap.Height; y++)
                {
                    for (int x = 0; x < this.bitmap.Width; x++)
                    {
                        // 4バイト境界の考慮
                        datas8bppGray[y * this.bitmap.Width + x + offsetOut] = (byte)(
                            ((int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn]) +
                            (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 1]) +
                            (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + offsetIn + 2])) /
                            3);
                    }
                    // 4バイト境界の考慮
                    offsetIn += lineInDiff;
                    offsetOut += lineOutDiff;
                }

記述こそ単純ですが、無駄な計算を何度も行っており、非常に遅いです。これから順にチューンアップしていきます。
まず、インデックスの算出を何度も繰り返しているため、その部分に手を入れます。

                // ポインタを介して、ピクセル毎にグレースケール化
                for (int y = 0; y < this.bitmap.Height; y++)
                {
                    for (int x = 0; x < this.bitmap.Width; x++)
                    {
                        // 4バイト境界の考慮
                        int indexIn = y * this.bitmap.Width * 3 + x * 3 + offsetIn;
                        int indexOut = y * this.bitmap.Width + x + offsetOut;
                        datas8bppGray[indexOut] = (byte)(
                            ((int)(datas24bppRgb[indexIn]) +
                            (int)(datas24bppRgb[indexIn + 1]) +
                            (int)(datas24bppRgb[indexIn + 2])) /
                            3);
                    }
                    // 4バイト境界の考慮
                    offsetIn += lineInDiff;
                    offsetOut += lineOutDiff;
                }

これで、400msほど高速化できました。


次に、インデックスのY成分を算出している部分は、Xのループの外に出せますので、修正します。

                // ポインタを介して、ピクセル毎にグレースケール化
                for (int y = 0; y < this.bitmap.Height; y++)
                {
                    int indexYOut = y * this.bitmap.Width;
                    int indexYIn = indexYOut * 3;
                    for (int x = 0; x < this.bitmap.Width; x++)
                    {
                        // 4バイト境界の考慮
                        int indexIn = indexYIn + x * 3 + offsetIn;
                        int indexOut = indexYOut + x + offsetOut;
                        datas8bppGray[indexOut] = (byte)(
                            ((int)(datas24bppRgb[indexIn]) +
                            (int)(datas24bppRgb[indexIn + 1]) +
                            (int)(datas24bppRgb[indexIn + 2])) /
                            3);
                    }
                    // 4バイト境界の考慮
                    offsetIn += lineInDiff;
                    offsetOut += lineOutDiff;
                }

これで、300msほど高速化できました。


インデックス記述ではなく、ポインタの参照外しに修正します。

                // ポインタを介して、ピクセル毎にグレースケール化
                for (int y = 0; y < this.bitmap.Height; y++)
                {
                    int indexYOut = y * this.bitmap.Width;
                    int indexYIn = indexYOut * 3;
                    for (int x = 0; x < this.bitmap.Width; x++)
                    {
                        // 4バイト境界の考慮
                        int indexIn = indexYIn + x * 3 + offsetIn;
                        int indexOut = indexYOut + x + offsetOut;
                        *(datas8bppGray + indexOut) = (byte)(
                            ((int)(*(datas24bppRgb + indexIn)) +
                            (int)(*(datas24bppRgb + indexIn + 1)) +
                            (int)(*(datas24bppRgb + indexIn + 2))) /
                            3);
                    }
                    // 4バイト境界の考慮
                    offsetIn += lineInDiff;
                    offsetOut += lineOutDiff;
                }

若干ではありますが高速化しているようです。


最終的には、以下となりました。

                int width = this.bitmap.Width;
                int height = this.bitmap.Height;
                byte* currentData = datas24bppRgb - 1;
                for (int y = 0; y < height; ++y)
                {
                    int widthY = y * width;
                    for (int x = 0; x < width; ++x)
                    {
                        *(datas8bppGray + widthY + x + offsetOut) =
                            (byte)(((int)*++currentData + (int)*++currentData + (int)*++currentData) / 3);
                    }
                    currentData += lineInDiff;
                    offsetOut += lineOutDiff;
                }

処理速度も、申し分ないレベルです。