フルカラービットマップからグレースケールビットマップへの変換

下準備(プロジェクトの作成)

Winfowsフォームアプリケーションを新規作成します。プロジェクト名は、ImageFiltersとしておきます。

以下のようなフォームを作成します。
ボタンを2つ、パネルを1つ、(パネル内に)ピクチャボックスを1つ配置します。


2つのボタンのAnchorプロパティをbottom,leftに設定します。
パネルのAutoScrollプロパティをtrueに、Anchorプロパティをtop,bottom,left,rightに設定します。
ピクチャボックスのSizeModeプロパティをAutoScrollに設定します。
2つのボタンをダブルクリックし、イベントハンドラのコードを生成します。

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

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

        private void button1_Click(object sender, EventArgs e)
        {

        }

        private void button2_Click(object sender, EventArgs e)
        {

        }
    }
}

下準備(ビットマップの表示)

次に、ボタン1をクリックすると、ファイル選択ダイアログが開き、選択された画像がパネル内に表示されるようにします。
ここで、button1という名前は不適切なので、NameプロパティをbuttonLoadに変更します。また、Textプロパティを「読み込み」に変更します。
イベントハンドラの名前(button1_Click)も適切な名前に変更する必要があります。イベントハンドラ名の上にマウスカーソルを持っていき、[リファクタ]→[名前の変更]を選択します。
新しい名前にbuttonLoad_Clickを選択し、OKを押します。


変更箇所のプレビューが表示されます。ここでは、メソッド名と同様に、イベントハンドラの登録部分が変更されることを確認します。
Form1というクラス名等も不適切ですので、同様に変更しておくと良いでしょう。


buttonLoadがクリックされた際に行う処理は、以下となります。

    • ファイルオープンダイアログを開き、画像を選択する
    • 選択した画像を元にしてBitmapクラスのオブジェクトを作成する
    • 作成したBitmapクラスのオブジェクトをピクチャボックスに設定する

上記を実装したコードを以下に示します。なお、フォームの名前をImageFiltersFormに、ピクチャボックスの名前をpictureBoxに、パネルの名前をpanelに変更しています。さらに、Bitmapクラスのオブジェクトを保持するため、bitmapというメンバ変数を追加しています。

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

namespace ImageFilters
{
    public partial class ImageFiltersForm : Form
    {
        public ImageFiltersForm()
        {
            InitializeComponent();
        }

        private Bitmap bitmap = null;

        private void buttonLoad_Click(object sender, EventArgs e)
        {
            OpenFileDialog dialog = new OpenFileDialog();
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                if (this.bitmap != null)
                {
                    this.bitmap.Dispose();
                    this.bitmap = null;
                }
                try
                {
                    this.bitmap = new Bitmap(dialog.FileName);
                }
                catch
                {
                    MessageBox.Show("画像ファイル読み込みに失敗しました。");
                    return;
                }
                this.pictureBox.Image = this.bitmap;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {

        }
    }
}

ここまでの作業で、以下のように、ビットマップ画像をパネルに表示することが可能になりました。

グレースケール化

次に、ボタン2をクリックすると、パネル上にグレースケール化された画像が表示されるようにします。
button2についても、(button1と同様に)不適切な名前のため、buttonExecに変更します。イベントハンドラ名も同様に変更します。
buttonExecがクリックされた際に行う処理は、以下となります。ここでは、簡単のため、24bitフルカラー画像から8bitグレースケール画像への変換のみを考慮します。

    • 出力ビットマップの領域確保
    • 入出力ビットマップをシステムメモリにロック
    • ポインタを介して、pixcel毎にグレースケール化
    • グレースケールのパレット情報の設定
    • 入出力ビットマップのロックを解除

以下のような疑似コードを元に、順次実装していきます。System.Drawing.Imagingにあるクラスを使用するため、事前にusing指定しておくと良いかと思います。また、ポインタを使用するunsafeブロックが存在するため、プロジェクトの設定を変更し、unsafeコードを許可する必要があります。

        private void buttonExec_Click(object sender, EventArgs e)
        {
            if (bitmap == null)
            {
                return;
            }
            if (bitmap.PixelFormat != PixelFormat.Format24bppRgb)
            {
                MessageBox.Show("24ビットの画像ファイルを指定してください。");
                return;
            }

            this.pictureBox.Image = null;

            unsafe
            {
                // 出力ビットマップの領域確保
                // 入出力ビットマップをシステムメモリにロック
                // ポインタを介して、ピクセル毎にグレースケール化
                // グレースケールのパレット情報の設定
                // 入出力ビットマップのロックを解除
            }

            this.pictureBox.Image = this.grayscale;
        }

出力ビットマップを保持するために、事前に、grayscaleというメンバ変数を追加します。
出力ビットマップの領域は、以下のように確保します。別途、パレット情報を与える必要がありますが、それについては後述します。

                // 出力ビットマップの領域確保
                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;

上記の処理により、入力ビットマップと出力ビットマップのピクセルデータの先頭アドレスが取得できたので、ピクセル毎にグレースケール化を行います。グレースケール化の方法は、RGB値の平均値を使用しています。

                // ポインタを介して、pixcel毎にグレースケール化
                for (int y = 0; y < this.bitmap.Height; y++)
                {
                    for (int x = 0; x < this.bitmap.Width; x++)
                    {
                        datas8bppGray[y * this.bitmap.Width + x] = (byte)(
                            ((int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3]) +
                            (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + 1]) +
                            (int)(datas24bppRgb[y * this.bitmap.Width * 3 + x * 3 + 2])) /
                            3);
                    }
                }

8ビットインデックスカラーのビットマップをグレースケールにするために、パレット情報を生成します。具体的には、0x00から0xffまでの値を、順番に、RGB全てに与えています。

                // グレースケールのパレット情報の設定
                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);

ここまでの手順で、以下のように、24ビットフルカラー画像を8ビットグレースケール画像に変換できます。



おわりに

ここまでの手順で作成したグレースケール変換処理には、実は、バグがあります。次回はその辺りの所を書こうと思っています。