GetPixel/SetPixel は遅い

所用で簡単な画像処理の実装を(お願い)しているのですが,何というか実行が非常に遅いのが気になりました.どうやら Bitmap クラス の GetPixel/SetPixel が原因のようで,これらのメソッドは非常に遅い事で有名なようです.

void TranslateImage(Bitmap original) {
    for (int h = 0; h < original.Height; ++h) {
        for (int w = 0; w < original.Width; ++w) {
            Color c = original.GetPixel(w, h);
            
            // ... 何らかの変換処理を行う
            
            original.SetPixel(w, h, c);
        }
    }
}

改善方法としてポピュラーなものは BitmapData.LockBits を利用するもののようです.そこで,上記のサンプルコードと使用感がほとんど変わらないようなラッパクラスをざっと書いてみました(ソースコード).下記のラッパクラスでは,コンストラクタ時に LockBits を実行し,Dispose メソッドが呼ばれた段階で UnLockBits を実行します.対応している PixelFormat は取りあえず,Format24bppRgb, Format32bppRgb, Format32bppArgb (Canonical は Format32bppArgb と同じ?).

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace Cubic {
    /* --------------------------------------------------------------------- */
    ///
    /// BitmapAccessor
    ///
    /// <summary>
    /// Bitmap の GetPixel/SetPixel メソッドは非常に時間がかかると言う問題
    /// があるため,これらのメソッドを高速化するためのラッパクラス.
    /// </summary>
    ///
    /* --------------------------------------------------------------------- */
    class BitmapAccessor : IDisposable {
        /* ----------------------------------------------------------------- */
        ///
        /// IsSupported
        /// 
        /// <summary>
        /// 指定した PixelFormat が BimatpAccessor で対応しているかどうか.
        /// </summary>
        /// 
        /* ----------------------------------------------------------------- */
        public static bool IsSupported(PixelFormat format) {
            switch (format) {
                case PixelFormat.Canonical:
                case PixelFormat.Format24bppRgb:
                case PixelFormat.Format32bppRgb:
                case PixelFormat.Format32bppArgb:
                    return true;
                // 以下,未実装の PixelFormat
                // case PixelFormat.Alpha:
                // case PixelFormat.DontCare:
                // case PixelFormat.Extended:
                // case PixelFormat.Format16bppArgb1555:
                // case PixelFormat.Format16bppGrayScale:
                // case PixelFormat.Format16bppRgb555:
                // case PixelFormat.Format16bppRgb565:
                // case PixelFormat.Format1bppIndexed:
                // case PixelFormat.Format32bppPArgb:
                // case PixelFormat.Format48bppRgb:
                // case PixelFormat.Format4bppIndexed:
                // case PixelFormat.Format64bppArgb:
                // case PixelFormat.Format64bppPArgb:
                // case PixelFormat.Format8bppIndexed:
                // case PixelFormat.Gdi:
                // case PixelFormat.Indexed:
                // case PixelFormat.Max:
                // case PixelFormat.PAlpha:
                default:
                    break;
            }
            return false;
        }
        
        /* ----------------------------------------------------------------- */
        /// Constructor
        /* ----------------------------------------------------------------- */
        public BitmapAccessor(Bitmap img) {
            Debug.Assert(IsSupported(img.PixelFormat));
            
            original_ = img;
            data_ = original_.LockBits(new Rectangle(0, 0, original_.Width, original_.Height), ImageLockMode.ReadWrite, img.PixelFormat);
        }

        /* ----------------------------------------------------------------- */
        /// Dispose
        /* ----------------------------------------------------------------- */
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /* ----------------------------------------------------------------- */
        /// Dispose
        /* ----------------------------------------------------------------- */
        protected virtual void Dispose(bool disposing) {
            if (!disposed_) {
                if (disposing) {
                    if (original_ != null && data_ != null) {
                        original_.UnlockBits(data_);
                        data_ = null;
                    }
                }
            }
            disposed_ = true;
        }

        /* ----------------------------------------------------------------- */
        ///
        /// GetPixel
        ///
        /// <summary>
        /// Bitmap の指定したピクセルの色を取得する.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        public Color GetPixel(int x, int y) {
            Debug.Assert(this.original_ != null);
            Debug.Assert(this.data_ != null);

            int offset = this.GetOffset(x, y);
            return Color.FromArgb(this.GetAlpha(offset), this.GetRed(offset), this.GetGreen(offset), this.GetBlue(offset));
        }

        /* ----------------------------------------------------------------- */
        ///
        /// SetPixel
        ///
        /// <summary>
        /// Bitmap の指定したピクセルの色を設定する.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        public void SetPixel(int x, int y, Color c) {
            Debug.Assert(this.original_ != null);
            Debug.Assert(this.data_ != null);

            int offset = this.GetOffset(x, y);
            this.SetAlpha(offset, c.A);
            this.SetRed(offset, c.R);
            this.SetGreen(offset, c.G);
            this.SetBlue(offset, c.B);
        }

        /* ----------------------------------------------------------------- */
        ///
        /// Width
        /// 
        /// <summary>
        /// Bitmap オブジェクトのピクセルの幅を取得する.
        /// </summary>
        /// 
        /* ----------------------------------------------------------------- */
        public int Width {
            get { return data_.Width; }
        }

        /* ----------------------------------------------------------------- */
        ///
        /// Height
        ///
        /// <summary>
        /// Bitmap オブジェクトの高さ(ピクセル単位)を取得する.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        public int Height {
            get { return data_.Height; }
        }

        /* ----------------------------------------------------------------- */
        /// 内部処理のためのメソッド群
        /* ----------------------------------------------------------------- */
        #region Private methods
        /* ----------------------------------------------------------------- */
        ///
        /// GetOffset
        ///
        /// <summary>
        /// Bitmap の (x, y) ピクセルの色情報に対応する最初のバイト位置を
        /// 取得する.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private int GetOffset(int x, int y) {
            switch (data_.PixelFormat) {
                case PixelFormat.Format64bppArgb:
                case PixelFormat.Format64bppPArgb:
                    return x * 8 + data_.Stride * y;
                case PixelFormat.Format48bppRgb:
                    return x * 6 + data_.Stride * y;
                case PixelFormat.Canonical:
                case PixelFormat.Format32bppRgb:
                case PixelFormat.Format32bppArgb:
                case PixelFormat.Format32bppPArgb:
                    return x * 4 + data_.Stride * y;
                case PixelFormat.Format24bppRgb:
                    return x * 3 + data_.Stride * y;
                case PixelFormat.Format16bppArgb1555:
                case PixelFormat.Format16bppGrayScale:
                case PixelFormat.Format16bppRgb555:
                case PixelFormat.Format16bppRgb565:
                    return x * 2 + data_.Stride * y;
                case PixelFormat.Format8bppIndexed:
                    return x + data_.Stride * y;
                case PixelFormat.Format4bppIndexed:
                    return 2 / x + data_.Stride * y;
                case PixelFormat.Format1bppIndexed:
                    return 8 / x + data_.Stride * y;
                // 以下,計算方法が不明な PixelFormat
                // case PixelFormat.Alpha:
                // case PixelFormat.DontCare:
                // case PixelFormat.Extended:
                // case PixelFormat.Gdi:
                // case PixelFormat.Indexed:
                // case PixelFormat.Max:
                // case PixelFormat.PAlpha:
                default:
                    break;
            }
            return -1;
        }

        /* ----------------------------------------------------------------- */
        ///
        /// GetAlpha
        ///
        /// <summary>
        /// Bitmap の特定ピクセルのアルファ値を取得する.引数 offset は,
        /// 取得したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private int GetAlpha(int offset) {
            if (data_.PixelFormat == PixelFormat.Format32bppArgb) {
                return Marshal.ReadByte(data_.Scan0, offset + 3);
            }
            else return 0;
        }

        /* ----------------------------------------------------------------- */
        ///
        /// GetRed
        ///
        /// <summary>
        /// Bitmap の特定ピクセルの赤色値を取得する.引数 offset は,取得
        /// したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private byte GetRed(int offset) {
            return Marshal.ReadByte(data_.Scan0, offset + 2);
        }

        /* ----------------------------------------------------------------- */
        ///
        /// GetGreen
        ///
        /// <summary>
        /// Bitmap の特定ピクセルの緑色値を取得する.引数 offset は,取得
        /// したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private byte GetGreen(int offset) {
            return Marshal.ReadByte(data_.Scan0, offset + 1);
        }

        /* ----------------------------------------------------------------- */
        ///
        /// GetBlue
        ///
        /// <summary>
        /// Bitmap の特定ピクセルの青色値を取得する.引数 offset は,取得
        /// したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private byte GetBlue(int offset) {
            return Marshal.ReadByte(data_.Scan0, offset);
        }

        /* ----------------------------------------------------------------- */
        ///
        /// SetAlpha
        ///
        /// <summary>
        /// Bitmap の特定ピクセルのアルファ値を設定する.引数 offset は,
        /// 設定したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private void SetAlpha(int offset, byte value) {
            if (data_.PixelFormat == PixelFormat.Format32bppArgb) {
                Marshal.WriteByte(data_.Scan0, offset + 3, value);
            }
        }

        /* ----------------------------------------------------------------- */
        ///
        /// SetRed
        ///
        /// <summary>
        /// Bitmap の特定ピクセルの赤色値を設定する.引数 offset は,設定
        /// したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private void SetRed(int offset, byte value) {
            Marshal.WriteByte(data_.Scan0, offset + 2, value);
        }

        /* ----------------------------------------------------------------- */
        ///
        /// SetGreen
        ///
        /// <summary>
        /// Bitmap の特定ピクセルの緑色値を設定する.引数 offset は,設定
        /// したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private void SetGreen(int offset, byte value) {
            Marshal.WriteByte(data_.Scan0, offset + 1, value);
        }

        /* ----------------------------------------------------------------- */
        ///
        /// SetBlue
        ///
        /// <summary>
        /// Bitmap の特定ピクセルの青色値を設定する.引数 offset は,設定
        /// したいピクセルの最初のバイト位置を示す.
        /// </summary>
        ///
        /* ----------------------------------------------------------------- */
        private void SetBlue(int offset, byte value) {
            Marshal.WriteByte(data_.Scan0, offset, value);
        }
        #endregion

        /* ----------------------------------------------------------------- */
        /// 変数定義
        /* ----------------------------------------------------------------- */
        #region Member variables
        private Bitmap original_ = null;
        private BitmapData data_ = null;
        private bool disposed_ = false;
        #endregion
    }
}

