C#で画像の差分を表示するプログラム

コンピュータ
2つの画像の各ピクセルの差を計算し表示しています。

実行環境構築

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new winforms
code .

ソースプログラム

using System.Drawing.Imaging;

namespace ImgDiff;

public partial class Form1 : Form
{
    // Bitmapオブジェクトをbyte配列に変換
    static public byte[] BitmapToBytes(Bitmap src)
    {
        var bmpData = src.LockBits(
            new Rectangle(0, 0, src.Width, src.Height),
            ImageLockMode.ReadOnly,
            src.PixelFormat);
        
        byte[] bytes = new byte[Math.Abs(bmpData.Stride) * src.Height];

        System.Runtime.InteropServices.Marshal.Copy(
            bmpData.Scan0,
            bytes, 0, bytes.Length);
        
        src.UnlockBits(bmpData);

        return bytes;
    }
    // byte配列をにBitmapオブジェクト変換
    static public Bitmap BytesToBitmap(byte[] bytes, int width, int height, PixelFormat format)
    {
        var dst = new Bitmap(width, height, format);

        var bmpData = dst.LockBits(
            new Rectangle(0, 0, dst.Width, dst.Height),
            ImageLockMode.ReadWrite,
            dst.PixelFormat);

        System.Runtime.InteropServices.Marshal.Copy(
            bytes,
            0, bmpData.Scan0,
            bytes.Length);
        dst.UnlockBits(bmpData);

        return dst;
    }
    public Form1()
    {
        InitializeComponent();

        Text = "画像の差分";

        // ベース画像
        var fnt = new Font("MS UI Gothic", 24);
        var resultBitmap = new Bitmap(800, 600);
        using (var g = Graphics.FromImage(resultBitmap))
        {
            g.FillRectangle(Brushes.White, 0, 0, resultBitmap.Width, resultBitmap.Height);
            g.DrawString("結果", fnt,  Brushes.Black, 0.0f, 0.0f);
        }

        var aBitmap = new Bitmap(800, 600);
        using (var g = Graphics.FromImage(aBitmap))
        {
            g.FillRectangle(Brushes.White, 0, 0, aBitmap.Width, aBitmap.Height);
            g.DrawString("A:画像をD&D", fnt,  Brushes.Black, 0.0f, 0.0f);
        }

        var bBitmap = new Bitmap(800, 600);
        using (var g = Graphics.FromImage(bBitmap))
        {
            g.FillRectangle(Brushes.White, 0, 0, bBitmap.Width, bBitmap.Height);
            g.DrawString("B:画像をD&D", fnt,  Brushes.Black, 0.0f, 0.0f);
        }
        // スプリットコンテナ
        var scViewConsole = new SplitContainer
        {
            Dock = DockStyle.Fill,
            Orientation = Orientation.Horizontal,   // 上下
            Panel1MinSize = 120,
        };
        // スプリットコンテナ
        var scInOut = new SplitContainer
        {
            Dock = DockStyle.Fill,
            Orientation = Orientation.Vertical,   // 左右
            Panel1MinSize = 240,
        };
        // スプリットコンテナ
        var scAb = new SplitContainer
        {
            Dock = DockStyle.Fill,
            Orientation = Orientation.Horizontal,   // 上下
            Panel1MinSize = 120,
        };
        // ピクチャボックス
        var picboxResult = new PictureBox
        {
            SizeMode = PictureBoxSizeMode.AutoSize,
            Image = (Bitmap)resultBitmap.Clone(),
        };
        // ピクチャボックス
        var picboxA = new PictureBox
        {
            SizeMode = PictureBoxSizeMode.AutoSize,
            Image = aBitmap,
        };
        // ピクチャボックス
        var picboxB = new PictureBox
        {
            SizeMode = PictureBoxSizeMode.AutoSize,
            Image = bBitmap,
        };
        // パネル
        var flowPanel = new FlowLayoutPanel
        {
            Dock = DockStyle.Fill,
        };
        // 実行ボタン
        var execButton = new Button
        {
            Text = "実行",
            Size = new System.Drawing.Size(180, 50),
        };
        // コントロールの追加
        flowPanel.Controls.AddRange(new Control[]
        {
            execButton,
        });

        scAb.Panel1.Controls.Add(picboxA);
        scAb.Panel1.AutoScroll = true;
        scAb.Panel1.AllowDrop = true;

        scAb.Panel2.Controls.Add(picboxB);
        scAb.Panel2.AutoScroll = true;
        scAb.Panel2.AllowDrop = true;

        scInOut.Panel1.Controls.Add(picboxResult);
        scInOut.Panel1.AutoScroll = true;
        scInOut.Panel1.AllowDrop = false;

        scInOut.Panel2.Controls.Add(scAb);
        scViewConsole.Panel1.Controls.Add(scInOut);
        scViewConsole.Panel2.Controls.Add(flowPanel);

        Controls.Add(scViewConsole);

        // イベント

        // ドラッグエンターイベント
        scAb.Panel1.DragEnter += (sender, e) =>
        {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            e.Effect = DragDropEffects.Copy;
        };
        // ドラッグアンドドロップイベント
        scAb.Panel1.DragDrop += (sender, e) =>
        {
            if (e.Data == null) return;
            var fd = e.Data.GetData(DataFormats.FileDrop);
            if (fd == null) return;
            string? path = ((string[])fd)[0];
            picboxA.Image?.Dispose();
            using(var fs = new FileStream(path, FileMode.Open))
            {
                picboxA.Image = new Bitmap(fs);
            }
        };
        // ドラッグエンターイベント
        scAb.Panel2.DragEnter += (sender, e) =>
        {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            e.Effect = DragDropEffects.Copy;
        };
        // ドラッグアンドドロップイベント
        scAb.Panel2.DragDrop += (sender, e) =>
        {
            if (e.Data == null) return;
            var fd = e.Data.GetData(DataFormats.FileDrop);
            if (fd == null) return;
            string? path = ((string[])fd)[0];
            picboxB.Image?.Dispose();
            using(var fs = new FileStream(path, FileMode.Open))
            {
                picboxB.Image = new Bitmap(fs);
            }
        };
        // 実行ボタンをクリック
        execButton.Click += (s, e) =>
        {
            if (picboxA.Image is null)
            {
                MessageBox.Show("A画像が未設定", "警告");
                return;
            }
            if (picboxB.Image is null)
            {
                MessageBox.Show("B画像が未設定", "警告");
                return;
            }
            if (picboxA.Image.Width != picboxB.Image.Width || picboxA.Image.Height != picboxB.Image.Height)
            {
                MessageBox.Show("AB画像の幅、高さが異なる", "警告");
                return;
            }
            if (picboxA.Image.PixelFormat != picboxB.Image.PixelFormat)
            {
                MessageBox.Show("AB画像がPixelFormatが異なる", "警告");
                return;
            }

            byte[] abytes = BitmapToBytes((Bitmap)picboxA.Image);
            byte[] bbytes = BitmapToBytes((Bitmap)picboxB.Image);
            byte[] cbytes = new byte[bbytes.Length];

            int stride = cbytes.Length / picboxA.Image.Height;
            int channel = Bitmap.GetPixelFormatSize(picboxA.Image.PixelFormat) / 8;
            for (int ch = 0; ch < channel; ch++)
            {
                for (int y = 0; y < picboxA.Image.Height; y++)
                {
                    for (int x = 0; x < picboxA.Image.Width; x++)
                    {
                        int a = abytes[y * stride + x * channel + ch];
                        int b = bbytes[y * stride + x * channel + ch];
                        int c = Math.Abs(a - b);
                        if (ch == 3)
                        {
                            // アルファチャンネルは強制的に255
                            cbytes[y * stride + x * channel + ch] = 255;
                        }
                        else
                        {
                            cbytes[y * stride + x * channel + ch] = (byte)c;
                        }
                    }
                }
            }
            
            Bitmap result = BytesToBitmap(cbytes, picboxA.Image.Width, picboxA.Image.Height, picboxA.Image.PixelFormat);
            picboxResult.Image?.Dispose();
            picboxResult.Image = result;
        };
    }
}

実行

dotnet run

使い方

A:及びB:に画像をドラッグアンドドロップ。
実行ボタンを押す

二つの画像に異なる部分(差)が無い場合、全面黒(0)になります。

感想

最初、差分画像が表示されず全面灰色になってしまい悩みました。原因はアルファチャンネルの差分まで計算したため結果が0で透明になっていたためでした。とりあえずアルファチャンネルは強制的に255を埋めるようにしています。

コメント