ラベル 拡張メソッド の投稿を表示しています。 すべての投稿を表示
ラベル 拡張メソッド の投稿を表示しています。 すべての投稿を表示

2018年5月7日月曜日

輪切り画像の描画処理はこれで完成

拡張メソッド、ラムダ式のエントリーを挟んだ前回のエントリーで、輪切り画像の描画のための基本的な部品は出来上がりました。

前回までの進捗を整理すると、


  • データファイルをOpen File Dialogを使って選択
  • Visual BasicのCSVパーサーを使ってCSVのデータをArrayListに格納
  • プロット
  • 輪切りする場所を指定する為のUIを実装(X座標を指定するかY座標を指定するかを排他的に選択
  • マウスでクリックした場所の座標を拾う(X座標のみ)
  • 拾った座標に最も近いデータをArrayListから探してくる(X座標のみ)


というところまで、実装済みです。この後は、


  • 奥から描画していくようにデータを並び替え(隠面処理)
  • マウスでクリックした場所の座標を拾う(前回はX軸だけだったのでY軸も含めて)
  • 拾った座標に最も近い座標をArrayListから探してくる(X座標、Y座標ともに)
  • ArrayListから見つけてきた一番近い座標を含む全てのデータを取得
  • プロット

という機能を実装すれば、輪切り画像の描画処理は完成となります。

奥から描画していくようにデータを並び替え


立体的な物体を画面に描くことは、物体の形状を平面に投影するということになります。その際に気をつけなければいけないのが、見える場所は見えるように、隠れて見えない場所は見えないように描くこと。

人間が鉛筆で絵を描く場合は、隠れて見えない場所は描かないのでしょうが、コンピューターに描かせる場合は、隠れて見えない場所は描かない、という処理をしてあげる必要があります。これを隠面処理と言います。

隠面処理には色々な方法があるようですが、今回のプログラムでは、見ている場所から遠い点から描画して、後ろを遮るものが前にある場合は、後ろの点を重ね書きして隠してしまう方法を採用します。一番単純な方法だからです。

プログラムが読むデータファイルは、データの順番が都合の良いように並んでいるとは限らない、と仮定すると、描画する前にデータを画面奥から手前に向かう座標軸、今回の場合はZ軸に関して並べ替える必要があります。

並べ替えの手法は、「業務開発で使えるプログラム」様にて勉強させていただきました。

描画する点のデータは、X座標、Y座標、Z座標及び点における測定値の大きさ、という4つのデータがひとかたまりになっています。これらをExcelのソートのように、1つのデータ、Excelのワークシートに例えるなら、1つのカラムをキーにして並べ替えたいわけです。

これは、バラバラのArrayListのままではなかなか難しいので、データテーブルというデータ形式を使って表形式のデータにまとめてから取り扱うのがよさそうです。

            //
            // 後でデータの並べ替えが必要になるので、プロットデータをデータテーブルに格納
            //
            table.Columns.Add("x", Type.GetType("System.Int32"));
            table.Columns.Add("y", Type.GetType("System.Int32"));
            table.Columns.Add("z", Type.GetType("System.Int32"));
            table.Columns.Add("dt", Type.GetType("System.Int32"));

            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);

                table.Rows.Add(x, y, z, d);  //データ1行分追加
            }
            
            // データテーブルのソートをするために、Dataviewを使う
            DataView dv = new DataView(table);
            dv.Sort = "z";        // 昇順 -> Z座標の数値が大きい方が手前。小さい方が奥
            //dv.Sort = "z DESC"; // 降順 -> Z座業の数値が小さい方が手前、大きい方が奥
            table = dv.ToTable();

データテーブルを作った後は、DataViewというクラスを使って、簡単にデータテーブルの並べ替えをすることができます。DataViewにデータテーブルを設定して、並べ替えをして、並べ替えたデータを元のデータテーブルに書き戻すまで、DataView dv = new DataView(table);の行からたった3行で記述することができます。

マウスでクリックした場所の座標を拾う


前回、X軸に垂直な平面に関して輪切りを行う場合について、クリックした場所の座標を拾う処理を実装しました。今回は、Y軸に垂直な平面に関して輪切りを行う場合の処理も合わせて組み込みます。

作成したプログラムの中では、Form1の上にあるpictureboxに対する描画処理が終了したあと、ユーザーが輪切り画像描画のボタンを押すことにより輪切り画像描画モードに遷移します。

