プログラミング素人のはてなブログ

プログラミングも電気回路も専門外の技術屋の末端が勉強したことや作品をアウトプットするブログ。コードに間違いなど見つけられたら、気軽にコメントください。 C#、Python3、ラズパイなど。

C#で画像処理アプリを作った

ブログ用の簡単な画像処理ができるアプリを作りました。

欲しい機能としては以下でした。
クリップボードから画像を取り込む
・画像ファイルから画像を読み込む(File open dialog or DragDrop)
・適切な大きさに縮小する
・周囲を囲む
・四角く塗りつぶす
・トリミングする
・保存するときに自動的に名前を付ける
・ひとつの工程分だけ戻すことが出来る(Undo)

Windows標準のペイントや、ペイント3D、フリーソフトGIMPなどはありますが、そこまで高度でなくていいし、もっと限定的な用途に特化して作業効率アップが目的です。

ここで使ったC#のコードの一部を紹介します。
Dobon.netのサンプルを大量に参考にしています。
dobon.net

ファイルオープン

フォルダアイコンをクリックしたとき。

private void buttonFileOpen_Click(object sender, EventArgs e)
{
    //OpenFileDialogクラスのインスタンスを作成
    OpenFileDialog ofd = new OpenFileDialog();
    //ダイアログを表示する
    if (ofd.ShowDialog() == DialogResult.OK)
    {
        //OKボタンがクリックされたとき、選択されたファイル名を表示する
        textBoxFileName.Text = ofd.FileName;
        PreviewDrowPicture();
    }
}

ファイルをドラッグドロップしたとき。

private void Form1_DragDrop(object sender, DragEventArgs e)
{
    //コントロール内にドロップされたとき実行される
    //ドロップされたすべてのファイル名を取得する
    string[] fileName = (string[])e.Data.GetData(DataFormats.FileDrop, false);
    //textBoxに追加する
    textBoxFileName.Text = fileName[0];
     PreviewDrowPicture();
}

private void Form1_DragEnter(object sender, DragEventArgs e)
{
    //コントロール内にドラッグされたとき実行される
    if (e.Data.GetDataPresent(DataFormats.FileDrop))
        //ドラッグされたデータ形式を調べ、ファイルのときはコピーとする
        e.Effect = DragDropEffects.Copy;
     else
        //ファイル以外は受け付けない
       e.Effect = DragDropEffects.None;
}

どちらも、TextBoxにファイル名(フルパス)をいったん取得し、 PreviewDrowPicture();を呼び出し画像描画します。

プレビュー

画像の出力先となる、BitmapGraphicsオブジェクトを作成し、imageを読み込みます。
出力サイズ FinalPictureHeightFinalPictureWidth以下になるように、reSizeRateを算出して縮小します。
labelStartMsg(xx)は最初に表示しているガイドです。
処理を開始したら不要ですので見えなくします。
クリップボードから画像を取得したときは、引数にその画像が入ります。
それ以外のときは、textBoxFileNameのファイルを取得します。

//描画先とするImageオブジェクトを作成する
Bitmap canvas = new Bitmap(pictureBoxWidth, pictureBoxHeight);

private void PreviewDrowPicture(Image defImg = null)
{
        //ImageオブジェクトのGraphicsオブジェクトを作成する
        Graphics g = Graphics.FromImage(canvas);

        //画像ファイルを読み込んで、Imageオブジェクトとして取得する
        Image img;
        if (defImg == null)
        {
            img = Image.FromFile(textBoxFileName.Text);
        }
        else
        {
            img = defImg;
        }
        //リサイズ率が小さいほう
        reSizeRate = ((float)pictureBox1.Width / (float)img.Width) < ((float)pictureBox1.Height / (float)img.Height) ? (float)pictureBox1.Width / (float)img.Width : (float)pictureBox1.Height / (float)img.Height;

        if (reSizeRate > 1) reSizeRate = 1; //小さい画像は拡大しない
        //画像をcanvasの座標(0, 0)の位置に描画する
        float w = (float)img.Width;
        float h = (float)img.Height;
        FinalPictureHeight = (int)(h * reSizeRate);
        FinalPictureWidth = (int)(w * reSizeRate);
        g.DrawImage(img, 0, 0, (int)(w * reSizeRate), (int)(h * reSizeRate));
        //Imageオブジェクトのリソースを解放する
        img.Dispose();

        //Graphicsオブジェクトのリソースを解放する
        g.Dispose();

        labelStartMsg1.Visible = false;
        labelStartMsg2.Visible = false;
        labelStartMsg3.Visible = false;
        labelStartMsg4.Visible = false;
}

