前回までの進捗を整理すると、
- データファイルを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()は、コガネブログ様で教えていただきました。ありがとうございます。
座標データは、データファイルの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を全て書き換える必要があると思います。
なお、プログラムソースの内容については無保証です。ご質問は可能な範囲でお受けいたしますが、回答にお時間を頂く場合もございますことをご了承下さい。