このモードに遷移すると、Clickイベント、MouseMoveイベント、MouseHoverイベントが有効となり、マウスカーソルがpictureboxの上を動くたびにマウスカーソルの座標が、クラス内のグローバル変数であるmouse_x、mouse_yの中に記録され、更新され続けます。

このmouse_x、mouse_yのデータを拾ってくることで、マウスでクリックした場所の座標を知ることが出来ます。

拾った座標に最も近い座標をArrayListから探してくる

輪切り画像の描画を行うメソッドであるdrawSlice()の冒頭で、button1が有効かbutton2が有効かを調べて、X軸に関して輪切りをするか、Y軸に関して輪切りをするかを切り分け、それぞれでX軸の場合はmouse_x、Y軸の場合はmouse_yの値を取り出し、これに最も近い値をArrayList x_axis_dt_int(X軸に関して輪切りにする場合) y_axis_dt_int(Y軸に関して輪切りにする場合)から見つけてきます。

下記のプログラムリストにおいて、

// マウスでポイントした座標に最も近いデータ上の座標を拾ってくる
nearest_point = x_axis_dt_int.Nearest(mouse_x);  
nearest_point = y_axis_dt_int.Nearest(mouse_y);

の部分です。上はX軸に関して、下はY軸に関して輪切りにする場合の座標取得です。
ただし、ここで気をつけたいのは、まだ描画処理が走っていない状態で、ArrayListから最も近い値を探してくる処理を走らせてはいけないことです。これを防ぐ為に、描画データを格納するArrayListにデータが入っているかどうかを、逆に言えばArrayListが空でないかどうかをテストする処理を入れています。これは拡張メソッドIsEmpty()として定義しています。

実はあえて拡張メソッドにする必要もないのですが、使うときの書きやすさで、空かどうかをbool値で返す拡張メソッドにしてみました。

ArrayListから見つけてきた一番近い座標を含む全てのデータを取得


拡張メソッドNearest()を使ってArrayListから探してきた数値(X座標もしくはY座標)を含む座標データを、座標データが格納されているArrayListから取得します。

座標データは、データファイルのCSVデータを読み込んだ後、座標変換を行い、一番奥にある座標から順に並ぶように並べ替えたものが、x_axis_dt_int、y_axis_dt_int、z_axis_dt_int、dt_dt_intに格納されています。

この座標データに対して、X軸に対して垂直な面で輪切りにするのであればx_axis_dt_intからx_axis_dt_int.Nearest()で探してきた数値と等しい座標データを全て、Y軸に対して垂直な面で輪切りにするのであればy_axis_dt_intからy_axis_dt_int.Nearest()で探してきた数値と等しい座標データを全て取得します。

プロット


記事ではデータの取得とプロットを別項目にしましたが、実際にはNearest()で探した数値と同じX座標、もしくはY座標を持ったデータかどうかをif分で振り分けて、目的の座標であれば、そのままプロットの色を決めてプロットしてしまうような処理にしました。

以下、上記説明をコーディングしたプログラムリストを以下に示します。pictureBox1_Click()は、pictureBox1の上でマウスをクリックしたときに発生するイベントのイベントハンドラー、drawSlice()は実際に輪切り画像を描画するメソッド、IsEmpty()はArrayListの中身が空かどうかをテストするための拡張メソッドとなります。ArrayListの中身が空だった場合は、何もしないで呼び出し元に戻ります。