private void buttonGetClipbord_Click(object sender, EventArgs e)
{
    if (Clipboard.ContainsImage())
    {
        Image img = Clipboard.GetImage();
        PreviewDrowPicture(img);
    }
}

Undoできるようにする

意外とUndoは使います。
そのため、現在の画像を保存しておくことで、一つだけ工程を戻すことが出来るようにします。
何か処理を行うときには、先にこの関数を呼び出すようにしておくことで、状態を保存しておくことが出来ます。

using System.Drawing;
const int pictureBoxWidth = 1000;
const int pictureBoxHeight = 750;
Color[,] pic = new Color[pictureBoxWidth, pictureBoxHeight];

private void storeCurrentImage()
{
    Image img = pictureBox1.Image;
    Bitmap bitmap = new Bitmap(img);

    for (int x = 0; x < FinalPictureWidth; x++)
    {
        for (int y = 0; y < FinalPictureHeight; y++)
        {
            pic[x, y] = bitmap.GetPixel(x, y);
        }
    }
}

周囲を線で囲む

private void buttonDrowEdgeLine_Click(object sender, EventArgs e)
{
    storeCurrentImage();

    Color color;
    if (radioButtonDrowEdge_Red.Checked) color = Color.FromArgb(255, 0, 0);
    else if (radioButtonDrowEdge_Yellow.Checked) color = Color.FromArgb(255, 255, 0);
    else if (radioButtonDrowEdge_Green.Checked) color = Color.FromArgb(0, 255, 0);
    else if (radioButtonDrowEdge_Blue.Checked) color = Color.FromArgb(0, 0, 255);
    else color = Color.FromArgb(0, 0, 0);

    for (int x = 0; x < FinalPictureWidth; x++)
    {
        canvas.SetPixel(x, 0, color);
    }
    for (int y = 0; y < FinalPictureHeight; y++)
    {
        canvas.SetPixel(0, y, color);
    }
    for (int x = 0; x < FinalPictureWidth; x++)
    {
        canvas.SetPixel(x, FinalPictureHeight - 1, color);
    }
    for (int y = 0; y < FinalPictureHeight; y++)
    {
        canvas.SetPixel(FinalPictureWidth - 1, y, color);
    }
    pictureBox1.Image = canvas;
}

四角く囲む

マウスクリックイベントで2回クリックされたら、始点と終点として四角形の対角線として、四角を作成します。
1回目であるか2回目であるかはFlagを立てて区別します。
塗りつぶす場合も同様です。

private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
    if (FlagMask == 1)
    {
        Mask_address[0] = e.Location.X;//配列の0番にクリックした座標を入れる
        Mask_address[1] = e.Location.Y;//配列の1番にクリックした座標を入れる
        FlagMask = 2;

        //ImageオブジェクトのGraphicsオブジェクトを作成する
        Graphics g = Graphics.FromImage(canvas);

        //Penオブジェクトの作成(幅1の黒色)
        Pen p = new Pen(brush, 1);
        g.DrawRectangle(p, Mask_address[0], Mask_address[1], 1, 1);
        g.Dispose();
    }
    else if (FlagMask == 2)
    {
        Mask_address[2] = e.Location.X;//配列の2番に現在の座標を入れる
        Mask_address[3] = e.Location.Y;//配列の3番に現在の座標を入れる

        if (Mask_address[0] > Mask_address[2]) swap_address(ref Mask_address[0], ref Mask_address[2]);
        if (Mask_address[1] > Mask_address[3]) swap_address(ref Mask_address[1], ref Mask_address[3]);

        //ImageオブジェクトのGraphicsオブジェクトを作成する
        Graphics g = Graphics.FromImage(canvas);

        //Penオブジェクトの作成(幅1の黒色)
        Pen p = new Pen(Color.Black, 1);
        //長方形を描く
        int size_x = Mask_address[2] - Mask_address[0];
        int size_y = Mask_address[3] - Mask_address[1];
        if (size_x > 0 && size_y > 0)
        {
            g.FillRectangle(brush, Mask_address[0], Mask_address[1], size_x, size_y);
        }
        else if (size_x < 0 && size_y < 0)
        {
            g.FillRectangle(brush, Mask_address[2], Mask_address[3], -size_x, -size_y);
        }
        //リソースを解放する
        p.Dispose();
        g.Dispose();

        buttonDrowMask.Enabled = true;
        FlagMask = 0;

    }
}

