2018年4月3日火曜日

いよいよ輪切り画像の生成に取り組む

いよいよ、輪切り画像の生成に取り組みます。

輪切りする場所の座標を取得する


今まで何度かマウスカーソルの座標を取得し続ける、という動作を取り上げました。取得し続けるのはリアルタイム処理で難しそうですが、輪切りする場所の座標を取得する、という処理はそれ程難しくありません。pictureBoxの上でマウスボタンがクリックされたとき、そのクリックされた座標を取得すれば良いのです。

        /*
         * pictureboxの上でマウスをクリックしたときに発生するイベント
         */
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            // クリックされたときのクライアント座標をsaveして使う。
            // mouse_x/mouse_yはマウスイベントが入ると更新されてしまうので、それを防ぐ
            int x_slice = mouse_x, y_slice = mouse_y;

            MessageBox.Show("Mouse clicked X= " + x_slice + " : Y= " + y_slice);
            // データの中にある座標で、クリックした座用に最も近いものを得る(Nearest拡張メソッド)
            MessageBox.Show("Slice by " + x_axis_dt_int.Nearest(x_slice));
           // drawSlice();

        }

この中で、mouse_xとmouse_yは、MouseMoveイベントが発生するたびにマウスカーソルの座標が代入されるグローバル変数です。

このメソッドをpictureBox_Clickイベントへのイベントハンドラとして登録すると、クリックされた時点でのマウスの座標をx_slice、y_sliceに取り込むことができます。

この時点では、輪切り画像を実際に描画するメソッドをまだ作成していなかったので、取得したマウスカーソルの座標をMessageBoxに表示するようにしました。

実はこれ、その後の学習で、イベントハンドラの引数のeの中に、イベントが発生したときのマウスカーソルの座標の情報が含まれることがわかりました。それを参照すれば、もっとシンプルで外の影響を受けないコードを書くことが出来ます。

今回のケースでは、学習したときそのままのコードを引用すると言うことで、最初に書いたコードをそのまま載せました。

最も近い数値を拾ってくる


マウスで輪切りするポイントの座標を拾うとき、拾った座標の数値と全く同じ数値がデータの中にあるとは限りません。データが全て整数でできていて、マウスポインターの座標も整数であれば、全く同じ数値がデータの中に存在する可能性はありますが、それは特殊なケースと考えておくのが後々無難です。

プログラムの詳細仕様を考えていて、この問題に突き当たりました。これを解決する為には、指定した数値に最も近い数値をデータの中から探す、という機能が必要になります。

色々と調べていたら、発見しました。コガネブログ様です。

下記が、最も近い値を拾ってくる、ということを実現するコードです。この中には、拡張メソッド、という技術と、ラムダ式という技術が入っています。


using System;
using System.Collections.Generic;
using System.Linq;

public static class IEnumerableExtensions
{
    public static int Nearest(this IEnumerable < int > self, int target)
    {
        var min = self.Min(c => Math.Abs(c - target));
        return self.First(c => Math.Abs(c - target) == min);
    }
}

呼び出す側では、こう言う書き方をします。

         private void drawSlice()
        {
            Bitmap img = new Bitmap(Constants.size_slicebox_x, Constants.size_slicebox_y);
            MessageBox.Show("Slice by "+x_axis.Nearest(mouse_x));
        }

呼び出す側では、まだスライス画像を実際に描くコードの実装ができていなかったので、指定したx座標に最も近いx座標をx_axisオブジェクトから探し出して、メッセージボックスに表示するようにしています。