なお、拡張メソッドIsEmpty()は、コガネブログ様で教えていただきました。ありがとうございます。

        /*
         * pictureboxの上でマウスをクリックしたときに発生するイベント
         * 何も描かれていない状態でスライス描画処理を呼び出すと、データが入っているリストに
         * 中身がある前提の処理に
         * 支障を来すし、何も描かれていない状態でスライス描画処理を呼び出すことは意味が
         * 無いことなので、何も描かれて
         * いないときにはスライス描画処理が行われないようにする必要がある。
         * その判断をここで実施。
         */
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            // データを入れるリストが単独で空であることはあり得ないので、
            // X軸もしくはY軸の座標データがあるかどうか一つだけのテストで判断
           if (!x_axis.IsEmpty())
            {
                drawSlice();  // データのリストが空でなければスライスを描く処理を呼び出す
            }
            else
            {
                MessageBox.Show("何も描かれていません");
            }
        }
        /*
         * スライス画像を作成する
         */
         private void drawSlice()
        {
            int nearest_point;
            Color col = new Color();

            Form3 frm3 = new Form3();
            frm3.Show();

            Bitmap img = new Bitmap(Constants.size_slicebox_x, Constants.size_slicebox_y);

            if (button1_on)
            {

               // マウスでポイントした座標に最も近いデータ上の座標を拾ってくる
               nearest_point = x_axis_dt_int.Nearest(mouse_x);  
                frm3.Text = "Slice by X-axis : " + nearest_point;
               // ArrayListの全ての要素数でループを回し、一番近い数値に関してだけ描画処理を動かす
                for (int i = 0; i < x_axis_dt.Count; ++i) 
                {
                    if (x_axis_dt_int[i] == nearest_point) 
                    {
                        // プロットデータの内容から描画色を決定
                        if (dt[i] != 0)   // データが0の時は描画しない
                        {
                            switch (dt[i])
                            {
                                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(z_axis[i], y_axis[i], col);
                    }
                }
                frm3.pictureBox1.Image = img;
            }

            //
            // Y軸でスライスする場合の処理
            //
            if(button2_on)
            {
                nearest_point = y_axis_dt_int.Nearest(mouse_y);
                frm3.Text = "Slice by Y-axis : " + nearest_point;

                for (int i = 0; i < y_axis_dt.Count; ++i)
                {
                    if (    y_axis_dt_int[i] == nearest_point)
                    {
                        // プロットデータの内容から描画色を決定
                        if (dt[i] != 0)   // データが0の時は描画しない
                        {
                            switch (dt[i])
                            {
                                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_axis[i], z_axis[i], col);
                    }
                }
                frm3.pictureBox1.Image = img;
            }

/*
 * 配列やリストが空かどうかを返す拡張メソッドisEmpty.cs
 * http://baba-s.hatenablog.com/entry/2015/07/17/100000
 */
using System.Collections.Generic;

public static class IListExtentions
{
    public static bool IsEmpty<T>( this IList<T> self )
    {
        return self.Count == 0;
    }
}

下に、プログラムを動作させた様子をキャプチャした動画を示します。輪切り画像がきちんと表示できればよかったのですが、色々な都合で綺麗に出ていないことをお許し下さい。


ソースファイルはここにzipファイルにして置きます。ソースのみですので、Visual Studioでビルドして動かす場合には、色々な設定が必要になると思います。それに関しては、誠に勝手ながら、各自にてお願いいたします。また、プログラムソースは学習用の使用に留めて下さいますようお願いいたします。

本プログラムもWindows10上のVisual Studio 2017 Communityエディションで開発、動作確認しています。動作環境として.Net Frameworkを使用していますので、Mac OS版のVisual Studio 2017では動かすことはできません。GUIを全て書き換える必要があると思います。

なお、プログラムソースの内容については無保証です。ご質問は可能な範囲でお受けいたしますが、回答にお時間を頂く場合もございますことをご了承下さい。

2018年4月17日火曜日

さらに立ち止まって、拡張メソッドの研究

前々回のエントリーで出て来た下記のリスト、この中で記述されているのが、前回のエントリーで研究したラムダ式と、今日やる拡張メソッドです。

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);
    }
}

拡張メソッドとは何か?という簡単な説明が、Qiitaのkuwapp様記事にありました。

メソッドを追加しようと思ったら、普通はソースを編集してメソッドを追加して再コンパイルするか、派生クラスを作るしかないのですが、この拡張メソッドはそれをせずにクラスの追加が出来るように見えるものです。

上記で引用させていただいた記事には具体的に記述方法の説明はありません。記述方法などに関する詳しい説明は、いつも勉強させていただいている++C++ ; //未確認飛行 C様の記事にあります。

静的クラスの中に静的メソッドを定義し、その中で第一引数にthis修飾語を付けた静的メソッドを定義する、ということのようです。

下記リストではこの拡張メソッドを、スライス画像を描くためのメソッドの中で呼び出していますが、それは下記のように記述しています。

        /*
         * スライス画像を作成する
         */
         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_axisというArrayListには、Nearestというメソッドは元々ありませんが、拡張メソッドを定義しているので、あたかもメソッドが拡張されたかのように記述できている、という訳です。

拡張メソッドについては、ここまでにします。

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は割愛します。