<h3>トリミングする</h3>
トリミングはnumericUpDownでpixel数を指定し、切り取り後の範囲を指定して再描画します。
>|cs|
private void buttonTrimming_Click(object sender, EventArgs e)
{
    storeCurrentImage();
    Image img = pictureBox1.Image;
    //ImageオブジェクトのGraphicsオブジェクトを作成する
    Graphics g = Graphics.FromImage(canvas);

    //切り取る部分の範囲を決定する。
    int up = (int)numericUpDownTrimUp.Value;
    int down = (int)numericUpDownTrimDown.Value;
    int right = (int)numericUpDownTrimRight.Value;
    int left = (int)numericUpDownTrimLeft.Value;
    FinalPictureWidth -= (right + left);
    FinalPictureHeight -= (up + down);

    Rectangle srcRect = new Rectangle(left, up, FinalPictureWidth, FinalPictureHeight);
    //描画する部分の範囲。位置(0,0)、大きさX*Yで描画する
    Rectangle desRect = new Rectangle(0, 0, FinalPictureWidth, FinalPictureHeight);
    //画像の一部を描画する
    g.DrawImage(img, desRect, srcRect, GraphicsUnit.Pixel);

    //Graphicsオブジェクトのリソースを解放する
    g.Dispose();
}

名前を付けて保存

ここでの「トリミング」はCanvasより画像が小さい場合もあるため、Canvasの範囲を指定しています。

private void buttonSave_Click(object sender, EventArgs e)
{
    if (pictureBox1.Image != null)
    {
        int kakuchoshi = textBoxFileName.Text.IndexOf(".");
        string FileName;
        if (kakuchoshi == -1)
        {
            // https://dobon.net/vb/dotnet/file/getfolderpath.html
            FileName = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + "\\EasyRetouch.jpg";
            textBoxFileName.Text = FileName;
        }
        else
        {
            FileName = textBoxFileName.Text.Substring(0, kakuchoshi);
        }

        //描画先とするImageオブジェクトを作成する
        Bitmap canvas = new Bitmap(FinalPictureWidth, FinalPictureHeight);
        //ImageオブジェクトのGraphicsオブジェクトを作成する
        Graphics g = Graphics.FromImage(canvas);

        //トリミング
        Rectangle srcRect = new Rectangle(0, 0, FinalPictureWidth, FinalPictureHeight);
        //描画する部分の範囲。位置(0,0)、大きさX * Yで描画する
        Rectangle desRect = new Rectangle(0, 0, FinalPictureWidth, FinalPictureHeight);

        //画像の一部を描画する
        g.DrawImage(pictureBox1.Image, desRect, srcRect, GraphicsUnit.Pixel);
        string suffix = textBoxSuffix.Text;

        int suffix_no = 0;
        while (System.IO.File.Exists(FileName + suffix + (suffix_no == 0 ? "" : suffix_no.ToString()) + ".jpg"))
        {
            suffix_no += 1;
        }
        canvas.Save(FileName + suffix + (suffix_no == 0 ? "" : suffix_no.ToString()) + ".jpg");
    }
}

github.com