C#(GDI+) C#(others)  Delphi(Graphics) Delphi(others)  BBS Blog   Link Misc. Index

C#(GDI+) : Bitmap の内部色データにアクセスする (1)

C#(GDI+) : Bitmap の内部色データにアクセスする (1)

Bitmap オブジェクトのなかの色データにアクセスして、いろいろな処理を実行したい。出来るだけ高速に、しかもピクセルの座標を指定してランダムアクセスできることが理想である。

今回は、アクセスする方法をいろいろ試して、その処理時間を計測した。


photo by SeenyaRita

最初は Bitmap クラスの GetPixel() SetPixel() メソッドを試してみる。処理の内容は、各ピクセルの R 成分と G 成分を入れ換える。

public Color GetPixel (int x, int y)
public void SetPixel (int x, int y, Color color)

        private void button1_Click(object sender, EventArgs e)
        {
            Bitmap bmp = new Bitmap(@"tomatoes.png");

            int w = bmp.Width;
            int h = bmp.Height;

            Graphics g = this.CreateGraphics();
            g.DrawImage(bmp, 5, 5, w, h);

            Color getC;

            StopWatch.Start();
            
            for (int y = 0; y < h; y++)
                for (int x = 0; x < w; x++)
                {
                    getC = bmp.GetPixel(x, y);
                    bmp.SetPixel(x, y, Color.FromArgb(getC.G, getC.R, getC.B));
                }
                
            StopWatch.Stop();
            label1.Text = String.Format("{0:F} msec", StopWatch.time);

            g.DrawImage(bmp, 5, h + 10, w, h);

            g.Dispose();

            bmp.Dispose();
        }

この方法は簡単だが、図を見ると分かるように遅い。わたしの古い PC では 517ms かかった。

なお、処理時間を計測するときは、かならず CTL+F5 を押してリリースビルドすること。デバッグビルドでは、正確な時間を計れない。

次は、LockBits() UnlockBits() を使って、ヘルプの例を真似して byte[] 配列にコピーし、処理したあとそれを元に戻すことを試す。

        private void button2_Click(object sender, EventArgs e)
        {
            Bitmap bmp = new Bitmap(@"tomatoes.png");

            int w = bmp.Width;
            int h = bmp.Height;

            Graphics g = this.CreateGraphics();
            g.DrawImage(bmp, 5, 5, w, h);

            StopWatch.Start();

            Rectangle rect = new Rectangle(0, 0, w, h);
            BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, 
                                                      PixelFormat.Format24bppRgb);

            IntPtr ptr = bmpData.Scan0;
            int stride = bmpData.Stride;

            int bytes = stride * h ;
            byte[] rgbValues = new byte[bytes];

            // Copy the RGB values into the array.
            System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);

            int initX, ix;
            byte tmp;

            for (int y = 0; y < h; y++)
            {
                initX = stride * y;
                for (int x = 0; x < w; x++)
                {
                    ix = initX + x*3;
                    tmp = rgbValues[ix + 1]; // green
                    rgbValues[ix + 1] = rgbValues[ix + 2]; // green <-- red
                    rgbValues[ix + 2] = tmp;               // red <-- green
                }
            }

            // Copy the RGB values back to the bitmap
            System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);

            bmp.UnlockBits(bmpData);

            StopWatch.Stop();
            label2.Text = String.Format("{0:F} msec", StopWatch.time);

            g.DrawImage(bmp, 5, h+10, w, h);
            g.Dispose();

            bmp.Dispose();
        }

処理の最初は、LockBits() によって内部メモリをロックする。

public BitmapData LockBits (Rectangle rect, ImageLockMode flags, PixelFormat format)

最初のパラメータは、ロックする領域を設定する Rectangle 構造体であり、特別の事情がない限り、このコードのように全領域をロックする。第二パラメータは、アクセス レベル (読み取り/書き込み) を指定する ImageLockMode 列挙体であり、{ ReadOnly, ReadWrite, UserInputBuffer, WriteOnly }のメンバから選ぶ。通常は ReadWrite を選んでおく。最後のパラメータは、データ形式を指定する PixelFormat 列挙体であり、ここでは Format24bppRgb を選ぶ。

LockBits() の戻り値が、このロック処理に関する情報を格納している BitmapData クラスのインスタンスであり、このフィールドを通して色データにアクセスする。

BitmapData.Scan0 プロパティには、ビットマップ内の最初のピクセルデータのアドレスが設定されている。また、BitmapData.Stride プロパティには、イメージの横一行のバイト数が設定されている。上のコードでは、Scan0Stride を使って、全色データを byte[] rgbValues 配列にコピーしている。それから、RG を入れ換え、もとに戻している。

処理が終わったら、LockBits() で取得した BimapData を設定して UnlockBits() を呼び出す。

public void UnlockBits (BitmapData bitmapdata)

これは、図を見ると前回の GetPixel() SetPixel() より100倍以上速い。


最後に、unsafe{ } ブロックを使ってポインタでアクセスすることを試した。

        private void button3_Click(object sender, EventArgs e)
        {
            Bitmap bmp = new Bitmap(@"tomatoes.png");

            int w = bmp.Width;
            int h = bmp.Height;

            Graphics g = this.CreateGraphics();
            g.DrawImage(bmp, 5, 5, w, h);

            StopWatch.Start();

            Rectangle rect = new Rectangle(0, 0, w, h);
            BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite,
                                                       PixelFormat.Format24bppRgb);

            int stride = bmpData.Stride;

            unsafe
            {
                byte* p = (byte*)(void*)bmpData.Scan0;
                int nResidual = stride - w * 3;
                byte green;

                for (int y = 0; y < h; ++y)
                {
                    for (int x = 0; x < w; ++x)
                    {
                        green = p[1];
                        p[1] = p[2];
                        p[2] = green;

                        p += 3;
                    }
                    p += nResidual;
                }
            }

            bmp.UnlockBits(bmpData);

            StopWatch.Stop();
            label3.Text = String.Format("{0:F} msec", StopWatch.time);

            g.DrawImage(bmp, 5, h + 10, w, h);
            g.Dispose();

            bmp.Dispose();
        }
    }

