今日の内容
Pachubeを使ってみる
世界各地のpachubeの情報をモニターしてみる
Processingで、Pachubeの情報をリアルタイムに取得して、視覚化する
Arduinoで取得したセンサーの情報を、Pachubeに書き出し
センサーの情報をシェアしてみる
Pachubeとは?
http://www.pachube.com/
インターネットを介してリアルタイムにセンサーやからの情報を共有するプロジェクト
世界中の環境、センサーからのリアルタイムの情報をモニター
自作のデバイスを接続、世界に向けて情報を発信
ユーザ登録することで、APIを利用して様々なアプリケーションからPachubeの情報を送受信可能となる
Processing, openFrameworks, Java, Ruby, PHP, iPhone ..etc.a
ユーザ登録
PachubeのAPIを利用するために、まずはPachubeにユーザ登録する
もちろん無料
Sign upの登録フォームに必要情報を記入して送信 https://www.pachube.com/signup
登録したemailアドレスに、メールが返送される
登録完了メールの中の「API Key」というアルファベットと数値の文字列が開発の際に必要となるので、控えておく
Webブラウザで情報を取得
[code language=”html”]
<eeml xmlns="http://www.eeml.org/xsd/005" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="5" xsi:schemaLocation="http://www.eeml.org/xsd/005 http://www.eeml.org/xsd/005/005.xsd">
<environment updated="2010-07-11T03:28:16Z" id="2263" creator="http://www.haque.co.uk">
<title>iBioart</title>
<feed>http://www.pachube.com/api/feeds/2263.xml</feed>
<status>live</status>
<description>A Plant connected with Pachube.</description>
<website>http://dp.idd.tamabi.ac.jp/bioart/</website>
<email>kubotaa@tamabi.ac.jp</email>
<location domain="physical" exposure="indoor" disposition="fixed">
<name>Tama Art University, Tokyo</name>
<lat>35.6105400074836</lat>
<lon>139.351229667664</lon>
</location>
<data id="0">
<tag>temperature</tag>
<value minValue="1.0" maxValue="168.0">83</value>
</data>
<data id="1">
<tag>humidity</tag>
<value minValue="63.0" maxValue="306.0">145</value>
</data>
<data id="2">
<tag>brightness</tag>
<value minValue="0.0" maxValue="942.0">846</value>
</data>
<data id="3">
<tag>water</tag>
<value minValue="0.0" maxValue="842.0">209</value>
</data>
<data id="4">
<tag>plant1</tag>
<value minValue="0.0" maxValue="928.0">353</value>
</data>
<data id="5">
<tag>plant2</tag>
<value minValue="0.0" maxValue="1023.0">401</value>
</data>
<data id="6">
<tag>plantdiff</tag>
<value minValue="-142.0" maxValue="220.0">141</value>
</data>
<data id="7">
<tag>human</tag>
<value minValue="0.0" maxValue="1.0">0</value>
</data>
</environment>
</eeml>
[/code]
このEEML形式のXMLファイルを様々なアプリケーションで解析することで、オリジナルのPachubeモニタを作成できる
Extended Environments Markup Language (EEML)
Pachubeでは、センサーや環境の情報を記述XML形式のフォーマットとして、Extended Environments Markup Language (EEML)というものを策定している
EEMLに関する詳細な情報は、http://www.eeml.org/ を参照
ProcessingでPachubeの情報を視覚化
EEML for Processingをインストール
EEML for Processingのページから、Processing用のライブラリをダウンロード(http://www.eeml.org/library/eeml.zip )
Zipファイルを展開した「eeml」フォルダを、「ProcessingのSketchbookの場所/libaries/」以下にコピーする
ProcessingのSketchbookの場所は、ProcessingのPreferenceの、「Sketchbook location」で確認
まずは、シンプルに情報を表示
最初のステップとして、遠隔地のセンサーの値を取得してテキストで出力してみる
[code language=”java”]
import eeml.*; //eemlライブラリのインストール
//場所のIDを指定、iBioartは2263
int locationId = 2263;
//配信されているデータの数を指定
int dataNum = 8;
//EEMLからのデータ入力
DataIn dIn;
//データのタグを記録する配列
String[] tag = new String[dataNum];
//データの値を記録する配列
float[] value = new float[dataNum];
void setup() {
size(400,400);
//指定した場所から、EEML形式のデータを15秒ごとに受信する
dIn = new DataIn(this,"http://www.pachube.com/api/feeds/" + locationId + ".xml", "※PachubeのAPI Keyを入力", 15000);
}
void draw() {
background(0);
fill(255);
noStroke();
//データの数だけ繰り返し
for(int i=0; i < dataNum; i++){
//データのタグと値の組み合わせを、テキスト表示
text(tag[i] + " : " + value[i], 10, i * 20 + 20);
}
}
//EEMLのデータを受信したら呼び出されるイベント
void onReceiveEEML(DataIn d) {
//データの数だけ繰り返し
for(int i=0; i < dataNum; i++) {
//タグの文字列を取得
tag[i] = d.getTag(i);
//センサーの値を取得
value[i] = d.getValue(i);
}
}
[/code]
センサーの変化をグラフ化する
センサーの値の変化をグラフで表示してみる
このコードを応用すれば、Pachubeの時系列のデータを様々な方法でビジュアライズできるはず
[code language=”java”]
import eeml.*; //eemlライブラリのインストール
//場所のIDを指定、iBioartは2263
int locationId = 2263;
//配信されているデータの数を指定
int dataNum = 8;
//表示するグラフの高さ
int graphHeight = 65;
//隣接するグラフ同士の余白
int graphMargin = 5;
//EEMLからのデータ入力
DataIn dIn;
//データのタグを記録する配列
String[] tag = new String[dataNum];
//データの値を記録する配列
float[] value = new float[dataNum];
//ひとつ前のグラフの座標を記録する配列
float[] lastX = new float[dataNum];
float[] lastY = new float[dataNum];
//データを取得した回数を記録
int count = 0;
void setup() {
size(800,570);
smooth();
//指定した場所から、EEML形式のデータを5秒ごとに受信する
dIn = new DataIn(this,"http://www.pachube.com/api/feeds/" + locationId + ".xml", "※ PachubeのAPI Keyを入力", 5000);
background(0);
}
void draw() {
}
//グラフの表示を初期化する
void initGraph() {
background(0);
noFill();
stroke(63);
//グラフの枠線を描いて、それぞれの値のタグを表示
for(int i=0; i < dataNum; i++){
//枠線の表示
rect(10, i * (graphHeight + graphMargin) + graphMargin, width – 20, graphHeight);
//データのタグと、最大値、最小値を取得
String title = dIn.getTag(i) + " (" + dIn.getMinimum(i) + " – " + dIn.getMaximum(i) + ")";
//取得したタグ、最大値最小値をテキストで表示
text(title , 12, i * (graphHeight + graphMargin) + 22);
}
}
//グラフの表示を更新
void showUpdate() {
stroke(0, 255, 255);
//データの数だけ繰り返し
for(int i=0; i < dataNum; i++){
//表示する座標を計算
int top = i * (graphHeight + graphMargin) + graphMargin;
int left = 10;
int h = graphHeight;
int w = width – 20;
//データX座標が表示領域の幅に納まるようにマッピング
float x = map(count, 0, 100, 10, w);
//データのY座標が、データの最大値と最小値の範囲に納まるようにマッピング
float y = map(value[i], dIn.getMinimum(i), dIn.getMaximum(i), top + h, top);
//ひとつ前の座標から現在の座標まで直線をひく
if(count > 0){
line(lastX[i], lastY[i], x, y);
}
//現在の値を記録
lastX[i] = x;
lastY[i] = y;
}
}
//EEMLのデータを受信したら呼び出されるイベント
void onReceiveEEML(DataIn d) {
//データの数だけ繰り返し
for(int i=0; i < dataNum; i++) {
//タグの文字列を取得
tag[i] = d.getTag(i);
//センサーの値を取得
value[i] = d.getValue(i);
println(tag[i] + " : " + value[i]);
}
//もし最初のデータだったら、グラフを初期化する
if(count == 0) {
initGraph();
}
//グラフの更新
showUpdate();
//カウンターを加算
count++;
//もし指定したカウントを越えたら、リセット
if(count > 100) {
count = 0;
}
}
[/code]
Pachubeにデータを配信する
Pachubeにユーザ登録をすると、データの受信だけでなく、データの配信をすることも可能
配信したデータは、Pachubeの地図上に表示され、世界中から閲覧可能となる
しかし、配信するにはグローバルIPが設定された、外部から閲覧可能なサーバが必要となる
今回は、ローカルなネットワーク(LAN)の中で、擬似的にPachubeのデータ共有を試してみる
Arduino + ブレットボード側の配線
光センサー、温度センサー、湿度センサーなど、変化する値を出力可能なセンサーをブレッドボードに配線
ArduinoのAnalog inに接続する
Funnelライブラリのインストール
今回はArduinoとProcessingの通信にFunnelライブラリを使用
Google CodeよりFunnelの最新版をダウンロードする http://code.google.com/p/funnel/
右側にある「Featured downloads: 」より、Funnelの最新版のパッケージ「Funnel-….zip」を選択
「ProcessingのSketchbookの場所/libaries/」以下に「funnel」フォルダを作成
Zipファイルを展開、フォルダ「libraries」の中にある「processing」フォルダの中身を、作成した「funnel」フォルダ以下にコピー
Arduino側の準備
Arduinoには、StandardFirmataを読み込んでアップロードしておく
File > Example > Firmata > StandardFirmarta からロード
Processing側のプログラム
Arduinoからの入力を、Funnelを経由して取得し、それをeeml形式で送信するプログラム
[code language=”java”]
import processing.funnel.*; //Funnelライブラリの読み込み
import eeml.*; //EEMLライブラリの読み込み
//Arduinoクラスを定義
Arduino arduino;
//Arduinoから取得した値
float myValue
//Pachubeへ送出する値
DataOut dOut;
void setup() {
size(400, 200);
frameRate(15);
//Arduinoを初期化
arduino = new Arduino(this, Arduino.FIRMATA);
//5210番ポートから、Pachubeへデータを送信
dOut = new DataOut(this, 5210);
//データにタグを付ける
dOut.addData(0,"Brightness");
}
void draw() {
background(0);
fill(255);
noStroke();
//Arduinoのアナログ入力0番の値を取得
myValue = arduino.analogPin(0).value;
//画面上にテキストで表示
text("Brightness : " + myValue, 10, 20);
}
//データ送出のリクエストで呼びだされるイベント
void onReceiveRequest(DataOut d){
//Arduinoのアナログ入力0番の値を取得
myValue = arduino.analogPin(0).value;
//Pachubeに値を送出
d.update(0, myValue);
}
[/code]
プログラムを起動したら、以下のURLにWebブラウザでアクセスしてみる
送出したEEML形式のデータが見えているはず
Pachubeのデータをシェアする
教室内の他の人のEEMLデータを見てみる
システム環境設定 > ネットワーク を開く
その中に表示されているIPアドレスを控えておき、周囲の人同士で、IPアドレスを教えあう
それぞれのURLにアクセス
他の人のアドレスが参照できるはず
[参考] Pachubeデータを世界に公開する
ユーザ登録をすると、Pachubeのデータを公開することも可能
公開するには、ArduinoとProcessingの稼動しているPCが、外部から参照可能なIPアドレス(グローバルIP)に接続されていることが必要
Pachubeにサインアップした状態で、新規にフィードを生成するページへ http://www.pachube.com/feeds/new
Feed Typeを「automatic」に
Feed URLを、Arduinoが接続されたPCの「グローバルIP:5210」にする
地図をダブルクリックして、データを配信している場所を指定
その他配信できる情報は全て記入
「Save Feed」をクリックして配信開始!!
サンプルファイルのダウンロード
授業内でとりあげたコードは下記からダウンロードしてください。
今日の内容
Arduinoからのデータを、Procssingを使用して「可聴化」する
データを音に変換する方法 – Processing minimライブラリについて
サウンドの生成
サウンドファイルを再生
可変抵抗の値でサウンドをコントロール
植物にとりつけたタッチセンサーで、音を鳴らす – 「植物シンセ」簡易版
Minimライブラリについて
Minimライブラリ – Processingで音を扱うためのライブラリ、Java Sound APIを使用している
音に関する様々な機能が利用できる
サウンドの再生 – WAV, AIFF, AU, SND, MP3形式のサウンドファイルを読み込んで再生する
録音:入力された音を、ディスクに直接、またはメモリー上のバッファーに録音可能
オーディオ入力:モノラル、ステレオのオーディオ入力の値を取得
オーディオ出力:モノラル、ステレオでサウンドを出力
音響合成:音響合成のためのシンプルなインタフェイスを提供
エフェクト:サウンドエフェクトのためのインタフェイスを提供
FFT:高速フーリエ変換で、リアルタイムにスペクトル解析が可能
今日の授業では、シンプルなサイン波の生成と、サウンドファイルの再生を使用
Minimライブラリを使用してみる
サイン波の生成
ステレオのサイン波を生成する
マウスのX座標で周波数を変化させる
マウスのY座標で音量を変化させる
import ddf.minim.*;
import ddf.minim.signals.*;
//Minimクラスのインスタンスminimを定義
Minim minim;
//オーディオをoutに出力する
AudioOutput out;
//サイン波の波形をsineに生成
SineWave sine;
void setup() {
size(800, 400);
//Minimクラスをインスタンス化
minim = new Minim(this);
//モノラルのオーディオ出力として、outを準備
out = minim.getLineOut(Minim.MONO);
//440Hz、音量1.0でサイン波を出力
sine = new SineWave(440, 1.0, out.sampleRate());
//ポルタメント(音程変化のなめらかさ)を200msecに
sine.portamento(200);
//生成したサイン波をオーディオ出力に追加
out.addSignal(sine);
}
void draw() {
background(0);
}
void mouseMoved() {
//マウスのX座標を周波数に設定
//X座標の(0, width)を、周波数(40, 2000)にマップ
float freq = map(mouseX, 0, width, 40, 2000);
sine.setFreq(freq);
//マウスのY座標を音量に設定
//Y座標の(0, height)を、周波数(1.0, 0.0)にマップ
float amp = map(mouseY, 0, height, 1.0, 0.0);
sine.setAmp(amp);
}
void stop() {
//プログラムの終了処理
out.close();
minim.stop();
super.stop();
}
波形の表示
import ddf.minim.*;
import ddf.minim.signals.*;
Minim minim;
AudioOutput out;
SineWave sine;
void setup() {
size(800, 400);
frameRate(60);
smooth();
strokeWeight(2);
minim = new Minim(this);
out = minim.getLineOut(Minim.MONO);
sine = new SineWave(440, 1.0, out.sampleRate());
sine.portamento(200);
out.addSignal(sine);
}
void draw() {
//波形を表示
background(0);
stroke(0,255,0);
//Y座標の原点を画面の中心に移動
translate(0, height / 2);
//バッファーに格納されたサンプル数だけくりかえし
for (int i = 0; i & lt; out.bufferSize() - 1; i++) {
// サンプル数から、画面の幅いっぱいに波形を表示するようにマッピング
float x = map(i, 0, out.bufferSize(), 0, width);
// 画面の高さいっぱになるように、サンプルの値をマッピング
float y = map(out.mix.get(i), 0, 1.0, 0, height / 2);
// 値をプロット
point(x, y);
}
}
voidmouseMoved() {
float freq = map(mouseX, 0, width, 20, 1000);
sine.setFreq(freq);
float amp = map(mouseY, 0, height, 1, 0);
sine.setAmp(amp);
}
void stop() {
out.close();
minim.stop();
super.stop();
}
複数の音を鳴らす
2種類のsin波を生成し、同時に鳴らす
波形の表示を工夫してみる – 1つめのSin波のレベルをX軸に、2つめのSin波のレベルをY軸にして波形をプロットとするとどうなるか?
リサージュ図形 – 互いに直交する二つの単振動を順序対として得られる点の軌跡が描く平面図形
import ddf.minim.*;
import ddf.minim.signals.*;
Minim minim;
AudioOutput out;
//2つのサイン波を定義
SineWave sine1, sine2;
void setup() {
size(600, 600);
frameRate(30);
smooth();
strokeWeight(2);
minim = new Minim(this);
//ステレオで出力
out = minim.getLineOut(Minim.STEREO);
//1つめのサイン波
sine1 = new SineWave(440, 0.8, out.sampleRate());
sine1.portamento(200);
//左から出力
sine1.setPan( -1);
//オーディオ出力に追加
out.addSignal(sine1);
//2つめのサイン波
sine2 = new SineWave(440, 0.8, out.sampleRate());
sine2.portamento(200);
//右から出力
sine2.setPan(1);
//オーディオ出力に追加
out.addSignal(sine2);
background(0);
}
void draw() {
//背景を黒にフェードさせる
fill(0, 0, 0, 15);
noStroke();
rect(0, 0, width, height);
//色の設定
noFill();
stroke(0,255,0);
//画面の中心へ原点を移動
translate(width / 2, height / 2);
//波形を描画
for (int i = 0; i & lt; out.bufferSize() - 1; i++) {
// X座標を右チャンネンル、Y座標を左チャンネルにする
point(out.right.get(i) * width / 2, out.left.get(i) * height / 2);
}
}
voidmouseMoved() {
float freq1 = map(mouseX, 0, width, 20, 1000);
sine1.setFreq(freq1);
float freq2 = map(mouseY, 0, height, 20, 1000);
sine2.setFreq(freq2);
}
void stop() {
out.close();
minim.stop();
super.stop();
}
サウンドファイルの読み込み、再生
import ddf.minim.*;
Minim minim;
//サウンドファイルのプレーヤーを定義
AudioPlayer player;
void setup() {
size(800, 400, P3D);
minim = new Minim(this);
//サウンドファイルを読み込む
player = minim.loadFile("anton.aif", 2048);
//サウンドファイルを再生
player.play();
}
void draw() {
//波形を描く
background(0);
stroke(0,255,0);
//左チャンネルを上半分に描く
//y座標の原点を移動
translate(0, height / 4);
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
// サンプルの値を画面にあわせてマッピング
float x = map(i, 0, player.bufferSize(), 0, width);
float y = map(player.left.get(i), 0, 1, 0, height / 4);
point(x, y);
}
// 右チャンネルを上半分に
// y座標の原点を移動
translate(0, height / 2);
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
//サンプルの値を画面にあわせてマッピング
float x = map(i, 0, player.bufferSize(), 0, width);
float y = map(player.right.get(i), 0, 1, 0, height / 4);
point(x, y);
}
}
//マウスボタンを押すとサンプルを再生
void mousePressed() {
//サウンドを巻き戻し
player.rewind();
//サウンドを再生する
player.play();
}
void stop() {
player.close();
minim.stop();
super.stop();
}
ArduinoからProcessing+Minimをコントロールしてみる
センサーの値でSin波をコントロール
「複数の音を鳴らす」のサンプルを改造
2つのアナログ入力の値を、2つのSin波のそれぞれの周波数に対応させてみる
リサージュ図形の表示の部分はそのまま
Arduino + ブレッドボード配線図
Arduino側コード
//アナログ入力の数を定義する
#define NUM 2
//アナログ入力の値を格納する配列
int val[NUM];
void setup() {
//シリアル通信の開始
Serial.begin(9600);
}
void loop() {
//アナログ入力の数だけ繰り返し
for (int i = 0; i & lt; NUM; i++) {
// アナログ入力の値を読み込み
val[i] = analogRead(i);
}
// Processingからの読み込み完了通知を待つ
if (Serial.available() & gt; 0) {
//アナログ入力の数だけ繰り返し
for (int i = 0; i & lt; NUM; i++) {
//上位 8bit を送出
Serial.print(val[i] & gt; & gt; 8, BYTE);
//下位 8bit を送出
Serial.print(val[i] & 255, BYTE);
}
//合図用データを読み込んでバッファ領域を空にする
Serial.read();
}
}
Processing側コード
import processing.opengl.*;
import ddf.minim.*;
import ddf.minim.signals.*;
import processing.serial.*;
Minim minim;
AudioOutput out;
SineWave sine1, sine2;
//アナログ入力の数を指定
int NUM = 2;
//Serialクラスのインスタンス
Serial myPort;
//Serialより読み込んだデータを格納する配列
int[] val = new int[NUM];
void setup() {
size(600, 600, OPENGL);
smooth();
strokeWeight(2);
//ポートの名前を取得
String portName = Serial.list()[1];
//Serialクラスをインスタンス化
myPort = new Serial(this, portName, 9600);
minim = new Minim(this);
out = minim.getLineOut(Minim.STEREO);
sine1 = new SineWave(440, 0.8, out.sampleRate());
sine1.portamento(200);
sine1.setPan(-1);
out.addSignal(sine1);
sine2 = new SineWave(440, 0.8, out.sampleRate());
sine2.portamento(200);
sine2.setPan(1);
out.addSignal(sine2);
background(0);
}
void draw() {
fill(0, 0, 0, 15);
noStroke();
rect(0, 0, width, height);
noFill();
stroke(0, 255, 0);
translate(width / 2, height / 2);
for (int i = 0; i & lt; out.bufferSize() - 1; i++) {
float x = map(i, 0, out.bufferSize(), 0, width);
point(out.right.get(i) * width / 2, out.left.get(i) * height / 2);
}
}
//シリアル入力を検出した際に発生するイベント
void serialEvent(Serial p) {
//もし、バッファーにアナログ入力の数の2倍(上位8bitと下位8bit)あれば
if (myPort.available() & gt; NUM * 2 - 1) {
//入力の数だけ繰り返し
for (int i = 0; i & lt; NUM; i++) {
//上位8bitを取得
int val_high = myPort.read();
//下位8bitを取得
int val_low = myPort.read();
//2つのbit列を合成
val[i] = (val_high & lt;& lt; 8 ) + val_low;
//もし全ての入力を処理したら
if (i == NUM - 1) {
//読み込み完了の合図を送る
myPort.write(255);
}
}
}
//取得したシリアルの値を周波数にマッピング
//サイン波1
float freq1 = map(val[0], 0, 1023, 20, 1000);
sine1.setFreq(freq1);
//サイン波2
float freq2 = map(val[1], 0, 1023, 20, 1000);
sine2.setFreq(freq2);
}
//マウスが押されたら通信開始
void mousePressed() {
//バッファ領域を空にする
myPort.clear();
//合図用データを送る
myPort.write(255);
}
void stop() {
out.close();
minim.stop();
super.stop();
}
「植物シンセ」タッチセンサーでサウンドファイルを再生
まずはスイッチで実験
Arduino+ブレッドボード回路図
Arduino側コード
//スイッチの入力ピンを定義
const int buttonPin = 2;
//ボタンの状態(初期値:0)
int buttonState = 0;
void setup() {
//シリアル通信開始
Serial.begin(9600);
//スイッチの入力を定義
pinMode(buttonPin, INPUT);
}
void loop(){
//デジタル入力の状態を取得
buttonState = digitalRead(buttonPin);
//もしスイッチが押されていたら
if (buttonState == HIGH) {
//シリアルから値255を出力
Serial.print(255, BYTE);
}
}
Processing側コード
import ddf.minim.*;
import processing.serial.*;
Minim minim;
AudioPlayer player;
Serial myPort;
int val;
void setup() {
size(800, 400, P3D);
//ポートの名前を取得
String portName = Serial.list()[1];
//Serialクラスをインスタンス化
myPort = new Serial(this, portName, 9600);
minim = new Minim(this);
player = minim.loadFile("anton.aif", 2048);
}
void draw() {
//波形を描く
background(0);
stroke(0, 255, 0);
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
float x = map(i, 0, player.bufferSize(), 0, width);
point(x, height / 4 + player.left.get(i) * height / 4);
}
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
float x = map(i, 0, player.bufferSize(), 0, width);
point(x, height / 4 * 3 + player.right.get(i) * height / 4);
}
}
//シリアル入力を検出した際に発生するイベント
void serialEvent(Serial p) {
//ポートの状態を確認
if (myPort.available() & gt; 1) {
//シリアルからの入力値を取得
val = myPort.read();
}
//もし値が0より大きく、まだ音を再生していなかったら
if (val & gt; 0 && amp; player.isPlaying() == false) {
//再生位置を巻き戻して、サウンドを再生
player.rewind();
player.play();
}
}
void stop() {
player.close();
minim.stop();
super.stop();
}
植物にとりつけたタッチセンサーをスイッチにしてみる
矢坂先生の前回と前々回の講義を参考にする
タッチセンサーでスイッチを切り替えるArduinoのプログラムを利用
Arduino+ブレッドボード回路図
8番、9番のデジタル入力にタッチセンサを接続
8番ピンの手前に1MΩの抵抗を入れる
Arduino側コード
#define POWER 8
#define METAL 9
int filter = 0;
int counter = 0;
void setup() {
Serial.begin(9600);
pinMode(POWER, OUTPUT);
pinMode(METAL, INPUT);
}
void loop() {
counter = 0;
digitalWrite(POWER, HIGH);
while (digitalRead(METAL) != HIGH) {
counter++;
}
delay(1);
digitalWrite(POWER, LOW);
filter += (counter - filter) / 2;
if (filter & gt; 5) {
Serial.print(255, BYTE);
}
}
Processing側コード
import ddf.minim.*;
import processing.serial.*;
Minim minim;
AudioPlayer player;
Serial myPort;
int val;
void setup() {
size(800, 400, P3D);
//ポートの名前を取得
String portName = Serial.list()[1];
//Serialクラスをインスタンス化
myPort = new Serial(this, portName, 9600);
minim = new Minim(this);
player = minim.loadFile("anton.aif", 2048);
}
void draw() {
//波形を描く
background(0);
stroke(0,255,0);
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
float x = map(i, 0, player.bufferSize(), 0, width);
point(x, height / 4 + player.left.get(i) * height / 4);
}
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
float x = map(i, 0, player.bufferSize(), 0, width);
point(x, height / 4 * 3 + player.right.get(i) * height / 4);
}
}
//シリアル入力を検出した際に発生するイベント
void serialEvent(Serial p) {
//ポートの状態を確認
if (myPort.available() & gt; 1) {
//シリアルからの入力値を取得
val = myPort.read();
}
//もし値が0より大きく、まだ音を再生していなかったら
if (val & gt; 0 && player.isPlaying() == false) {
//再生位置を巻き戻して、サウンドを再生
player.rewind();
player.play();
}
}
void stop() {
player.close();
minim.stop();
super.stop();
}
音をランダムに再生する (Processing側コードを変更)
import ddf.minim.*;
import processing.serial.*;
Minim minim;
AudioPlayer player;
Serial myPort;
int val;
String[] soundfile = new String[8];
void setup() {
size(800, 400, P3D);
soundfile[0] = "anton.aif";
soundfile[1] = "cello-f2.aif";
soundfile[2] = "cherokee.aif";
soundfile[3] = "drumLoop.aif";
soundfile[4] = "jongly.aif";
soundfile[5] = "rainstick.aif";
soundfile[6] = "sho0630.aif";
soundfile[7] = "vibes-a1.aif";
//ポートの名前を取得
String portName = Serial.list()[1];
//Serialクラスをインスタンス化
myPort = new Serial(this, portName, 9600);
minim = new Minim(this);
player = minim.loadFile(soundfile[int(random(7))], 2048);
}
void draw() {
//波形を描く
background(0);
stroke(0,255,0);
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
float x = map(i, 0, player.bufferSize(), 0, width);
point(x, height / 4 + player.left.get(i) * height / 4);
}
for (int i = 0; i & lt; player.bufferSize() - 1; i++) {
float x = map(i, 0, player.bufferSize(), 0, width);
point(x, height / 4 * 3 + player.right.get(i) * height / 4);
}
}
//シリアル入力を検出した際に発生するイベント
void serialEvent(Serial p) {
//ポートの状態を確認
if (myPort.available() & gt; 1) {
//シリアルからの入力値を取得
val = myPort.read();
}
//もし値が0より大きく、まだ音を再生していなかったら
//if(val & gt; 0 && player.isPlaying() == false) {
if (val & gt; 0) {
//再生位置を巻き戻して、サウンドを再生
player = minim.loadFile(soundfile[int(random(7))], 2048);
player.rewind();
player.play();
}
}
void stop() {
player.close();
minim.stop();
super.stop();
}
ArduinoとProcessingの連携2:大きな値を送信する、データの流れを視覚化する
今日の内容
先週の課題の講評会
センサーからの入力を視覚化する – ArduinoとProcessingの連携のつづき
大きな値を送受信する – 大きな値 → 0〜1023 (1024段階 = 210 = 10bit)
先週のように、0〜255にマッピングするのではなく、全ての値が反映されるように送受信する方法を学ぶ – ビット演算の使用
データの流れを視覚化する
ノイズの影響などを除去しながらデータが変化する様子を視覚化する
先週の課題の講評会
先週の課題:
Generative Gestaltungのコードを活用して、Arduinoにとりつけたセンサーや可変抵抗、スイッチなどを操作すると形態や動きが変化するプログラムをProcessingで作成する。
使用するセンサーは自由に選択してください。温度センサー、光センサー、可変抵抗以外のものでもOK
複数のセンサー、可変抵抗を組み合せても構いません
大きな値を送受信する
先週の授業でのArduino → Processingのデータの送受信
Arduino – 0 〜 1023 (1024段階、210)
Serialで一度に送出できる範囲 – 0 〜 255 (256段階、28)
Arduinoの値をSerialにあわせるため、map() 関数で値を補正 – map(val, 0, 1023, 0, 255);
値の解像度が1/4になってしまっていた – できることなら全ての値を送りたい
今日用いる手法:値を分割して送信する
値を8bitずつで分解、2回にわけて値を送出する
bit演算を用いて、分解、再合成を行う
ビット(bit)とは
ビット (bit) は、デジタルコンピュータが扱うデータの最小単位 – “binary digit” の略
2通りの状態しか表現できない – “0” または “1”
例:3bit – 3桁のbit
3bitで表現できる数 → 0 〜 7 の8通り
bitで表現できる数は、2の乗数で計算できる
3bit = 2^3 = 8
8bit = 1byte
00000000 から 11111111
10進数の数値にすると、0 から 255
Serial通信で一度に送れる数値は、1byte つまり 0 (00000000) 〜 255 (11111111)
Arduino – Processingの通信の流れ
1024 = 16bit → ArduinoのAnalog inの精度
255 = 8bit → Serial で一度に送出できる値
Arduinoの入力値16bitを、8bit のかたまり2つに分解する
2回にわけて8bit( = 1byte) ずつSerialで送信
例:入力の値が、950 (0000001110110110)だったら
16bitの列から上位の8bitをとりだすには? – bit シフトを行う – 各桁を左または右に移動する
例
右方向に1bitシフト
0000001110110110 >> 1 = 0000000111011011
右に1桁移動
16bitの列から上位の8bitを取り出すには
右方向に8bitシフト
0000001110110110 >> 8 = 0000000000000011
右に8桁移動 → 上位の8桁をとりだした状態
10進数で表現すると
950 >> 8 = 3
16bitの列から下位の8bitをとりだすには?
例:下位8bitだけをとりだす
まとめ – 16bitの数値の、上位 8bit と下位 8bit を分解するには
上位 8bit:8桁 bitシフトを行う
下位 8bit:下位 8bit をマスクする
例:入力が 950 (0000001110110110) の場合
上位8bit:950 >> 8 = 3 (00000011)
下位8bit:950 & 255 = 182 (110110110)
それぞれを、Serialで送信してあげればよい
以上の手順を、ArduinoとProcessing側双方で実装してみる
Arduino側
//アナログ入力の数を定義する
#define NUM 1
//アナログ入力の値を格納する配列
int val[NUM];
void setup() {
//シリアル通信の開始
Serial.begin(9600);
}
void loop() {
//アナログ入力の数だけ繰り返し
for(int i=0; i 0){
//アナログ入力の数だけ繰り返し
for(int i=0; i > 8, BYTE);
//下位 8bit を送出
Serial.print(val[i] & 255, BYTE);
}
//合図用データを読み込んでバッファ領域を空にする
Serial.read();
}
}
Processing側
import processing.serial.*;
//アナログ入力の数を指定
int NUM = 6;
//Serialクラスのインスタンス
Serial myPort;
//Serialより読み込んだデータを格納する配列
int[] val = new int[NUM];
void setup() {
//画面を生成
size(400, 400);
//ポートの名前を取得
String portName = Serial.list()[1];
//Serialクラスをインスタンス化
myPort = new Serial(this, portName, 9600);
}
void draw() {
//背景を黒に
background(0);
//塗りを白に
fill(255);
//入力の数だけ繰り返し
for(int i=0; i NUM * 2 - 1) {
//入力の数だけ繰り返し
for(int i=0; i
データの流れを視覚化する
データの流れを視覚化する1
データからノイズを除去する
センサーからの入力値には、意図しない信号(ノイズ)がのってしまう場合がある
Processing側の工夫で、急激な入力信号の変化は除去したい
実際の値の変化を、指定した値でeasingする → なめらかな変化となる
Arduino側
//アナログ入力の数を定義する
#define NUM 1
//アナログ入力の値を格納する配列
int val[NUM];
void setup() {
//シリアル通信の開始
Serial.begin(9600);
}
void loop() {
//アナログ入力の数だけ繰り返し
for(int i=0; i 0) {
//アナログ入力の数だけ繰り返し
for(int i=0; i > 8, BYTE);
//下位 8bit を送出
Serial.print(val[i] & 255, BYTE);
}
//合図用データを読み込んでバッファ領域を空にする
Serial.read();
}
}
Processing側
import processing.serial.*;
//アナログ入力の数を指定
int NUM = 1;
//現在走査しているX座標
int x;
//なめらかさ
float easing = 0.05;
//なめらかに変換した値
float easedVal;
//現在Serial信号を受信しているか
boolean running = false;
//Serialクラスのインスタンス
Serial myPort;
//Serialより読み込んだデータを格納する配列
int[] val = new int[NUM];
void setup() {
//画面を生成
size(1024, 480);
//フレームレート
frameRate(30);
//ポートの名前を取得
String portName = Serial.list()[1];
//Serialクラスをインスタンス化
myPort = new Serial(this, portName, 9600);
//背景を黒に
background(255);
}
void draw() {
//もし現在信号を受信していたら
if(running) {
//入力値から目標とする値をマッピング
float targetVal = map(val[0], 0, 1024, 0, height/2);
//目標値をなめらかに変換
easedVal += (targetVal - easedVal) * easing;
//グラフの描画
//白い線で前の値を消す
stroke(255);
line(x, 0, x, height);
//色を指定
stroke(63,127,255);
//現在走査している場所を線で示す
line(x+1, 0, x+1, height);
//なめらかにしていない値を描画
line(x, height/2, x, targetVal);
//なめらかにした値を描画
line(x, height, x, easedVal + height/2);
//X座標の更新
x++;
//もし画面の端まできたら、最初から
if(x > width) {
x = 0;
}
}
else {
//まだ信号を受信していなかったら
//画面をクリックするようにメッセージを出す
background(255);
fill(0);
text("click on screen", 10, 20);
}
}
//シリアル入力を検出した際に発生するイベント
void serialEvent(Serial p) {
//もし、バッファーにアナログ入力の数の2倍(上位8bitと下位8bit)あれば
if(myPort.available() > NUM * 2 - 1) {
//入力の数だけ繰り返し
for(int i=0; i
データの流れを視覚化する 2
視覚的な部分をもう少し工夫してみる
データを横にスキャンするのではなく、円形にスキャンしてみる
Arduino側のプログラムは同じ
Processing側
import processing.serial.*;
//アナログ入力の数を指定
int NUM = 1;
//半径
float radius;
//現在の角度
float angle;
//現在Serial信号を受信しているか
boolean running = false;
//Serialクラスのインスタンス
Serial myPort;
//Serialより読み込んだデータを格納する配列
int[] val = new int[NUM];
void setup() {
//画面を生成
size(640, 640);
colorMode(HSB,360,100,100,100);
frameRate(15);
strokeWeight(3);
//ポートの名前を取得
String portName = Serial.list()[1];
//Serialクラスをインスタンス化
myPort = new Serial(this, portName, 9600);
//背景を黒に
background(0);
//スムージング
smooth();
}
void draw() {
if(running) {
//入力された値から円の半径をマッピング
float radius = map(val[0], 0, 1023, 0, height/2);
//円の中心点を指定
int middleX = width/2;
int middleY = height/2;
//三角関数を仕様して角度から座標を算出
float x = middleX + cos(angle) * height/2;
float y = middleY + sin(angle) * height/2;
//黒い線で前の値を消す
stroke(0,0,0);
line(middleX, middleY, x, y);
x = middleX + cos(angle) * radius;
y = middleY + sin(angle) * radius;
//値によって色相を変化させる
float h = map(val[0], 0, 1023, 180, 360);
//算出した色相で線を描く
stroke(h, 75, 100);
line(middleX, middleY, x, y);
//角度の更新
angle += 0.01;
}
else {
//まだ信号を受信していなかったら
//画面をクリックするようにメッセージを出す
background(0);
fill(0,0,100);
text("click on screen", 10, 20);
}
}
//シリアル入力を検出した際に発生するイベント
void serialEvent(Serial p) {
//もし、バッファーにアナログ入力の数の2倍(上位8bitと下位8bit)あれば
if(myPort.available() > NUM * 2 - 1) {
//入力の数だけ繰り返し
for(int i=0; i
サンプルコードのダウンロード
今回の授業で紹介した全てのサンプルは、下記のリンクからダウンロード可能です
今日の内容
ArduinoとProcessingを連携する Arduinoのアナログインにセンサーからの情報を入力
ProcessingからArduinoの情報を取得してリアルタイムに情報を視覚化 今後のBio Media Artの肝となってくる部分!!
ArduinoとProcessinの連携方法
Serial 通信による連携 Funnelによる連携
この授業では、Serial通信のほうを採用 参考として、Funnelを使用した方法も紹介
シリアル通信:全体の構成
サンプル1:可変抵抗の値を送信
まずはシンプルな構成で連携をテスト ブレッドボードとArduinoの配線
可変抵抗1つ
ArduinoのAnalog in 0に抵抗値を接続
USBケーブルで、ArduinoをPCに接続
Arduino
Analog in の値を取得 – analogRead()
シリアルで値を送信 – Serial.print()
Processing
シリアルポートに接続
シリアルポートから値を読み取る
可変抵抗器 (ポテンショメーター)
抵抗値を変更することができる抵抗器
今回は、10KΩの可変抵抗器を用意
Arduino側のプログラム
setup() – 初期設定
loop() – メインループ
Analog in 0 の値を読み取る
Analog in の値の範囲は、0〜1024
Serialで一度に出力できる範囲は、0〜255 (1 BYTE)
入力値を、0〜255の範囲にマッピング (map関数)
シリアルで補正した値を送信
一定間隔静止して、繰り返す
int in0; //アナログ入力0
int outByte; //シリアルで出力する値
void setup(){
//シリアル通信開始
Serial.begin(9600);
}
void loop(){
//アナログ入力0番ピンの値を読み取り(0~1023)
in0 = analogRead(0);
//値を、0〜255の範囲にマップ
outByte = map(in0, 0, 1023, 0, 255);
//シリアルでoutByteを送信(BYTEフォーマット)
Serial.print(outByte,BYTE);
//1秒間に10回ループ
delay(100);
}
Processing側プログラム setup() – 初期設定 serialEvent() – シリアルイベント シリアル通信でデータを受信した際に発生するイベント read() で値を取得する (0〜255) 取得した値をモニタする – println() draw() – 描画 取得した値を背景のグレースケールのレベルに対応させている
import processing.serial.*;
// Serialクラスのインスタンス
Serial myPort;
// シリアルポートから取得したデータ(Byte)
int inByte;
void setup()
{
size(640, 640);
// Macのシリアルのリストの最初のポートがFTDIアダプタのポート
String portName = Serial.list()[0];
// ポートとスピードを設定して、Serialクラスを初期化、
myPort = new Serial(this, portName, 9600);
}
void draw()
{
// シリアルから取得した値を背景色に設定
background(inByte);
}
void serialEvent(Serial p){
// 設定したシリアルポートからデータを読み取り
inByte = myPort.read();
}
完成! 可変抵抗の値を変化させると、背景色の濃度が変化する
サンプル2:複数の値を送信
複数のセンサーからの値を取得するには Arduinoには、Analog in が最大5つまで使用できる 複数のAnalog入力の値を取得してみる まずは可変抵抗でテスト ブレットボードとArduinoの配線例
ArduinoとProcessinを連携する際の注意 Arduinoからのシリアル出力は、値が順番に送られる
例:入力が3つ(byte0, byte1, byte2)の場合
byte0, byte1, byte2, byte0, byte1, byte2, ….
Processingの受信方法を工夫しないと、値がずれてしまう場合がある 例:
012012012012… – 想定した値
120120120120… – 順番がずれた値
順番がずれてしまわないように、Processing側で値のセットを受け取ったら読み込み完了の合図を送る 同期のイメージ
int in0, in1, in2; //アナログ入力0
int outByte0, outByte1, outByte2; //シリアルで出力する値
void setup(){
//シリアル通信開始
Serial.begin(9600);
}
void loop(){
//アナログ入力0〜2番ピンの値を読み取り(0~1023)
in0 = analogRead(0);
in1 = analogRead(1);
in2 = analogRead(2);
//値を、0〜255の範囲にマップ
outByte0 = map(in0, 0, 1023, 0, 255);
outByte1 = map(in1, 0, 1023, 0, 255);
outByte2 = map(in2, 0, 1023, 0, 255);
//Processingと同期
if(Serial.available()>0){
//シリアルでoutByteを送信(BYTEフォーマット)
Serial.print(outByte0,BYTE);
Serial.print(outByte1,BYTE);
Serial.print(outByte2,BYTE);
}
//1秒間に10回ループ
delay(100);
}
import processing.serial.*;
// Serialクラスのインスタンス
Serial myPort;
// シリアルポートから取得したデータ(Byte)
int inByte0, inByte1, inByte2;
void setup()
{
size(640, 640);
// Macのシリアルのリストの最初のポートがFTDIアダプタのポート
String portName = Serial.list()[0];
// ポートとスピードを設定して、Serialクラスを初期化、
myPort = new Serial(this, portName, 9600);
noStroke();
smooth();
}
void draw()
{
background(inByte0);
fill(inByte1);
float diameter = float(inByte2)/255.0*width;
ellipse(width/2, height/2, diameter, diameter);
}
void serialEvent(Serial p){
//Arduinoから送られてきたデータが3個来たら
if(myPort.available()>2){
//順番にデータを読み込む
inByte0 = myPort.read();
inByte1 = myPort.read();
inByte2 = myPort.read();
//Arduinoへ読み込み完了の合図を送る
myPort.write(255);
}
}
//マウスが押されたら通信開始
void mousePressed(){
//バッファ領域を空にする
myPort.clear();
//合図用データを送る
myPort.write(255);
}
可変抵抗の設定で、背景色、描画色、円の大きさが変化する
サンプル3:光センサーを使う
可変抵抗でなくても、変化するアナログ値であれば、なんでも利用可能 可変抵抗を各種センサーに入れ替えてみる
光センサー (CdSセル)
測距センサー (赤外線センサー)
感圧センサー
振動センサー
曲げセンサー
温度センサー
傾きセンサー
まずは、光センサーを使用してみます Arduinoと、Processingのプログラムは、そのまま流用可能 ブレットボードとArduinoの配線例
サンプル4:温度センサーを使う
温度センサーをつかって、温度に反応するようにしてみる LM35DZ – 温度センサー、0℃で0V、20℃で200mVになる
課題:センサーの値を視覚化する
Generative Gestaltungのコードを活用して、Arduinoにとりつけたセンサーや可変抵抗、スイッチなどを操作すると形態や動きが変化するプログラムをProcessingで作成する。 使用するセンサーは自由に選択してください。温度センサー、光センサー、可変抵抗以外のものでもOK 複数のセンサー、可変抵抗を組み合せても構いません 次回の月曜の授業まで!
課題のサンプル
Generative Gestaltungの”M_1_2_01″を改造して作成 マウスのX座標によって変化する変数”faderX”を、シリアル入力の値に変更 色も微妙に変化させている Arduino側のコードは、サンプル1と一緒
int in0; //アナログ入力0
int outByte; //シリアルで出力する値
void setup(){
//シリアル通信開始
Serial.begin(9600);
}
void loop(){
//アナログ入力0番ピンの値を読み取り(0~1023)
in0 = analogRead(0);
//値を、0〜255の範囲にマップ
outByte = map(in0, 0, 1023, 0, 255);
//シリアルでoutByteを送信(BYTEフォーマット)
Serial.print(outByte,BYTE);
//1秒間に10回ループ
delay(100);
}
Processing側コード Generative Gestaltung M_1_2_01 を改造
import processing.serial.*;
import processing.pdf.*;
boolean savePDF = false;
int actRandomSeed = 0;
int count = 360;
// Serialクラスのインスタンス
Serial myPort;
// シリアルポートから取得したデータ(Byte)
int inByte;
void setup()
{
size(800, 800);
colorMode(HSB, 360,100,100,100);
cursor(CROSS);
smooth();
// Macのシリアルのリストの最初のポートがFTDIアダプタのポート
String portName = Serial.list()[0];
// ポートとスピードを設定して、Serialクラスを初期化、
myPort = new Serial(this, portName, 9600);
}
void draw() {
if (savePDF) beginRecord(PDF, timestamp()+".pdf");
background(0);
noStroke();
float faderX = inByte/255.0;
randomSeed(actRandomSeed);
float angle = radians(360/float(count));
for (int i=0; i
今日の目標
時系列のデータファイル(タブ区切りのテキスト)を読みこみ、Processingで可視化する 「ビジュアライジング・データ」の4章、「時系列」のサンプルプログラムを解析 まずは普通にグラフ化する手法を学ぶ より自由な発想でビジュアライズしてみる (バイオDJ!?)
Visualizing Dataのサンプルプログラムをダウンロード
シンプルなプロット表示を理解する
時系列のデータを点の連なりで表現
「ch04-milkteacoffee」→「figure_01_just_points」を開く まずは実行してみる 点によるシンプルなプロットが表示される
このプロットは、dataフォルダ内にある、tab区切りのデータファイルを読み込んでいる データの内容は下記のとおり
Year Milk Tea Coffee
1910 32.2 9.6 21.7
1911 31.3 10.2 19.7
1912 34.4 9.6 25.5
1913 33.1 8.5 21.2
1914 31.1 8.9 21.8
1915 29 9.6 25
1916 28 9.6 27.1
... (中略) ...
2002 21.9 7.8 18.1
2003 21.6 7.5 18.5
2004 21.2 7.3 18.8
プログラムのおおまかな構造
プログラムの構造 – メインのプログラム(figure_01_just_points.pde)と、FloatTableクラス(FloatTable.pde)から構成 メインプログラム – FloatTableクラスを利用してデータを読み込み、データをプロットする点を描画 FloatTableクラス – 表形式のデータファイルを読み込んで、その値を利用しやすい形に変換する。また、最大値や最小値の算出などデータを扱うための様々な便利な機能を備えている
FloatTableクラス
FloatTableクラスは、tsv(タブ区切り)形式の表データをファイルから読み込み、データの最大値と最小値を求めたり、余計なデータを除去したりと、様々な機能を内包している FloatTableクラスのプロパティ(属性)とメソッド(機能)をまとめてみる
プロパティ(属性)
rowCount – 行の数 (横)
columnCount – 列の数 (縦)
data[][] – 読み込まれたデータ data[行][列]
rowNames[] – 行につけられたラベル
columnNames[] – 列につけられたラベル
メソッド
FloatTable(String filename) – コンストラクタ。指定されたファイルからタブ区切りのデータを読み込み、取得された値をプロパティに読み込む
void scrubQuotes(String[] array) – データから、引用符(“)を除去する
int getRowCount() – 行の数を数える
String getRowName(int rowIndex) – 指定した行の名前を返す
String[] getRowNames() – 全ての行の名前を返す
int getRowIndex(String name) – 行の名前から行番号を返す
int getColumnCount() – 列の数を数える
String getColumnName(int colndex) – 指定した列の名前を返す
String[] getColumnNames() – 全ての列の名前を返す
float getFloat(int rowIndex, int col) – 指定した行と列の値をFloat型で返す
boolean isValid(int row, int col) – 指定した行と列の値が存在するかどうかを調べる
float getColumnMin(int col) – 指定した列の最小値を求める
float getColumnMax(int col) – 指定した列の最大値を求める
float getRowMin(int col) – 指定した行の最小値を求める
float getRowMax(int col) – 指定した行の最大値を求める
float getTableMax() – 表全体の最大値を求める
コメントを付加したソースコードは下記のとおり
//ファイルの最初の行は、各列の名前を示したヘッダーでなければならない
//最初の列は、各行のタイトルでなくてはならない
//その他の値は、全てfloat型の値であると理解される
//例えば、getFloat(0, 0)と指定すると、最初の行の一番始めのコラムの値が返される
//ファイルは、テキスト形式、タブ区切りで書くこと
//空白の行は無視される
//空白文字は無視される
class FloatTable {
int rowCount;
int columnCount;
float[][] data;
String[] rowNames;
String[] columnNames;
//コンストラクタ
//指定されたファイルからタブ区切りのデータを読み込み、取得された値をプロパティに読み込む
FloatTable(String filename) {
String[] rows = loadStrings(filename);
String[] columns = split(rows[0], TAB);
columnNames = subset(columns, 1); //一番左上のラベルは無視
scrubQuotes(columnNames);
columnCount = columnNames.length;
rowNames = new String[rows.length-1];
data = new float[rows.length-1][];
//最初の行はヘッダーなので無視して、次の行から読みこむ
for (int i = 1; i < rows.length; i++) {
if (trim(rows[i]).length() == 0) {
continue; //空白行は無視する
}
if (rows[i].startsWith("#")) {
continue; //コメント行(#のついた行)は無視する
}
//タブ区切りでデータを読み込む
String[] pieces = split(rows[i], TAB);
//引用符を除去
scrubQuotes(pieces);
//行のタイトル(最初の列)をコピー
rowNames[rowCount] = pieces[0];
//最初の列(タイトル)は無視して、次の列からデータをdata配列にコピー
data[rowCount] = parseFloat(subset(pieces, 1));
//行のカウントを1つ増加
rowCount++;
}
//必要であれば、data配列の大きさをリサイズ
data = (float[][]) subset(data, 0, rowCount);
}
//データから引用符を除去
void scrubQuotes(String[] array) {
for (int i = 0; i < array.length; i++) {
if (array[i].length() > 2) {
if (array[i].startsWith("\"") && array[i].endsWith("\"")) {
array[i] = array[i].substring(1, array[i].length() - 1);
}
}
//ダブルクォートをシングルクォートに
array[i] = array[i].replaceAll(""\"", "\"");
}
}
//行の数を数える
int getRowCount() {
return rowCount;
}
//指定した列番号の行の名前を返す
String getRowName(int rowIndex) {
return rowNames[rowIndex];
}
//全ての行の名前を返す
String[] getRowNames() {
return rowNames;
}
//行の名前から行番号を返す
//見つからなかった場合は、-1を返す
int getRowIndex(String name) {
for (int i = 0; i < rowCount; i++) {
if (rowNames[i].equals(name)) {
return i;
}
}
//println("No row named '" + name + "' was found");
return -1;
}
//列の数を数える
int getColumnCount() {
return columnCount;
}
//指定した列の名前を返す
String getColumnName(int colIndex) {
return columnNames[colIndex];
}
//全ての列の名前を返す
String[] getColumnNames() {
return columnNames;
}
//指定した行と列の値をFloat型で返す
float getFloat(int rowIndex, int col) {
//異常な値を引数が指定されていた再には、例外処理をしている
if ((rowIndex < 0) || (rowIndex >= data.length)) {
throw new RuntimeException("There is no row " + rowIndex);
}
if ((col < 0) || (col >= data[rowIndex].length)) {
throw new RuntimeException("Row " + rowIndex + " does not have a column " + col);
}
return data[rowIndex][col];
}
//指定した行と列の値が存在するかどうかを調べる
boolean isValid(int row, int col) {
if (row < 0) return false;
if (row >= rowCount) return false;
//if (col >= columnCount) return false;
if (col >= data[row].length) return false;
if (col < 0) return false;
return !Float.isNaN(data[row][col]);
}
//指定した列の最小値を求める
float getColumnMin(int col) {
float m = Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
if (isValid(row, col)) {
if (data[row][col] < m) {
m = data[row][col];
}
}
}
return m;
}
//指定した列の最大値を求める
float getColumnMax(int col) {
float m = -Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
if (isValid(row, col)) {
if (data[row][col] > m) {
m = data[row][col];
}
}
}
return m;
}
//指定した行の最小値を求める
float getRowMin(int row) {
float m = Float.MAX_VALUE;
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] < m) {
m = data[row][col];
}
}
}
return m;
}
//指定した行の最大値を求める
float getRowMax(int row) {
float m = -Float.MAX_VALUE;
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] > m) {
m = data[row][col];
}
}
}
return m;
}
//表全体の最小値を求める
float getTableMin() {
float m = Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] < m) {
m = data[row][col];
}
}
}
}
return m;
}
//表全体の最大値を求める
float getTableMax() {
float m = -Float.MAX_VALUE;
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < columnCount; col++) {
if (isValid(row, col)) {
if (data[row][col] > m) {
m = data[row][col];
}
}
}
}
return m;
}
}
メインプログラムの構造
メインプログラム(figure_01_just_points.pde)では、このFloatTableクラスを活用して、時系列のデータを点によってプロットしている
void setup()
画面の初期設定
タブ区切りのデータファイル、”milk-tea-coffe.tsv”をデータとして、FloatTableクラスをインスタンス化 → data
データを読み込んだFloatTableのインスタンスdataを利用して、初期化に必要な値を取得
年 – 行の名前から
プロットの開始年 – 行の最小値
プロットの終了年 – 行の最大値
プロットする画面の領域を計算
void draw()
プロット画面の背景を生成
drawDataPoints() 関数を呼び出し、読み出すコラムは0
void drawDataPoints()
データの行数を計算
指定したコラムの値を取り出し
画面内に納まるように、値を整える(map関数)
値を整えた座標に点を描画
map() 関数
map(値, 変換前の値の最小値, 変換前の値の最大値, 変換後の値の最小値, 変換後の値の最大値)
数値の範囲を別の範囲に変換する
例:プロットするデータの最小値が dataMin、最大値が dataMax のときに、y座標 plotY2 から plotY1 の範囲にちょうど納まるように値 value を変換する
map(value, dataMin, dataMax, plotY2, plotY1);
FloatTable data; //FloatTableクラスのインスタンス
float dataMin, dataMax; //表示するデータの最大値と最小値
float plotX1, plotY1; //プロットする画面のX座標とY座標の最小値
float plotX2, plotY2; //プロットする画面のX座標とY座標の最大値
int yearMin, yearMax; //データの年の最小値と最大値
int[] years; //年の配列
void setup() {
size(720, 405);
//読み込むデータファイルを指定して、FloatTableクラスをインスタンス化
data = new FloatTable("milk-tea-coffee.tsv");
//列の名前(年)を配列yearsに読み込む
years = int(data.getRowNames());
//最小の年は、years配列の先頭
yearMin = years[0];
//最大の年は、years配列の最後
yearMax = years[years.length - 1];
//データの最小値は0に設定
dataMin = 0;
//全ての数値から最大値を求める
dataMax = data.getTableMax();
//プロットする画面の座標の最大値と最小値を設定
plotX1 = 50;
plotX2 = width - plotX1;
plotY1 = 60;
plotY2 = height - plotY1;
smooth();
}
void draw() {
//画面サイズを指定
background(224);
//プロットするエリアを白い四角形で表示
fill(255);
rectMode(CORNERS);
noStroke();
rect(plotX1, plotY1, plotX2, plotY2);
//最初の列(milk)のデータをプロット
strokeWeight(5);
stroke(#5679C1);
drawDataPoints(0);
}
//点の並びでデータを表示する関数
//引数には表示したいデータの列を入れる
void drawDataPoints(int col) {
//行の数(=データの数)を数える
int rowCount = data.getRowCount();
//データの数だけくりかえし
for (int row = 0; row < rowCount; row++) {
//もし正しい数値だったら(データの範囲が正しければ)
if (data.isValid(row, col)) {
//行と列を指定して、データの数値をとりだす
float value = data.getFloat(row, col);
//プロットする画面の幅(x座標)にちょうど納まるように、値を変換
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
//プロットする画面の高さ(y座標)にちょうど納まるように、値を変換
float y = map(value, dataMin, dataMax, plotY2, plotY1);
//変換した値を座標にして、点を描画
point(x, y);
}
}
}
情報を表示
現在表示しているデータの内容や数値を文字や図形で記述する 表示しているデータの内容 → Milk 垂直方向の目盛(消費量) 垂直方向の消費量の数値 水平方向の目盛(年) 水平方向の年の値 ラベル → Gallons cosumed per caita, Year キーボードで”]”を入力すると次の列のデータに変更 キーボードで”[“を入力すると前の列のデータに変更
FloatTable data;
float dataMin, dataMax;
float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;
int rowCount;
int columnCount;
int currentColumn = 0;
int yearMin, yearMax;
int[] years;
int yearInterval = 10;
int volumeInterval = 10;
int volumeIntervalMinor = 5;
PFont plotFont;
void setup() {
size(720, 405);
data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();
years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length - 1];
dataMin = 0;
dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;
plotX1 = 120;
plotX2 = width - 80;
labelX = 50;
plotY1 = 60;
plotY2 = height - 70;
labelY = height - 25;
plotFont = createFont("SansSerif", 20);
textFont(plotFont);
smooth();
}
void draw() {
background(224);
fill(255);
rectMode(CORNERS);
noStroke();
rect(plotX1, plotY1, plotX2, plotY2);
//タイトルを表示
drawTitle();
//それぞれの軸のラベル(消費量/年)を表示
drawAxisLabels();
//横軸の値(年)を表示
drawYearLabels();
//縦軸の値(消費量)を表示
drawVolumeLabels();
stroke(#5679C1);
strokeWeight(5);
//指定した列(currentColumn)のデータをプロット
drawDataPoints(currentColumn);
}
//タイトルを表示
void drawTitle() {
fill(0);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 - 10);
}
//それぞれの軸のラベル(消費量/年)を表示
void drawAxisLabels() {
fill(0);
textSize(13);
textLeading(15);
textAlign(CENTER, CENTER);
text("Gallons\nconsumed\nper capita", labelX, (plotY1+plotY2)/2);
textAlign(CENTER);
text("Year", (plotX1+plotX2)/2, labelY);
}
//横軸の値(年)を表示
void drawYearLabels() {
fill(0);
textSize(10);
textAlign(CENTER, TOP);
// Use thin, gray lines to draw the grid
stroke(224);
strokeWeight(1);
for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + 10);
line(x, plotY1, x, plotY2);
}
}
}
//縦軸の値(消費量)を表示
void drawVolumeLabels() {
fill(0);
textSize(10);
stroke(128);
strokeWeight(1);
for (float v = dataMin; v <= dataMax; v += volumeIntervalMinor) {
if (v % volumeIntervalMinor == 0) {
float y = map(v, dataMin, dataMax, plotY2, plotY1);
if (v % volumeInterval == 0) {
if (v == dataMin) {
textAlign(RIGHT);
} else if (v == dataMax) {
textAlign(RIGHT, TOP);
} else {
textAlign(RIGHT, CENTER);
}
text(floor(v), plotX1 - 10, y);
line(plotX1 - 4, y, plotX1, y);
} else {
//line(plotX1 - 2, y, plotX1, y);
}
}
}
}
//指定した列の値を点の並びでプロット
void drawDataPoints(int col) {
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = map(value, dataMin, dataMax, plotY2, plotY1);
point(x, y);
}
}
}
//キー入力で表示する列を切り替える
void keyPressed() {
if (key == '[') {
currentColumn--;
if (currentColumn < 0) {
currentColumn = columnCount - 1;
}
} else if (key == ']') {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
線分で表示する
点でプロットするのではなく、線でそれぞれのデータを結んでいく void drawDataLine(int col) – 指定した列のデータを線分(vertex)で結んでいる
FloatTable data;
float dataMin, dataMax;
float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;
int rowCount;
int columnCount;
int currentColumn = 0;
int yearMin, yearMax;
int[] years;
int yearInterval = 10;
int volumeInterval = 10;
PFont plotFont;
void setup() {
size(720, 405);
data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();
years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length - 1];
dataMin = 0;
dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;
plotX1 = 120;
plotX2 = width - 80;
labelX = 50;
plotY1 = 60;
plotY2 = height - 70;
labelY = height - 25;
plotFont = createFont("SansSerif", 20);
textFont(plotFont);
smooth();
}
void draw() {
background(224);
fill(255);
rectMode(CORNERS);
noStroke();
rect(plotX1, plotY1, plotX2, plotY2);
drawTitle();
drawAxisLabels();
drawYearLabels();
drawVolumeLabels();
strokeWeight(5);
noFill();
stroke(#5679C1);
//線でデータを結ぶ
drawDataLine(currentColumn);
}
void drawTitle() {
fill(0);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 - 10);
}
void drawAxisLabels() {
fill(0);
textSize(13);
textLeading(15);
textAlign(CENTER, CENTER);
text("Gallons\nconsumed\nper capita", labelX, (plotY1+plotY2)/2);
textAlign(CENTER);
text("Year", (plotX1+plotX2)/2, labelY);
}
void drawYearLabels() {
fill(0);
textSize(10);
textAlign(CENTER);
stroke(224);
strokeWeight(1);
for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
line(x, plotY1, x, plotY2);
}
}
}
int volumeIntervalMinor = 5;
void drawVolumeLabels() {
fill(0);
textSize(10);
textAlign(RIGHT);
stroke(128);
strokeWeight(1);
for (float v = dataMin; v <= dataMax; v += volumeIntervalMinor) {
if (v % volumeIntervalMinor == 0) {
float y = map(v, dataMin, dataMax, plotY2, plotY1);
if (v % volumeInterval == 0) {
float textOffset = textAscent()/2;
if (v == dataMin) {
textOffset = 0;
} else if (v == dataMax) {
textOffset = textAscent();
}
text(floor(v), plotX1 - 10, y + textOffset);
line(plotX1 - 4, y, plotX1, y);
} else {
//line(plotX1 - 2, y, plotX1, y);
}
}
}
}
//線でデータを結ぶ
void drawDataLine(int col) {
beginShape();
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = map(value, dataMin, dataMax, plotY2, plotY1);
vertex(x, y);
}
}
endShape();
}
void keyPressed() {
if (key == '[') {
currentColumn--;
if (currentColumn < 0) {
currentColumn = columnCount - 1;
}
} else if (key == ']') {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
より自由な発想で視覚化してみる
この授業の趣旨は、科学的に厳密な視覚化ではない より自由な発想で、データをもとに形態を生成してみる バイオVJ?
応用1:円の大きさ量でを表現してみる
数値の変化を円の直径にあてはめて、円の大きさの変化によって、情報を表現してみる
FloatTable data;
float dataMin, dataMax;
float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;
int rowCount;
int columnCount;
int currentColumn = 0;
int yearMin, yearMax;
int[] years;
int yearInterval = 10;
int volumeInterval = 10;
PFont plotFont;
void setup() {
size(1024, 360);
data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();
years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length - 1];
dataMin = data.getTableMin();
dataMax = ceil(data.getTableMax() / volumeInterval) * volumeInterval;
plotX1 = 10;
plotX2 = width - 10;
labelX = 10;
plotY1 = 40;
plotY2 = height - 40;
labelY = height - 40;
plotFont = createFont("SansSerif", 20);
textFont(plotFont);
smooth();
}
void draw() {
background(255);
drawTitle();
drawYearLabels();
noStroke();
fill(63,127,255,30);
//円の大きさで、データを視覚化
drawDataCircleSize(currentColumn);
}
//円の大きさでデータを視覚化する
void drawDataCircleSize(int col) {
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = height/2;
//円の直径を、取得した値をもとに計算
float diameter = map(value, dataMin, dataMax, 0, 200);
//円を描く
ellipse(x, y, diameter, diameter);
}
}
}
void drawTitle() {
fill(0);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 - 10);
}
void drawYearLabels() {
fill(0);
textSize(10);
textAlign(CENTER);
stroke(224);
strokeWeight(1);
for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
line(x, plotY1, x, plotY2);
}
}
}
void keyPressed() {
if (key == '[') {
currentColumn--;
if (currentColumn < 0) {
currentColumn = columnCount - 1;
}
} else if (key == ']') {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
応用2:色の透明度で量を表現してみる
描画する図形の透明度によって、データを表現してみる 数値が高くなるほど、透明度が下がるようにする
FloatTable data;
float dataMin, dataMax;
float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;
int rowCount;
int columnCount;
int currentColumn = 0;
int yearMin, yearMax;
int[] years;
int yearInterval = 10;
int volumeInterval = 10;
PFont plotFont;
void setup() {
size(1024, 360);
data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();
years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length - 1];
dataMin = data.getTableMin();
dataMax = data.getTableMax();
plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height - 30;
labelY = height - 30;
plotFont = createFont("SansSerif", 20);
textFont(plotFont);
smooth();
}
void draw() {
background(0);
drawTitle();
drawYearLabels();
noStroke();
//色の濃度(alpha)でデータを視覚化する
drawDataAlpha(currentColumn);
}
//色の濃度(alpha)でデータを視覚化する
void drawDataAlpha(int col) {
rectMode(CENTER);
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = height/2;
//データを色の濃度と対応させている
float a = map(value, dataMin, dataMax, 0, 255);
//四角形の幅を算出
float w = (plotX2-plotX1)/parseFloat(years.length)*1.1;
//計算した濃度で色を指定
fill(31,127,255,a);
//四角形を描画
rect(x, y, w, plotY2-plotY1);
}
}
}
void drawTitle() {
fill(255);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 - 10);
}
void drawYearLabels() {
fill(255);
textSize(10);
textAlign(CENTER);
stroke(224);
strokeWeight(1);
for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
//line(x, plotY1, x, plotY2);
}
}
}
void keyPressed() {
if (key == '[') {
currentColumn--;
if (currentColumn < 0) {
currentColumn = columnCount - 1;
}
} else if (key == ']') {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
応用3:色相で量を表現してみる
透明度ではなく、色相で表現するように変更してみる 低い数値 → 青(色相=240)、高い数値 → 赤(色相=360) というようにマッピングしてみる
FloatTable data;
float dataMin, dataMax;
float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;
int rowCount;
int columnCount;
int currentColumn = 0;
int yearMin, yearMax;
int[] years;
int yearInterval = 10;
int volumeInterval = 10;
PFont plotFont;
void setup() {
size(1024, 360);
colorMode(HSB,360,100,100,100);
data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();
years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length - 1];
dataMin = data.getTableMin();
dataMax = data.getTableMax();
plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height - 30;
labelY = height - 30;
plotFont = createFont("SansSerif", 20);
textFont(plotFont);
smooth();
}
void draw() {
background(0);
drawTitle();
drawYearLabels();
noStroke();
//色相でデータを視覚化
drawDataHue(currentColumn);
}
//色相でデータを視覚化
void drawDataAlpha(int col) {
rectMode(CENTER);
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
float y = height/2;
//取得したデータで色相を算出
float h = map(value, dataMin, dataMax, 240, 360);
float w = (plotX2-plotX1)/parseFloat(years.length)*1.1;
//算出した色相で色を設定
fill(h,100,100,70);
//四角形を描画
rect(x, y, w, plotY2-plotY1);
}
}
}
void drawTitle() {
fill(255);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 - 10);
}
void drawYearLabels() {
fill(255);
textSize(10);
textAlign(CENTER);
stroke(224);
strokeWeight(1);
for (int row = 0; row < rowCount; row++) {
if (years[row] % yearInterval == 0) {
float x = map(years[row], yearMin, yearMax, plotX1, plotX2);
text(years[row], x, plotY2 + textAscent() + 10);
//line(x, plotY1, x, plotY2);
}
}
}
void keyPressed() {
if (key == '[') {
currentColumn--;
if (currentColumn < 0) {
currentColumn = columnCount - 1;
}
}
else if (key == ']') {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
応用4:放射状に配置してみる
FloatTable data;
float dataMin, dataMax;
float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;
int rowCount;
int columnCount;
int currentColumn = 0;
int yearMin, yearMax;
int[] years;
int yearInterval = 10;
int volumeInterval = 10;
PFont plotFont;
void setup() {
size(640, 640);
colorMode(HSB,360,100,100,100);
data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();
years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length - 1];
dataMin = 0;
dataMax = data.getTableMax();
plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height - 30;
labelY = height - 30;
plotFont = createFont("SansSerif", 20);
textFont(plotFont);
smooth();
}
void draw() {
background(0);
drawTitle();
stroke(63,127,255,180);
translate(width/2, height/2);
//放射状に情報を表示
drawCirclePlot(currentColumn);
}
//放射状に情報を表示
void drawCirclePlot(int col){
//線の太さを設定
strokeWeight(6);
//線の終端の形状を四角に
strokeCap(SQUARE);
//年を表示するテキストのサイズ
textSize(8);
int rowCount = data.getRowCount();
for (int row = 0; row < rowCount; row++) {
if (data.isValid(row, col)) {
float value = data.getFloat(row, col);
//全ての情報を表示した際にちょうど一周するように角度を算出
//360° = 2π(ラジアン)
float rot = map(years[row], yearMin, yearMax+1, 0.0, 2.0*PI);
float x = 0;
float y = map(value, dataMin, dataMax, 0, width*0.6);
//数値によって色相を変更する
float h = map(value, dataMin, dataMax, 240, 360);
stroke(h,100,100,50);
pushMatrix();
//算出した角度だけ回転
rotate(rot);
//情報の量に応じて、線の長さを変える
line(0, 0, 0, -y);
//10年ごとに年を表示
if(int(years[row])%10 == 0){
text(years[row], -10, -y-8);
}
popMatrix();
}
}
}
void drawTitle() {
fill(255);
textSize(20);
textAlign(LEFT);
String title = data.getColumnName(currentColumn);
text(title, plotX1, plotY1 - 10);
}
void keyPressed() {
if (key == '[') {
currentColumn--;
if (currentColumn < 0) {
currentColumn = columnCount - 1;
}
}
else if (key == ']') {
currentColumn++;
if (currentColumn == columnCount) {
currentColumn = 0;
}
}
}
応用5:アニメーションで変化を表現する
値が変化する様子を、円の大きさがアニメーションすることで表現してみる
FloatTable data;
float dataMin, dataMax;
float plotX1, plotY1;
float plotX2, plotY2;
float labelX, labelY;
int rowCount;
int columnCount;
int yearMin, yearMax;
int[] years;
int yearInterval = 10;
int volumeInterval = 10;
int currentRow = 0;
PFont plotFont;
void setup() {
size(640, 280);
colorMode(HSB,360,100,100,100);
frameRate(10);
data = new FloatTable("milk-tea-coffee.tsv");
rowCount = data.getRowCount();
columnCount = data.getColumnCount();
years = int(data.getRowNames());
yearMin = years[0];
yearMax = years[years.length - 1];
dataMin = 0;
dataMax = data.getTableMax();
plotX1 = 10;
plotX2 = width-10;
labelX = 20;
plotY1 = 30;
plotY2 = height - 30;
labelY = height - 30;
plotFont = createFont("SansSerif", 20);
textFont(plotFont);
smooth();
}
void draw() {
background(0);
noStroke();
//項目名を表示
drawTitles();
//現在の年を表示
drawCurrentYear();
//値の変化をアニメーションで表現
animateEllipsePlot();
}
//値の変化をアニメーションで表現
void animateEllipsePlot(){
//それぞれの列の値を取得
float value0 = data.getFloat(currentRow, 0);
float value1 = data.getFloat(currentRow, 1);
float value2 = data.getFloat(currentRow, 2);
//フォントの設定
textSize(10);
textAlign(CENTER);
//1つ目の円
float r0 = map(value0, dataMin, dataMax, 0, width/3);
fill(0,70,70,70);
ellipse(width/6,height/2,r0,r0);
//円の中心に値を表示
fill(0,0,100,100);
text(r0,width/6,height/2+5);
//2つ目の円
float r1 = map(value1, dataMin, dataMax, 0, width/3);
fill(240,70,70,70);
ellipse(width/2,height/2,r1,r1);
//円の中心に値を表示
fill(0,0,100,100);
text(r1,width/2,height/2+5);
//3つ目の円
float r2 = map(value2, dataMin, dataMax, 0, width/3);
fill(120,70,70,70);
ellipse(width/6*5,height/2,r2,r2);
//円の中心に値を表示
fill(0,0,100,100);
text(r2,width/6*5,height/2+5);
//年を1つ進める
currentRow++;
//もし最後の年まできたら、最初から繰り返す
if(currentRow > years.length-1){
currentRow = 0;
}
}
void drawTitles(){
textSize(12);
textAlign(CENTER);
fill(0,0,100,100);
String title0 = data.getColumnName(0);
text(title0, width/6, height-20);
String title1 = data.getColumnName(1);
text(title1, width/2, height-20);
String title2 = data.getColumnName(2);
text(title2, width/6*5, height-20);
}
void drawCurrentYear(){
textSize(20);
textAlign(LEFT);
fill(0,0,100,100);
int currentYear = years[currentRow];
text(currentYear, 10,24);
}
サンプルファイルのダウンロード
今回の授業の全てのサンプルのソースコードは、下記のリンクよりダウンロードしてください。
Bio Media Art 01 – データの可視化と生成的表現
Bio Media Art 概要
生物と人間の新しいインタラクションの可能性
ボタンやスイッチなどの無機的なものだけなく、植物、動物、人間などの生体やそこからの情報を利用する有機的なインタラクションの可能性を探る。
生体情報を美しくみせる
生体のさまざまな情報や、様々な刺激に対する反応を可視化、可聴化することで、新たな植物(生命)の美や映像音響表現を探求する。
「可視化 (= Visualization)」とは
可視化 = Visualization
人間が直接「見る」ことのできない現象や事象、あるいは関係性を、「見る」ことのできるものにすること
見ることのできるもの → 画像、映像、グラフ、図、表など
視覚化意外の方法
可聴化 – 音によって聴くことができるようにする
可触化 – 触れることによって表現する
参考書:「ビジュアライジングデータ」
ビジュアライジング・データ ―Processingによる情報視覚化手法
Ben Fry (著), 増井 俊之 (監訳) (監修), 加藤 慶彦 (翻訳)
オライリージャパン (2008/12/1)
情報の視覚化の7ステップ
Ben Flyによる、情報視覚化の7ステップ -「ビジュアライジング・データ」より
データ収集 (acquire)
解析 (parse)
フィルタリング (filter)
マイニング (mine)
表現 (reporesent)
精緻化 (refine)
インタラクション (interact)
ただし、いつも、全てのステップが必要というわけではない
データ収集 (acquire)
データを収集する
ディスク上のファイル
ネットワーク上のデータ
この授業では、Arduinoとセンサーを用いて収集した、植物などの生体情報のデータ
解析 (parse)
データの意味をもとに構造を付加する
データをカテゴリに分ける
データの個々の部分の形式が明確になる
フィルタリング (filter)
用途にあわない部分を除去する
データの中で必要とならないレコードを除去
数理モデルをあてはめたり、正規化したりするための数学的な処理
マイニング (mine)
数学的な処理によって、パターンをみつけだす
頻出パターン抽出 – 高頻度で発生する特徴的なパターンを見つける
クラス分類 – 与えられたデータに対応するカテゴリを予測
例:薬品の化合物のデータから,その化合物に薬効がある・ないといったカテゴリを予測
回帰分析 – 与えられたデータに対応する実数値を予測
曜日、降水確率、今日の売上げなどのデータを元に、明日の売上げという実数値データを予測
クラスタリング – データの集合をクラスタと呼ぶグループに分ける
Webの閲覧パターンのデータから、類似したものをまとめることで、閲覧の傾向が同じ利用者のグループを発見する
表現 (reporesent)
精緻化 (refine)
グラフィックデザインの手法をつかって、表現をさらに明快にする
より魅力的な表現へ
色、形
特定のデータに注意を引き付ける工夫 (階層の導入)
インタラクション (interact)
対話的な機能を追加
ユーザによるデータの探索や制御
サブセットの選択
視点の切り替え
月曜の授業では、「表現」「精緻化」「インタラクション」に重点を置いて進めていく予定
様々な可視化の手法に触れる
Visual Complexty – 700以上のプロジェクトを紹介
Similar Diversity
Philipp Steinweber, Andreas Koller
Visualizing information flow in science
Opte Project
The Remotest place on Earth
Joint Research Centre (European Commission) & World Bank
subblue
視覚化の手法の調査
Visual Complexyのサイトから、自分の興味をもった視覚化手法をひとつとりあげてみる
その視覚化の手法の詳細について調べてみる
その手法は、Processingで実現可能か?
生成的な表現
生成的 (Generative) な表現について考える
生成的な表現 (Generative Art)
コンピュータのソフトウェアによるアルゴリズムから生成される表現
ある基本原則や数式やテンプレートなどの素材を設定し、そこに無作為または半無作為のプロセスが作用するよう設定する
生成的な表現の例 1.:ウルフラムの1次元セル・オートマトン
ある特定の時刻(t)における、自分自身とその左右のセルの状態によって、次の時刻(t+1)の状態が決まる
セルのon/offを、0と1で表現すると可能性は8つ
111
110
101
100
011
010
001
000
それぞれの状態での次の値をルールとして定める
ルールの可能性は全部で256個となる (2の8乗、 8bit)
例:ルール90
111
110
101
100
011
010
001
000
0
1
0
1
1
0
1
0
ルール90 = {0, 1, 0, 1, 1, 0, 1, 0}
ルール30 = {0, 0, 0, 1, 1, 1, 1, 0}
ルール110 = {0, 1, 1, 0, 1, 1, 1, 0}
ウルフラムのクラス分類
クラス 1 – 線形系
単一の平衡状態に収束する.つまり,全てのセルが,0 または 1 になってしまう.
クラス 2 – 非線形系 (周期解)
縞模様のような,周期的に無限に繰り返すパターンに収束する.
クラス 3 – 非線形系 (カオス)
単一の状態にも,周期的な状態にも収束せず,カオス的な振舞いをする.
クラス 4 – 非線形系 (その他)
予測しがたい,複雑なパターンで,周期,非周期が交互に現われたり,パターンが消えたりする.
生成的な表現の例 2.:コンウェイのライフゲーム
2次元のCA
ルール
誕生 – 死んでいるセルの周囲に3つの生きているセルがあれば次の世代では生きる(誕生する)。
維持 – 生きているセルの周囲に2つか3つの生きているセルがあれば次の世代でも生き残る。
死亡 – 上以外の場合には次の世代では死ぬ。
Generative Gestaltung
Generative Gestaltungのサンプルをみてみる
4つのライブラリ(Generative Design Lib、controlP5、Geomerative、treemap) のフォルダを、Processingフォルダのlibraryにコピーする
来週までの課題