Processingでリアルタイムでグラフを作成する方法

Processing

みなさん,こんにちは.
おかしょです.

電子工作で作ったロボットの状態をセンサーで取得しても,表示しなければどのような状態なのかわかりません.

Processingというフリーのソフトはデータを使ってアニメーションを作成することができます.

数値を表示することもできますが,数値をただ眺めるだけではイメージがしずらいことがあります.
できれば,グラフにすると変化の過程などがわかりやすくなります.

そこで,この記事ではリアルタイムで取得したデータを使ってグラフにする方法を解説します.
この記事では例として,横軸に時間をとった時系列グラフを示しますが,GPSなどのデータから位置のマッピングをすることも可能になります.

この記事を読むと以下のようなことがわかる・できるようになります.

  • Processingでグラフを作る方法
  • 時系列グラフを作る方法

 

この記事を読む前に

この記事ではグラフを作る方法を解説します.

そのためにはArduinoからProcessingにデータを送る必要があります.

データを送る方法を知らない方は,以下の記事で解説しているので先に読んでおくことをおすすめします.

 

グラフ作成プログラム

まずはグラフ作成をするプログラムはどのような処理をしていくのか,その順序を以下に示します.

  1. グラフ描画範囲の作成
  2. メモリ線の作成
  3. メモリに数値を記入
  4. 縦軸と横軸の名前を記入
  5. データをグラフ化する

大まかにはこのような流れで,プログラムしていきます.

 

グラフ描画範囲の作成

ここでは,グラフを描く範囲を設定します.

今回描くグラフは時系列グラフなので,グラフ描画範囲は長方形とします.
Processingで長方形を描くのは非常に簡単で,rectというコマンドを使います.

実際に描画範囲を決定するプログラムは以下のようになります.

  noFill();  // 塗りつぶしなし
  stroke(100);  // 枠線の色
  strokeWeight(3);  // 枠線の幅
  translate(positionX, positionY);  // 枠の左上の点を指定
  rect(0, 0, Width, Height);  // 枠の描画

まず,1行目で塗りつぶしをしないようにします.

2行目(stroke)で,描画範囲の枠線の色を指定します.
ここでは灰色にしています.

3行目(strokeWeight)で枠線の幅を決定しています.

4行目(translate)ではグラフ描画範囲の位置を決定しやすいように,座標系の原点を移動させています.
translateの引数のpositionX,positionYにグラフ描画範囲の左上隅の座標を入力します.

最後の行(rect)で,グラフ描画範囲である長方形を描きます.
rectの引数でwidthとheightには描画範囲の大きさを入力します.

次は,この長方形にメモリを刻んでいきます.

 

メモリ線の作成

メモリの区切り方は,データを表示する範囲や描画範囲の大きさなどによって,刻み幅などが異なります.
ここでは,縦軸と横軸を6個に分けます.

メモリ線を引くプログラムは以下のようになります.

  strokeWeight(1);  // メモリ線の幅
  for (int i=1; i<6; i++)  // メモリ線の描画
  {
    line(0, Height/6*i, Width, Height/6*i);  // 縦軸
    line(Width/6*i, 0, Width/6*i, Height);  // 横軸
  }

1行目(strokeWeight)で,メモリ線の幅を決定します.
先程の描画範囲の枠線よりも細くしました.

次の行からfor文を使って,連続してメモリ線を描くようにしています.
メモリ線はlineコマンドを使用しています.

各軸を6個に分けるため,線は5本必要です.
そのため,for文の制御文は(int i=1; i<6; i++)として,iが1から5まで変化するようにしています.

lineコマンドは始点の座標と終点の座標を引数に入力することで線が引けます.

そのため,縦軸のメモリ線の場合,x座標は始点を0,終点を描画範囲の幅であるwidthとしています.
y座標は始点と終点ともに同じ値とする必要があり,描画範囲の高さを6等分するので6で割って1から5までの数字をかけた値を入力します.

横軸の場合は反対にx座標を変化させ,y座標は固定した値を引数に入力します.

このようにすることで,以下のようになります.

 

メモリに数値を記入

次に,メモリに数値を記入していきます.

以下に,プログラムを示します.

  stroke(0);
  fill(100);  // メモリ値の文字色
  // 縦軸のメモリ値
  textSize(Height*0.06);  // 文字の大きさ
  textAlign(RIGHT);  // 文字の座標指定位置
  int j = -90;  // 縦軸の目盛りの最低値
  for (int i=6; i>=0;i--)  // メモリ戦の描画
  {
    text(j, -width/3*0.02, Height/6*i);  // 文字の描画
    j = j + 30;  // 縦軸の目盛りの更新
  }

上のプログラムを使用することで,縦軸の目盛りをを記入することができます.
横軸に関しても同様のプログラムを使えばかけるので参考にしてみてください.