使用方法としては,以下のように using 句を用いることを想定しています.

void TranslateImage(Bitmap original) {
    using (Cubic.BitmapAccessor img = new Cubic.BitmapAccessor(original)) {
        for (int h = 0; h < img.Height; ++h) {
            for (int w = 0; w < img.Width; ++w) {
                Color c = img.GetPixel(w, h);
                
                // ... 何らかの変換処理を行う
                
                img.SetPixel(w, h, c);
            }
        }
    }
}

各種 PixelFormat 対応

ラップする上で問題となるものの一つに「各種 PixelFormat に対応するか」と言うものがあります.例えば,各ピクセルの先頭のバイト位置を求めるにしても PixelFormat 毎に違いが出てきます.

Because the stride is the width of a row, to index any given row or Y coordinate you can multiply the stride by the Y coordinate to get the beginning of a particular row. Finding the correct pixel within the row is possibly more difficult and depends on knowing the layout of the pixel formats. The following examples show how to access a particular pixel for a given pixel format.

  • Format32BppArgb Given X and Y coordinates, the address of the first element in the pixel is Scan0+(y*stride)+(x*4). This Points to the blue byte. The following three bytes contain the green, red and alpha bytes.
  • Format24BppRgb Given X and Y coordinates, the address of the first element in the pixel is Scan0+(y*Stride)+(x*3). This points to the blue byte which is followed by the green and the red.
  • Format8BppIndexed Given the X and Y coordinates the address of the byte is Scan0+(y*Stride)+x. This byte is the index into the image palette.
  • Format4BppIndexed Given X and Y coordinates the byte containing the pixel data is calculated as Scan0+(y*Stride)+(x/2). The corresponding byte contains two pixels, the upper nibble is the leftmost and the lower nibble is the rightmost of two pixels. The four bits of the upper and lower nibble are used to select the colour from the 16 colour palette.
  • Format1BppIndexed Given the X and Y coordinates, the byte containing the pixel is calculated by Scan0+(y*Stride)+(x/8). The byte contains 8 bits, each bit is one pixel with the leftmost pixel in bit 8 and the rightmost pixel in bit 0. The bits select from the two entry colour palette.
http://www.bobpowell.net/lockingbits.htm

実際問題としては,いくつかの画像(フルカラー,インデックス,グレースケール)で試してみましたが,new Bitmap(filepath) のような形でロードすると Bitmap クラスが Format32BppArgb に変換するようなので,取りあえず Format32BppArgb だけ考慮すれば,ある程度は使えるようです.ただし,インデックスカラーの画像等をこの形でロード and セーブすると 32bit の αRGB に展開されますのでファイルサイズが増大します.そう言った事も考慮する必要がある場合は,各種 PixelFormat に対する処理をもう少し真面目に実装する必要がありそうです.