OpenCVSharp「インペイント」のような物を試作する。

コンピュータ

OpenCVのInpaint()のように指定範囲の周辺色で塗りつぶすプログラムを作ってみます。

実行環境構築

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new winforms
dotnet add package OpenCvSharp4.Windows
dotnet add package OpenCvSharp4.Extensions
code .

ソースプログラム

namespace InpaintSample2;

using OpenCvSharp;
using OpenCvSharp.Extensions;
public partial class Form1 : Form
{
    Mat? view = null;

    public class Line_xy
    {
        public int SY {get; set;} = 0;
        public int SX {get; set;} = 0;
        public int EY {get; set;} = 0;
        public int EX {get; set;} = 0;
    };

    static Mat filter(Mat src, Mat mask)
    {
        Mat result = src.Clone();

        var line_xy = new List<Line_xy>();

        // 横方向へループ
        for (int x = 0; x < src.Width; x++) {
            int start_y = -1;
            // 縦方向へループ
            for (int y = 0; y < src.Height; y++) {
                byte b = mask.At<byte>(y, x);

                if (b == 255) {
                    // ON(WHITE)
                    if (start_y == -1) {
                        start_y = y; // 開始座標をセット
                    }
                } else {
                    // OFF(BLACK)
                    if (start_y > -1) {
                        var o = new Line_xy { SY = start_y, SX = x, EY = y-1, EX = x};
                        line_xy.Add(o);
                        start_y = -1;
                    }
                }
            } // for y
            if (start_y > -1) {
                var o = new Line_xy { SY = start_y, SX = x, EY = result.Height-1, EX = x};
                line_xy.Add(o);
            }
        }// for x

        foreach(var line in line_xy) {
            if (line.SY > 0) {
                var c = result.At<Vec4b>(line.SY-1, line.SX);
                Cv2.Line(result, line.SX, line.SY, line.EX, line.EY, new Scalar(c[0], c[1], c[2], 255));
            } else if (line.EY < (result.Height-1)) {
                var c = result.At<Vec4b>(line.EY+1, line.EX);
                Cv2.Line(result, line.SX, line.SY, line.EX, line.EY, new Scalar(c[0], c[1], c[2], 255));
            }
        }
        return result;
    }
    public Form1()
    {
        InitializeComponent();

        Text = "インペイント";
        Size = new System.Drawing.Size(850, 800);

        var fnt = new Font("MS UI Gothic", 12);
        var vb = new Bitmap(800, 600);
        using (var g = Graphics.FromImage(vb)) {
            g.FillRectangle(Brushes.White, 0, 0, vb.Width, vb.Height);
            g.DrawString("こちらにD&D", fnt,  Brushes.Green, 0.0f, 0.0f);
        }
        
        var ud = new SplitContainer {
            Dock = DockStyle.Fill,
            Orientation = Orientation.Horizontal,   // 上下
            Panel1MinSize = 120,
        };
        var picboxV = new PictureBox {
            SizeMode = PictureBoxSizeMode.Zoom,
            Dock = DockStyle.Fill,
            
            AllowDrop = true,
            Image = vb,
        };
        var execBtn = new RadioButton {
            Text = "フィルターOFF",
            Appearance = Appearance.Button,
            AutoCheck = false,
            Location = new System.Drawing.Point(10, 10),
            Size = new System.Drawing.Size(180, 50),
        };
        var toClip = new Button {
            Text = "コピー",
            Location = new System.Drawing.Point(200, 10),
            Size = new System.Drawing.Size(120, 50),
        };
        var fromClip = new Button {
            Text = "貼り付け",
            Location = new System.Drawing.Point(330, 10),
            Size = new System.Drawing.Size(120, 50),
        };


        picboxV.DragEnter += (sender, e) => {
            if (e.Data == null) return;
            if(!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
            e.Effect = DragDropEffects.Copy;
        };
        picboxV.DragDrop += (sender, e) => {
            if (e.Data == null) return;
            var fd = e.Data.GetData(DataFormats.FileDrop);
            if (fd == null) return;
            string? path = ((string[])fd)[0];
            if (view is not null) view.Dispose();
            view = Cv2.ImRead(path);
            if (picboxV.Image is not null) picboxV.Image.Dispose();
            picboxV.Image = BitmapConverter.ToBitmap(view);
            execBtn.Checked = false;
        };
        execBtn.Click += async (sender, e) => {
            if (view is null) return;
            if (execBtn.Checked == true) {
                if (picboxV.Image is not null) picboxV.Image.Dispose();
                picboxV.Image = BitmapConverter.ToBitmap(view);
                execBtn.Text = "フィルターOff";
                execBtn.Checked = false;
            } else {
                var bitmap = await Task.Run<Bitmap>(()=>{
                    using (var mask = new Mat()) 
                    {
                        Cv2.CvtColor(view, mask, ColorConversionCodes.RGB2GRAY);

                        for (int y = 0; y < view.Height; y++) {
                            for (int x = 0; x < view.Width; x++) {
                                var pic = view.At<Vec3b>(y, x);
                                if (pic[0] == 255 && pic[1] == 0 && pic[2] == 255) {
                                    mask.Set(y, x, (byte)255);                            
                                } else {
                                    mask.Set(y, x, (byte)0);
                                }
                            }
                        }

                        // Cv2.Inpaint(view, mask, result, 0.0, InpaintMethod.Telea);

                        using (var result = filter(view, mask)) {
                            return BitmapConverter.ToBitmap(result);
                        }
                    }
                });
                picboxV.Image?.Dispose();
                picboxV.Image = bitmap;
                execBtn.Text = "フィルターOn";
                execBtn.Checked = true;
            }            
        };
        toClip.Click += (sender, e) => {
            if (picboxV.Image is null) return;
            var ms = new MemoryStream();
            picboxV.Image.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
            Clipboard.SetData("PNG", ms);            
        };
        fromClip.Click += (sender, e) => {
            var obj = Clipboard.GetData("PNG");
            if (obj == null) return;
            var ms = (MemoryStream)obj;
            if (ms is null) return;
            if (view is not null) view.Dispose();
            using(var b = new Bitmap(ms)) {
                view = BitmapConverter.ToMat(b);
                if (view.Channels() == 4)
                    Cv2.CvtColor(view, view, ColorConversionCodes.RGBA2RGB);
            }
            if (picboxV.Image is not null) picboxV.Image.Dispose();
            picboxV.Image = BitmapConverter.ToBitmap(view);
            execBtn.Checked = false;
        };

        ud.Panel1.Controls.Add(picboxV);
        ud.Panel2.Controls.AddRange(new Control[]{execBtn,toClip,fromClip});
        Controls.Add(ud);
    }
}

実行

dotnet run

元画像

インペイント処理を行う部分をペイントソフトで紫色(255,0.255)で塗りつぶしておく。(GIMPの場合、塗りつぶしのプロパティ⇒類似色の識別→なめらかにのチェックを外して塗りつぶし。PNG形式など劣化しないファイル形式が望ましい。)


ドラッグアンドドロップ

処理実行

画像を縦方向に調べて、ペイント処理を行う部分を検出したら、その直前の上(または直後の下)のピクセルの色で塗りつぶします。かなり単純なルーチンですが、素材画像によっては、いい感じに塗りつぶされる場合があります。

コメント