今回は角度の時系列グラフを描くことを想定しているので,想定する角度の範囲は-90度から90度としています.

なので,記入するメモリの値もその範囲内になります.
また,先程メモリ線を6等分で書いたので,角度で言うと30度ずつになります.

まず,プログラムの1行目(stroke)で文字の枠線の色を指定します.
今回は背景を黒にしているので,枠線の色も黒にしました.
このように枠線をなくしたい場合は,noStroke()というコマンドを使っても良いです.

2行目(fill)で文字の色を指定します.
描画範囲の枠線の色と同じ色にしました.

4行目(textSize)で文字の大きさを設定しています.
描画範囲の高さの6%ぐらいの大きさにしたらバランスが取れたので,このように設定しましたが,好みで変更してかまいません.

5行目(textAlign)で文字を右詰にしています.

その後のfor文ではメモリ線の描画の時と同様に,下から順番にメモリの値を記入しています.
このときに使用したtextというコマンドには,書きたい文字,座標の順で引数に入力しています.

横軸も同様のプログラムを使うことによって,グラフは以下のようになります.

なお,横軸は最新のデータから30秒前までのデータをグラフ化するため,上のように描いた.
また,メモリ値を書く座標も全体のバランスを見て調整したので,textコマンドの座標は好みで調整してください.

 

縦軸と横軸の名前を記入

ここまでのプログラムで,かなりグラフらしくなってきました.

しかし,このままでは何のグラフだかわかりません.
そこで,縦軸と横軸にラベル名を記入します.

横軸のラベル名は先ほどのtextコマンドを使って,適切な座標を指定すれば簡単に書けるので,説明は割愛します.

問題は縦軸のラベル名です.
縦軸のラベル名をこれまで通りに書くと,
文字が横書きなので,グラフが間延びしてしまいます.

文字を90度回転させて描いた方が,見栄えが良くなります.
そこで,以下のようなプログラムで縦軸のラベル名を記入します.

  pushMatrix();  // 座標系の保存
  rotate(radians(-90));  // 座標軸を90度回転
  textAlign(CENTER);  // 文字の座標指定位置
  text(name, -Height/2, -width/3*0.1);  // 縦軸ラベルの描画
  popMatrix();  // 保存した座標系の出力

このプログラムでは,文字を書くために座標を90度回転させるのですが
ラベルの記入を終えた後は,混乱を防ぐために座標軸は元に戻しておきたいです.

そこで,1行目のpushMatrix()を使って座標系を保存します.

その後の2行目(rotate)で座標系を90度回転させます.

3行目(textAlign)で,文字を中央揃えにして
4行目(text)でラベルを記入します.

ラベルの記入を終えたら,popMatrix()を使って保存した座標系に戻します.

 

データをグラフ化する

さて,ここからが本題です.
最後にデータを作成した長方形の中にグラフとして表示します.

今回はデータを単純に直線で結んで折れ線グラフを作成します.

以下が,折れ線グラフを作成するプログラムになります.

  pushMatrix();
  translate(0, Height/2, 0);  // 座標の原点を縦軸の中心に移動
  PlotData[299] = data;  // データの更新
  for(int i=0; i<300-1; i++)  // plotするデータ処理
  {
    PlotData[i] = PlotData[i+1];
  }
  for(int i=0; i<300-1; i++)
  {
    stroke(255, 0, 0);  // 線の色
    line(Width/300*i, Height/6*PlotData[i]/30, Width/300*(i+1), Height/6*PlotData[i+1]/30);
  }
  popMatrix();  

折れ線グラフを描く際にも,書きやすいように座標軸を移動させます.
グラフを描く前と後で座標軸は変わらないようにするために,最初にpushMatrix()で座標軸の保存をしておきます.

そして,2行目(translate)で座標の原点を縦軸の中心,つまり0度のところに移動させます.

3行目でデータの更新を行います.
このときのplotDataというのは,最初にglobal変数としてint型の配列で定義しておきます.
plotDataにはグラフ化するデータをすべて保存しておきます.
つまり,現在のデータから30秒間のデータを保存します.
今回は10Hzでデータを取得できると想定して,配列の大きさは300としました.

int[] PlotData = new int[300];

上のプログラムをsetup関数よりも上に定義します.

ここで,データがまだ来ていない,もしくはデータが来始めたばかりで30秒もたっていない時を考えます.
データがきていない部分のグラフは0度のまま変化してほしくないので,すべての要素の初期値を0とします.
そのために以下のプログラムをsetup関数内に書き込みます.

  for(int i=0; i<300; i++)  // plotするデータの初期化
  {
    PlotData[i] = 0;
  }

さて,先程のグラフかをするプログラムに戻ります.
plotDataにはグラフ化するデータをすべて保存しておくため,4行目から7行目のfor文で,データを一つずつずらしていきます.
このようにすることで,古いデータは消去されグラフ化されることはなくなります.