拡張メソッドは、こちら(いつも助けていただいている「++C++ //未確認飛行 C様」に説明がありますが、何やら難しくてよくわかりません。

ラムダ式についても、同じ所に説明があります。これも難しいです。時間をかけて理解を深める必要があります。

もう一つこう言う説明もあります。Qiitaの「今更ですが、ラムダ式」という記事です。

特にラムダ式は記述の仕方が通常のC#のコードとあまりにも違って、理解が難しいので、今後研究して理解することにして、今はこういう物を入れておくと意図通りの動きをする、というレベルにとどめておきたいと思います。

今回の記事では、
  • 測定データからプロットを描画
  • 描画したプロットをクリックして輪切り画像を作成したい場所の座標を取得
  • 取得した座標に最も近いデータ(今回はX軸のデータのみ)をリストから拾う
  • 取得した座標をメッセージボックスに表示した後、輪切りする座標をメッセージボックスに表示する
ということを行っています。

前回の記事に対して、輪切り画像を作成したい場所の座標の取得と、取得した座標に最も近いデータをリストから拾う、という大きな2つの機能を追加しています。

プログラムリストを以下に示します。Form1のデザインは前回の記事と全く同じで、イベントの追加もないので、Form1.Designer.csは割愛します。


ファイル Program.cs
/*
Program.cs
 */

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

namespace EventOnOff
{
    /*
     * 定数をクラスとして定義する
     */
    static class Constants
    {
        public const string FileName = "";
        // public const string FileName = "c:\\users\\ぐすたふ\\検討用テストデータ.csv";
        public const int size_pic_box_x = 400;
        public const int size_pic_box_y = 400;
        public const int offset_x = (Constants.size_pic_box_x) / 2;
        public const int offset_y = Constants.size_pic_box_y - 100;
        public const int offset_z = 0;

        public const int size_slicebox_x = 400;
        public const int size_slicebox_y = 400;

    }


    static class Program
    {
        /// 
        /// アプリケーションのメイン エントリ ポイントです。
        /// 
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

ファイルForm1.cs
/*
 * スライス画像作成の試み
 * マウスの座標を取得して、それに対応する配列要素を取得
 * イベントの有効・無効の切り替え処理を関数にまとめた
 * 
 * スライス座標を求めた後、マウス座標に最も近いデータ中の座標値を求めるために、拡張メソッドNearest
 * を使って、実際にスライスする座標(データの中にある座標値)を求める処理を入れた(まずX座標について)
 * 
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.VisualBasic.FileIO;

namespace EventOnOff
{
    public partial class Form1 : Form
    {
        /*
         * 各種変数定義
         * なお、定数はmain部で定義
         */
        public static int mouse_x = 0, mouse_y = 0;       // マウスの座標

        List<double> x_axis_dt = new List<double>(); // X座標の生データの可変長配列
        List<double> y_axis_dt = new List<double>(); // Y座業の生データの可変長配列
        List<double> z_axis_dt = new List<double>(); // Z座標の生データの可変長配列
        List<double> dt_dt = new List<double>();     // プロットデータの生データの可変長配列

        List<int> x_axis_dt_int = new List<int>(); // 座標の生データの整数変換したもの
        List<int> y_axis_dt_int = new List<int>();
        List<int> z_axis_dt_int = new List<int>();
        List<int> dt_dt_int = new List<int>();

        List<int> x_axis = new List<int>(); // X座標の可変長配列(整数変換後
        List<int> y_axis = new List<int>(); // Y座標の可変長配列
        List<int> z_axis = new List<int>(); // Z座標の可変長配列
        List<int> dt     = new List<int>();     // プロットデータ

        public static bool button1_on = false;             // ボタン1が押されているかどうか
        public static bool button2_on = false;             // ボタン2が押されているかどうか
        public static bool event_by_inner1 = false;         // ボタンの状態変化が捜査によるものか内部からの書き換えか
        //public static bool event_by_inner2 = false;         // ボタンの状態変化が操作によるものか内部からの書き換えか
        public string fn = Constants.FileName;             // データファイル名入れ
           
        public Form1()
        {
            InitializeComponent();

            this.MaximumSize = this.Size;
            this.MinimumSize = this.Size;
        }

        /*
         * Form1がロードされたときの処理(ロードされるときに発生するイベント)
         */
        private void Form1_Load(object sender, EventArgs e)
        {
            // マウス座標を取り込むモードかどうかをチェックして、その旨表示
            // 本来不要な処理だが、Form1がロードされるのがアプリケーション開始の時だけか
            // どうかあやふやなので入れておく
            if (button1_on)
            {
                this.checkBox1.Text = "Push Button to stop getting mouse position X-axis";
            }
            else
            {
                this.checkBox1.Text = "Push Button to start getting mouse position X-axis";
            }

            if( button2_on )
            {
                this.checkBox2.Text = "Push Button to stop getting mouse position Y-axis";
            }
            else
            {
                this.checkBox2.Text = "Push Button to start getting mouse position Y-axis"
;            }

            // データファイルのファイル名初期値の設定
            textBox1.Text = fn;

            // checkBox1にフォーカスを当てる
            this.ActiveControl = this.checkBox1;
        }

        /*
         * pictureBox1の上にマウスカーソルが乗った時に発生するイベント
         */
        private void pictureBox1_MouseHover(object sender, EventArgs e)
        {
            getMousePosition();
        }

        /*
         * pictureBox1の上のマウスが動いたときに発生するイベント
         */
        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            getMousePosition();
        }

        /*
         * Button1(終了ボタン)がクリックされたときに発生するイベント
         */
        private void button1_MouseClick(object sender, MouseEventArgs e)
        {
            Application.Exit();
            //Environment.Exit((int)0); // アプリケーション終了
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Application.Exit();
            //Environment.Exit((int)0); // アプリケーション終了
        }

        /*
         * checkbox1(X軸でスライスの指示)の状態が変化したときに発生するイベント
         * 
         * X軸でのスライス、Y軸でのスライスは、それぞれ排他的に動かなければいけないので、X軸実行中に
         * Y軸のボタンを押したらX軸を止めてY軸を動かすようにする。
         * そのためにボタンの状態をプログラムから変更すると、状態変化によるイベントが発生する。
         * ボタンの状態変化によるイベントの中で、自分以外のボタンの状態をクリアすると、
         * そのことによってまたイベントが発生し、自分の状態をクリアしてしまうことになるので、
         * ボタン操作によって発生したイベントか、プログラムによる状態変更のよって発生したイベントか
         * 区別するためのフラグを設けた。それがevent_by_inner1。bool型の変数。
         * プログラムによる状態変化のときはtrueとなる前提。
         *
         */
        private void checkBox1_CheckedChanged(object sender, EventArgs e)
        {
            if (checkBox2.Checked && !event_by_inner1)
            {
                event_by_inner1 = true;
                checkBox2.Checked = false;
            }

            event_by_inner1 = false;

            if (checkBox1.Checked)
            {
                checkBox1.Text = "Push Button to stop getting mouse position X-axis";
                button1_on = true;
                mouse_x = mouse_y = 0; // マウス座標データ初期化
                // マウスイベント有効
                ctrlEvent(true);
            }
            else
            {
                checkBox1.Text = "Push Button to start getting mouse position X-axis";
                button1_on = false;
                // マウスイベント削除
                ctrlEvent(false);
            }
        }

        /*
         * checkbox2(Y軸でスライスの指示)の状態が変化したときに発生するイベント
         * 
         * X軸でのスライス、Y軸でのスライスは、それぞれ排他的に動かなければいけないので、X軸実行中に
         * Y軸のボタンを押したらX軸を止めてY軸を動かすようにする。
         * そのためにボタンの状態をプログラムから変更すると、状態変化によるイベントが発生する。
         * ボタンの状態変化によるイベントの中で、自分以外のボタンの状態をクリアすると、
         * そのことによってまたイベントが発生し、自分の状態をクリアしてしまうことになるので、
         * ボタン操作によって発生したイベントか、プログラムによる状態変更のよって発生したイベントか
         * 区別するためのフラグを設けた。それがevent_by_inner1。bool型の変数。
         * プログラムによる状態変化の時はtureとなる前提
         */
        private void checkBox2_CheckedChanged(object sender, EventArgs e)
        {

            if (checkBox1.Checked && !event_by_inner1)
            {
                event_by_inner1 = true;
                checkBox1.Checked = false;
            }

            event_by_inner1 = false;

            if (checkBox2.Checked)
            {
                checkBox2.Text = "Push Button to stop getting mouse position Y-axis";
                button2_on = true;
                mouse_x = mouse_y = 0; // マウス座標データ初期化
                // マウスイベント有効
                ctrlEvent(true);
            }
            else
            {
                checkBox2.Text = "Push Button to start getting mouse position Y-axis";
                button2_on = false;
                // マウスイベント削除
                ctrlEvent(false);

            }
        }

        /*
         * ファイル名操作のボタンを押したときに発生するイベント
         * 
         * textboxにある文字列をとりあえずのファイル名としたうえで、OpenFileDialogを起動
         * そのうえで、OpenFileDialogで得たファイル名を変数fnに入れると共にtextboxにも入れる
         */
        private void button2_Click(object sender, EventArgs e)
        {
            fn = textBox1.Text;

            // OpenFileDialogクラスのインスタンスを作成
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.FileName = textBox1.Text;
            ofd.Filter = "CSVファイル(*.csv)|*.csv|テキストファイル(*.txt)|*.txt|全てのファイル(*.*)|*.*";
            ofd.FilterIndex = 0;
            ofd.Title = "開くファイルを選択してください";
            ofd.RestoreDirectory = true;
            ofd.CheckFileExists = true;
            ofd.CheckPathExists = true;

            if (ofd.ShowDialog() == DialogResult.OK)
            {
                fn = ofd.FileName;
            }
            textBox1.Text = fn;  //ダイアログで得たファイル名を一旦textboxに入れる
        }

        /*
         * 描画ボタンが押されたときに発生するイベント
         */
        private void button3_Click(object sender, EventArgs e)
        {
            Plot_Data();
        }

        /*
         * pictureboxの上でマウスをクリックしたときに発生するイベント
         */
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            // クリックされたときのクライアント座標をsaveして使う。
            // mouse_x/mouse_yはマウスイベントが入ると更新されてしまうので、それを防ぐ
            int x_slice = mouse_x, y_slice = mouse_y;

            MessageBox.Show("Mouse clicked X= " + x_slice + " : Y= " + y_slice);
            // データの中にある座標で、クリックした座用に最も近いものを得る(Nearest拡張メソッド)
            MessageBox.Show("Slice by " + x_axis_dt_int.Nearest(x_slice));
           // drawSlice();

        }

        /*
         * データをプロットする
         */
        private void Plot_Data()
        {
            Color col = new Color();
            Bitmap img = new Bitmap(Constants.size_pic_box_x, Constants.size_pic_box_y);

            fn = textBox1.Text;  // プロット指示が出た時点のtextboxの値をファイル名とする

            if (!System.IO.File.Exists(fn))
            {
                MessageBox.Show("ファイルが見つかりません : " + fn);
                return;
            }

            try
            {
                using (TextFieldParser parser =
                    new TextFieldParser(fn, System.Text.Encoding.GetEncoding("Shift_JIS")))
                {
                    parser.TextFieldType = FieldType.Delimited;
                    parser.SetDelimiters(","); // 区切り文字はコンマ
                    parser.CommentTokens = new string[1] { "#" };
                    int line = 0;

                    while (!parser.EndOfData)
                    {
                        //++line;
                        //col = 0;

                        string[] row = parser.ReadFields(); // 1行読んで、要素に分解、配列rowに格納

                        x_axis_dt.Add(Convert.ToDouble(row[0]));  // ArrayListにX軸座標を追加
                        y_axis_dt.Add(Convert.ToDouble(row[1]));  // ArrayListにY座標を追加
                        z_axis_dt.Add(Convert.ToDouble(row[2]));  // ArrayListにZ座標を追加
                        dt_dt.Add(Convert.ToDouble(row[3]));      // ArrayListにデータ本体を追加

                        x_axis_dt_int.Add((int)x_axis_dt[line]);
                        y_axis_dt_int.Add((int)y_axis_dt[line]);
                        z_axis_dt_int.Add((int)z_axis_dt[line]);
                        dt_dt_int.Add((int)dt_dt[line]);

                        //Console.WriteLine("org-data : " + line + " : " + x_axis_dt[line] + " : " + y_axis_dt[line] + " : " + z_axis_dt[line] + " : " + dt_dt[line]);
                        ++line;
                    }
                }
            }
            catch (Exception e)
            {
                MessageBox.Show(e.Message + "\n終了します");
                Application.Exit();
            }

            for (int i = 0; i < x_axis_dt.Count; ++i)
            {
                //
                // 実座標をウィンドウ座標に変換するためにオフセット加算
                //
                int x = (int)x_axis_dt[i] + Constants.offset_x; 
                int y =  -1 * ((int)y_axis_dt[i]) + Constants.offset_y;
                int z = (int)z_axis_dt[i] + Constants.offset_z;
                int d = (int)dt_dt[i];

                x_axis.Add(x);
                y_axis.Add(y);
                z_axis.Add(z);
                dt.Add(d);
                Console.WriteLine("int-data : " + x_axis[i] + " : " + y_axis[i] + " : " + z_axis[i] + " : " + dt[i]);
                // プロットデータの内容から描画色を決定
                switch (d)
                {
                    case 0:
                        col = Color.Black;
                        break;
                    case 1:
                        col = Color.Blue;
                        break;
                    case 2:
                        col = Color.Red;
                        break;
                    case 3:
                        col = Color.Purple;
                        break;
                    case 4:
                        col = Color.Green;
                        break;
                    case 5:
                        col = Color.Yellow;
                        break;
                    case 6:
                        col = Color.Violet;
                        break;
                    case 7:
                        col = Color.White;
                        break;
                    default:
                        col = Color.Magenta;
                        break;
                }

                img.SetPixel(x, y, col);
            }

            pictureBox1.Image = img;

            MessageBox.Show("Plot Completed.");
        }

        /*
         * マウスの画面座標をcp.X、cp.Yから拾って、pictureBoxのクライアント座標
         * に変換して、ウィンドウの上のテキストエリアに表示
         */
        private void getMousePosition()
        {

            System.Drawing.Point cp = pictureBox1.PointToClient(Cursor.Position);

            mouse_x = cp.X - Constants.offset_x;
            mouse_y = -1 * cp.Y + Constants.offset_y;

            if (button1_on)
            {
                mouse_y = 0;
            }
            else
            {
                mouse_x = 0;
            }
            label1.Text = "x-size=" + pictureBox1.Size.Width;
            this.Text = "x=" + mouse_x + ":y=" + mouse_y + "(x=" + cp.X + ":y=" + cp.Y + ")";

        }

        /*
         * PictureBox1のMouseMove、MouseHover、Clickイベントをまとめて登録したり削除したりする
         * 引数:bool型
         * true : イベント登録
         * false : イベント削除
         */
        private void ctrlEvent(bool flag)
        {
            if (flag == true)
            {
                pictureBox1.MouseHover += new System.EventHandler(pictureBox1_MouseHover);
                pictureBox1.MouseMove += new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
                pictureBox1.Click += new System.EventHandler(pictureBox1_Click);
            }
            else
            {
                pictureBox1.MouseHover -= new System.EventHandler(pictureBox1_MouseHover);
                pictureBox1.MouseMove -= new System.Windows.Forms.MouseEventHandler(pictureBox1_MouseMove);
                pictureBox1.Click -= new System.EventHandler(pictureBox1_Click);
            }
        }

        /*
         * スライス画像を作成する
         */
         private void drawSlice()
        {
            Bitmap img = new Bitmap(Constants.size_slicebox_x, Constants.size_slicebox_y);
            MessageBox.Show("Slice by "+x_axis.Nearest(mouse_x));  //描画は未実装、座標をメッセージボックスに
        }
    }
}

ファイルNearest.cs

using System;
using System.Collections.Generic;
using System.Linq;

public static class IEnumerableExtensions
{
    public static int Nearest(this IEnumerable<int> self, int target)
    {
        var min = self.Min(c => Math.Abs(c - target));
        return self.First(c => Math.Abs(c - target) == min);
    }
}
今気が付きましたが、Nearest.csで定義してるクラスは、Mainと同じ名前空間に置かなくてもいいのですね。

0 件のコメント:

コメントを投稿

コメントを頂ければ幸いです。