C#のWinFormsで画像加工アプリ3「2値化」

コンピュータ

2値化を行うフィルター機能を追加します。

ソースコード

ファイル名:Form1.cs(前回の記事のソースコードに追加)

    // 2値化(Form1のメンバーとして追加)
    readonly ToolStripMenuItem thresholdMenuItem = new()
    {
        Text = "2値化",
    };
        // 2値化(Form1のコンストラクタに追加)
        filterMenuItem.DropDownItems.Add(thresholdMenuItem);
        thresholdMenuItem.Click += async (s, e) =>
        {
            if (_buffBmp is null) return;
            Bitmap? bmp = await ThresholdDialog(_buffBmp);
            if (bmp is not null)
            {
                this.Bmp = bmp;
                mainPicbox.Image = this.Bmp;
            }
        };

ファイル名:Form1.Threshold.cs(新規追加)

using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace GazouKakou01;
public partial class Form1 : Form
{
    public static async Task<Bitmap?> ThresholdDialog(Bitmap src)
    {
        const string filterName = "2値化";

        Form frm = new Form
        {
            FormBorderStyle = FormBorderStyle.FixedDialog,
            ClientSize = new System.Drawing.Size(640, 480),
            Text = filterName,
        };
        Label lbl = new Label
        {
            Size = new System.Drawing.Size(600, 60),
            Location = new System.Drawing.Point(10, 10),
            Text = filterName,
        };
        lbl.Parent = frm;
        PictureBox picbox = new()
        {
            Size = new System.Drawing.Size(600, 320),
            Location = new System.Drawing.Point(10, 70),
            Parent = frm,
        };
        Button okBtn = new Button
        {
            Size = new System.Drawing.Size(80,40),
            Location = new System.Drawing.Point(10, 410),
            Text = "OK",
            DialogResult = DialogResult.OK,
            Visible = false,
        };
        okBtn.Parent = frm;
        Button cancelBtn = new Button
        {
            Size = new System.Drawing.Size(80,40),
            Location = new System.Drawing.Point(100, 410),
            Text = "Cancel",
            DialogResult = DialogResult.Cancel,
            Visible = false,
        };
        cancelBtn.Parent = frm;

        frm.AcceptButton = okBtn;
        frm.CancelButton = cancelBtn;

        Label label1 = new()
        {
            Text = "閾値:0",
            Size = new System.Drawing.Size(120,40),
            Location = new System.Drawing.Point(190, 410),
            Parent = frm,
        };
        TrackBar track1 = new()
        {
            Minimum = 0,
            Maximum = 255,
            Value = 0,
            Size = new System.Drawing.Size(256,40),
            Location = new System.Drawing.Point(320, 410),
            Parent = frm,
        };
        Mat srcMat = await Task.Run(()=>
        {
            // mat変換
            var mat = BitmapConverter.ToMat(src);
            if (mat.Channels() >= 3)
            {
                // グレイスケール
                Cv2.CvtColor(mat, mat, ColorConversionCodes.RGB2GRAY);
            }
            return mat;
        });
        double th = 127.0d;
        Func<Task> filter = new(async ()=>
        {
            okBtn.Visible = false;
            cancelBtn.Visible = false;

            var dst = await Task.Run(()=>
            {
                var mat = new Mat();
                picbox?.Invoke(()=>{th = (double)track1.Value;});
                // 2値化
                Cv2.Threshold(srcMat, mat, th, 255.0d, ThresholdTypes.Binary);
                // bmp変換
                return BitmapConverter.ToBitmap(mat);
            });
            picbox.Image?.Dispose();
            picbox.Image = dst;
            okBtn.Visible = true;
            cancelBtn.Visible = true;
            label1.Text = String.Format("閾値:{0}", th);
        });
        track1.ValueChanged += async (s, e) => await filter();
        frm.Load += (s, e) =>
        {
            track1.Value = 127;
        };

        if (frm.ShowDialog() == DialogResult.Cancel)
        {
            return null;
        }
        return (Bitmap)picbox.Image;
    }
}

実行

画像が表示されている状態でメインメニュー「フィルター」→「2値化」を選ぶ

閾値のトラックバーの移動に合わせてフィルターの結果も変化します。

解説

8bitグレスケールの場合、取りうる値は0~255の256パターンになります。2値化の場合、これを0と1の2パターンに分類することになります。0と1であれば1ピクセルを1bitの配列で表現することも出来ますがbit演算は難しいので、8bitのまま0を0(黒)、1を255(白)としています。

2値化は色々と使い道があり、個人的にはざっくりとした物体の範囲を色で選択することによく使います。GIMPで色域選択やファジー選択をする場合、アンチエイリアスの影響で境界部分が上手く選択できない場合がありますが、2値化した画像だと上手く選択できる場合があります。

また、境界線を検出するフィルタ、直線を検出、物体検出するなどでも2値化画像が使われます。元がカラー画像の場合2値化する前に一度グレスケール化する必要がありますので、グレスケール化と2値化はセットで使う場合が多いです。

2値化する方式も色々ありますが、今回はシンプルに閾値を元に閾値より下を0(黒)それ以外を255(白)にするようになっています。
閾値はトラックバーで調整することが出来るようにしており、トラックバーの変化でプレビュー画像も連動して変化するようにしてあります。

コメント