次の8行目から12行目のfor文でplotDataに保存したデータを折れ線グラフとして描きます.
まず,最初のstrokeで線の色を設定します.
次にlineコマンドを使って配列の要素を順番に使用して,折れ線グラフを作成します.

最後にpopMatrix()で保存した座標系に戻して終了となります.

 

まとめ

最後にこれまでのプログラムをまとめたものを以下に示します.

int[] PlotData = new int[300];

void setup() {
  size(displayWidth,displayHeight);  // windowのサイズを画面いっぱいにする
  smooth();
  background(0);  // 背景を黒にする
  frameRate(10);  // frame rateを5fpsに設定
  for(int i=0; i<300; i++)  // plotするデータの初期化
  {
    PlotData[i] = 0;
  }
}

void draw() {
  background(0);
  TimeHistory(-width/2, -height/2+height/2/5, 0, "φ[deg]", data);  // 時間履歴の表示
}

void TimeHistory(int positionX, int positionY, String name, int data)
{
  // 引数説明 //
  // positionX, positionYはグラフを表示する枠の左上の点
  // nameはグラフの縦軸のラベル名
  // dataはリアルタイムのデータ
  // 枠線の描画
  float Width = width/3*0.85;  // 枠の幅
  float Height = height/2*0.6;  // 枠の高さ
  pushMatrix();  // 座標系の保存
  noFill();  // 塗りつぶしなし
  stroke(100);  // 枠線の色
  strokeWeight(3);  // 枠線の幅
  translate(positionX, positionY);  // 枠の左上の点を指定
  rect(0, 0, Width, Height);  // 枠の描画
  // メモリ線の描画
  strokeWeight(1);  // メモリ線の幅
  for (int i=1; i<6; i++)  // メモリ線の描画
  {
    line(0, Height/6*i, Width, Height/6*i);  // 縦軸
    line(Width/6*i, 0, Width/6*i, Height);  // 横軸
  }
  // メモリ値の描画
  stroke(0);
  fill(100);  // メモリ値の文字色
  // 縦軸のメモリ値
  textSize(Height*0.06);  // 文字の大きさ
  textAlign(RIGHT);  // 文字の座標指定位置
  int j = -90;  // 縦軸の目盛りの最低値
  for (int i=6; i>=0;i--)  // メモリ戦の描画
  {
    text(j, -width/3*0.02, Height/6*i);  // 文字の描画
    j = j + 30;  // 縦軸の目盛りの更新
  }
  pushMatrix();  // 座標系の保存
  rotate(radians(-90));  // 座標軸を90度回転
  textAlign(CENTER);  // 文字の座標指定位置
  text(name, -Height/2, -width/3*0.1);  // 縦軸ラベルの描画
  popMatrix();  // 保存した座標系の出力
  // 横軸のメモリ値
  textAlign(CENTER);  // 文字の座標指定位置
  text("now", Width, Height*1.08);  // 文字の描画
  j = -30;  // 横軸の目盛りの最低値
  for(int i=0; i<=5; i++)
  {
    text(j, Width/6*i, Height*1.08);  // 文字の描画
    j = j + 5;  // 横軸の目盛りの更新
  }
  text("time [s]", Width/2, Height*1.2);  // 横軸ラベルの描画
  // グラフの描画
  pushMatrix();
  translate(0, Height/2, 0);  // 座標の原点を縦軸の中心に移動
  PlotData[299] = data;  // データの更新
  for(int i=0; i<300-1; i++)  // plotするデータ処理
  {
    PlotData[i] = PlotData[i+1];
  }
  for(int i=0; i<300-1; i++)
  {
    stroke(255, 0, 0);  // 線の色
    line(Width/300*i, Height/6*PlotData[i]/30, Width/300*(i+1), Height/6*PlotData[i+1]/30);
  }
  popMatrix();  
  stroke(0);  // 線の色を戻す
  popMatrix();  // 保存した座標系の出力
}

このプログラムを実行しても動作しないので注意してください.
これまでの説明でもあったように,ご自分の好みで調整する部分が少しあります.

上記プログラムでは,私が設定した値が記入されています.

さらに,dataの取得を行うプログラムは書かれていないので,この記事を参考にして組み合わせてください.

また,このプログラムでは一種類のデータをグラフ化することしかしていませんが,複数のデータを重ねることもできるので,このプログラムを参考にプログラミングしてみてください.

 

続けて読む

この記事で解説した時系列グラフを使って,姿勢角を表示できるようにしました.

以下の記事でその方法を解説しているので参考にしてください.

Twitterでは記事の更新情報や活動の進捗などをつぶやいているので気が向いたらフォローしてください.

それでは,最後まで読んでいただきありがとうございました.

コメント

タイトルとURLをコピーしました