このコードをコンパイルするには、オプションに /unsafe を設定しなければならない。VC#IDE では、ソリューションエクスプローラのプロジェクトのところを右クリックし、コンテキストメニューの プロパティー を選んで表示されるダイアログの ビルド タブを選んで アンセーフコードの許可にチェックをいれる ことでコンパイルできる。

上のコードでは byte* p を使って、各ピクセルの色データの3バイトの最初のバイト(B 成分)を指し、p[1]G 成分、p[2]R 成分にアクセスしている。

このコードは、前回の配列コピーの場合より、さらに4倍以上速い。この方法は unsafe{} ブロックを含むが、もっとも高速なので以後はこの方法を使うことにする。


全コードをしめす。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

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


namespace AccessColorData1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Bitmap bmp = new Bitmap(@"tomatoes.png");

            int w = bmp.Width;
            int h = bmp.Height;

            Graphics g = this.CreateGraphics();
            g.DrawImage(bmp, 5, 5, w, h);

            Color getC;

            StopWatch.Start();
            for (int y = 0; y < h; y++)
                for (int x = 0; x < w; x++)
                {
                    getC = bmp.GetPixel(x, y);
                    bmp.SetPixel(x, y, Color.FromArgb(getC.G, getC.R, getC.B));
                }
            StopWatch.Stop();
            label1.Text = String.Format("{0:F} msec", StopWatch.time);

            g.DrawImage(bmp, 5, h + 10, w, h);

            g.Dispose();

            bmp.Dispose();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Bitmap bmp = new Bitmap(@"tomatoes.png");

            int w = bmp.Width;
            int h = bmp.Height;

            Graphics g = this.CreateGraphics();
            g.DrawImage(bmp, 5, 5, w, h);

            StopWatch.Start();

            Rectangle rect = new Rectangle(0, 0, w, h);
            BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, 
                                                      PixelFormat.Format24bppRgb);

            IntPtr ptr = bmpData.Scan0;
            int stride = bmpData.Stride;

            int bytes = stride * h ;
            byte[] rgbValues = new byte[bytes];

            // Copy the RGB values into the array.
            System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);

            int initX, ix;
            byte tmp;

            for (int y = 0; y < h; y++)
            {
                initX = stride * y;
                for (int x = 0; x < w; x++)
                {
                    ix = initX + x*3;
                    tmp = rgbValues[ix + 1]; // green
                    rgbValues[ix + 1] = rgbValues[ix + 2]; // green <-- red
                    rgbValues[ix + 2] = tmp;               // red <-- green
                }
            }

            // Copy the RGB values back to the bitmap
            System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);

            bmp.UnlockBits(bmpData);

            StopWatch.Stop();
            label2.Text = String.Format("{0:F} msec", StopWatch.time);

            g.DrawImage(bmp, 5, h+10, w, h);
            g.Dispose();

            bmp.Dispose();
        }

        private void button3_Click(object sender, EventArgs e)
        {
            Bitmap bmp = new Bitmap(@"tomatoes.png");

            int w = bmp.Width;
            int h = bmp.Height;

            Graphics g = this.CreateGraphics();
            g.DrawImage(bmp, 5, 5, w, h);

            StopWatch.Start();

            Rectangle rect = new Rectangle(0, 0, w, h);
            BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite,
                                                       PixelFormat.Format24bppRgb);

            int stride = bmpData.Stride;

            unsafe
            {
                byte* p = (byte*)(void*)bmpData.Scan0;
                int nResidual = stride - w * 3;
                byte green;

                for (int y = 0; y < h; ++y)
                {
                    for (int x = 0; x < w; ++x)
                    {
                        green = p[1];
                        p[1] = p[2];
                        p[2] = green;

                        p += 3;
                    }
                    p += nResidual;
                }
            }

            bmp.UnlockBits(bmpData);

            StopWatch.Stop();
            label3.Text = String.Format("{0:F} msec", StopWatch.time);

            g.DrawImage(bmp, 5, h + 10, w, h);
            g.Dispose();

            bmp.Dispose();
        }
    }

    public static class StopWatch
    {
        [DllImport("kernel32.dll")]
        extern static short QueryPerformanceCounter(ref long x);

        [DllImport("kernel32.dll")]
        extern static short QueryPerformanceFrequency(ref long x);

        private static double strt;
        public static double time;

        public static void Start()
        {
            long cnt = 0;
            long frq = 0;
            QueryPerformanceCounter(ref cnt);
            QueryPerformanceFrequency(ref frq);
            strt = (double)cnt / (double)frq;
        }

        public static void Stop()
        {
            long cnt = 0;
            long frq = 0;
            QueryPerformanceCounter(ref cnt);
            QueryPerformanceFrequency(ref frq);
            double c = (double)cnt / (double)frq;
            time = (c - strt) * 1000;
        }
    }
}

■ 参考

対応するブログ記事1  対応するブログ記事2  StopWatch クラス
▲top