今回は前半でopenFrameworksにおける3D表現の基本について解説し、後半に3Dを用いてデータを表現するにはどうすれば良いのかについて考えていきます。
このワークショップでは、ここまで主に2次元平面上での描画による表現を扱ってきました。今回は2次元での表現からさらに(文字通り)次元を越えて、3次元空間での表現について考えていきたいと思います。openFrameworksで3Dグラフィクスを扱う手法はこれまでとさほど変化はありません。なぜなら、openFrameworksの描画の基本はOpenGLで行っています。OpenGLはそもそも3次元のグラフィクスの描画のために開発されたライブラリであり、最初から3次元空間をとり扱うための様々な機能が備わっています。しかし、3Dの物体を扱うには、これまでとは違った様々な要素が加わります。カメラ(視点)、ライティング、光と影(シェーディング)、奥行の重なりなどといった2次元の平面には無かった様々な技術や概念の理解が必要となります。
後半は、前半で身に付けた3D表現を用いて、IoT機器で計測中のデータを3Dで表現できないか考えていきます。今回はデータをわかり易く表現するというよりは、データから生成される形態そのもの、「データ彫刻」のようなものを面白く見せるための工夫をしてみます。
スライド資料
サンプルプログラム
今回のサンプルプログラムは、WSのGithubリポジトリのweek10を参照してください。
Sonic Piでプログラミングによる音楽制作を行います。今回は、前回のシンセを使用した音の再生とは別の方法、サンプルを使用した音の再生を取り上げます。サンプルは、サウンドファイルを読み込んで指定したタイミングで音を再生します。前回のシンセがシンセサイザーとすると、サンプルはサンプラーに相当します。演奏するタイミングだけでなく、音量、定位、再生スピードなど様々なパラメータを設定可能です。また、Sonic Piにあらかじめ用意されたサンプルだけでなく、ファイルの場所を指定して外部ファイルを読み込むことも可能です。
今回はさらに、Sonic Piにおけるランダム (乱数) の生成とその使用方法について解説します。Sonic Piでは、単純な乱数だけでなく、指定した範囲の乱数 (rrand, rrand_i)、リストの中からランダムに選択(choose)、サイコロをふってその結果で選択(dice) など様々なランダム化の関数が用意されています。音楽への応用を考えながら、乱数を使用していきます。
スライド資料
課題: 乱数を利用して旋律を作る
- Sonic Piのランダムに関する機能(chooseを含む)を使用してループする旋律(メロディー)を作曲してください
課題の提出方法
- emailで提出
- To : tadokoro+teu17@gmail.com
- Subject: 課題6
- 作成したプログラムのソースコードを本文にカット&ペースト
- 本文に以下の内容を記入して提出
- 学籍番号
- 氏名
- 締切: 次回の講義開始時間 (7月3日 16:45) まで!
今回は、前回の調査で希望者の多かったTidalCyclesを使用したライブコーディングについてとりあげます。
ライブコーディングとは、プログラムをリアルタイムに実行しながらコーディングする行為自体をパフォーマンスするジャンルです。主に、音楽や映像を生成することが多いですが、Webや詩などその適用範囲は拡がっています。
TidalCyclesは、こうしたライブコーディングの環境の中でも、リズムパターンの生成に特化した環境となっています。シンプルな文法で複雑なリズムパターンをリアルタイムに生成し変奏していくことが可能となっています。Mac OS X、Linux、Windowsで動きます。
前半で、そもそもライブコーディングとは何なのか、その成り立ちと様々な応用について紹介します。後半は、実際にTidalCyclesを使用してライブコーディングのパフォーマンスに挑戦します。
TidalCyclesのインストール
TidalCyclesのインストールはとても時間がかかります。あらかじめ以下の環境をインストールしておくことを強くおすすめします。
Mac
Mac (macOS) 用に、インストールの自動化プログラムが公開されています。
Homebrewのインストール
インストール自動化スクリプトを実行するにはHomebrewというパッケージ管理システムが必要です。以下のコマンドをターミナルで実行します。
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
自動化スクリプトの実行
tidal-bootstrap/のページから「Download Zip」ボタンを押してスクリプト一式をダウンロードして、フォルダーの中にある「tidal-bootstrap.command」ダブルクリックします。するとターミナルが起動してインストールが開始します。ターミナルに表示される指示に従ってインストールを行います。全ての処理が完了すると、以下のように表示されます。
[Process completed]
これで、TidalCyclesが使用できるようになります!
Windows
Windowsは手動でインストールする必要があります。
基本環境のインストール
まず以下の言語とアプリケーションをひとつずつインストールします。
TidalCyclesのインストール
以下のコマンドをPowerShellもしくはCommandプロンプトから入力します。
cabal install tidal
SuperDirtのインストール
SuperColliderを起動して、以下のコードを入力します。
include("SuperDirt")
しばらくインストールが続きます(気長に待ちましょう)。完了すると以下の表示がコンソールに出力されます。
... the class library may have to be recompiled.
-> SuperDirt
TidalCycles Atom pluginのインストール
TidalCycles Atom pluginをインストールします。Atomを起動してedit > settings > installメニューに進みます。検索欄に「tidalcycles」と入力すると、プラグインが表示されるので、「Install」ボタンを押してインストールします。
これで完了です!
スライド資料
「メディア芸術の基礎」の前半では、主にProcessingを用いたビジュアルプログラミングを行ってきました。後半はまた新たな内容をとりあげていきます。
後半の講義では、音響や音楽をプログラミングを用いて処理し表現を行います。サウンドを扱うための開発環境として「Sonic Pi」を使用します。Sonic piは、教育現場でのプログラミングや音楽の授業をサポートするように設計された、ライブコーディング可能な無料のサウンドプログラミング開発環境です。「カノンからダブステップまで」というキャッチフレーズに代表されるように、古典〜現代の音楽を作曲できる、Mac OS XやWindows、さらにはRaspberry Piでも動かすことが可能で、柔軟なプログラミングが可能です。
今回は、Sonic Piの入門として、インストールから操作方法、そしてプログラミングの基本を学んでいきます。
スライド資料
課題: Sonic Piで旋律のループを作る
- Sonic Piを使用してループする旋律(メロディー)を作曲してください
- 和音やサンプル音など授業内で扱わなかったプログラミングの手法は自由に使用して構いません
課題の提出方法
- emailで提出
- To : tadokoro+teu17@gmail.com
- Subject: 課題5
- 作成したプログラムのソースコードを本文にカット&ペースト
- 本文に以下の内容を記入して提出
- 学籍番号
- 氏名
- 締切: 次回の講義開始時間 (6月26日 16:45) まで!
今回は、ネットワークを活用したサンプルを紹介します。Open Sound Control(OSC)というプロトコルを使用して、アプリケーション同士をネットワークを介して通信する方法について解説します。これにより、Processingのスケッチ間で通信したり、複数のユーザが1つのスケッチを遠隔操作することが可能となります。またProessingのスケッチを他のアプリケーション、例えば、Sonic Pi、Max/MSPやPd、SuperColliderなどの音楽アプリケーション、さらにはTouchOSCといったiPhoneアプリなどからコントロールすることが出来るようになります。
Sonic Piのダウンロードは下記のURLから行ってください。
スライド資料
サンプルプログラム
01 OSC基本、送信側
//OSC関連のライブラリーをインポート
import oscP5.*;
import netP5.*;
//OSCP5クラスのインスタンス
OscP5 oscP5;
//OSC送出先のネットアドレス
NetAddress myRemoteLocation;
void setup() {
size(800,600);
frameRate(60);
//ポートを12000に設定して新規にOSCP5のインスタンスを生成
oscP5 = new OscP5(this,12000);
//OSC送信先のIPアドレスとポートを指定
myRemoteLocation = new NetAddress("127.0.0.1",12000);
}
void draw() {
if(mousePressed){
background(255, 0, 0);
} else {
background(0);
}
//マウスの場所に円を描く
noFill();
stroke(255);
ellipse(mouseX, mouseY, 10, 10);
//現在のマウスの位置をOSCで送出
//新規にメッセージ作成
OscMessage msg = new OscMessage("/mouse/position");
msg.add(mouseX); //X座標の位置を追加
msg.add(mouseY); //Y座標の位置を追加
//OSCメッセージ送信
oscP5.send(msg, myRemoteLocation);
}
//マウスボタンを押した時にメッセージを送信
void mousePressed(){
OscMessage msg = new OscMessage("/mouse/cliked");
msg.add(1); //1を送信
//OSCメッセージ送信
oscP5.send(msg, myRemoteLocation);
}
//マウスボタンを離した時にメッセージを送信
void mouseReleased(){
OscMessage msg = new OscMessage("/mouse/cliked");
msg.add(0); //0を送信
//OSCメッセージ送信
oscP5.send(msg, myRemoteLocation);
}
02 OSC基本、受信側
import oscP5.*;
import netP5.*;
//OSCP5クラスのインスタンス
OscP5 oscP5;
//マウスの位置ベクトル
PVector mouseLoc;
//マウスのクリック検知
int clicked;
void setup() {
size(800,600);
frameRate(60);
//ポートを12000に設定して新規にOSCP5のインスタンスを生成
oscP5 = new OscP5(this,12000);
//マウスの位置ベクトルを初期化
mouseLoc = new PVector(width/2, height/2);
//マウスのクリック状態を初期化
clicked = 0;
}
void draw() {
if(clicked == 1){
background(255, 0, 0);
} else {
background(0);
}
//OSCで指定された座標に円を描く
noFill();
stroke(255);
ellipse(mouseLoc.x, mouseLoc.y, 10, 10);
}
//OSCメッセージを受信した際に実行するイベント
void oscEvent(OscMessage msg) {
//もしOSCメッセージが /mouse/position だったら
if(msg.checkAddrPattern("/mouse/position")==true) {
//最初の値をint方としてX座標に
mouseLoc.x = msg.get(0).intValue();
//次の値をint方としてY座標に
mouseLoc.y = msg.get(1).intValue();
}
if(msg.checkAddrPattern("/mouse/cliked")==true) {
//Bool値を読み込み
clicked = msg.get(0).intValue();
println("msg = " + clicked);
print("*");
}
}
03 OSC受信応用 – 拡がる波紋
import oscP5.*;
import netP5.*;
//OSCP5クラスのインスタンス
OscP5 oscP5;
//マウスの位置ベクトル
PVector mouseLoc;
//Ringクラスのリスト
ArrayList ringList = new ArrayList();
void setup() {
size(800, 600);
frameRate(60);
//ポートを12000に設定して新規にOSCP5のインスタンスを生成
oscP5 = new OscP5(this,12000);
//マウスの位置ベクトルを初期化
mouseLoc = new PVector(width/2, height/2);
}
void draw() {
background(0);
noFill();
stroke(255);
strokeWeight(1);
ellipse(mouseLoc.x, mouseLoc.y, 10, 10);
//リストに格納されたマウスの位置を全て描画する
strokeWeight(2);
for (int i = 0; i < ringList.size(); i++) {
ringList.get(i).draw();
}
}
//OSCメッセージを受信した際に実行するイベント
void oscEvent(OscMessage msg) {
//もしOSCメッセージが /mouse/position だったら
if (msg.checkAddrPattern("/mouse/position")==true) {
//最初の値をint方としてX座標に
mouseLoc.x = msg.get(0).intValue();
//次の値をint方としてY座標に
mouseLoc.y = msg.get(1).intValue();
}
if (msg.checkAddrPattern("/mouse/cliked")==true) {
//もしマウスがクリックされたメッセージを受けとったら
if (msg.get(0).intValue() == 1) {
//マウスの位置のリストに新規に現在の位置を追加する
ringList.add(new Ring(mouseLoc.x, mouseLoc.y));
}
}
}
class Ring {
PVector location;
float radius;
float speed;
float alpha;
float alphaSpeed;
float release;
Ring(PVector _location) {
location = new PVector();
location = _location;
radius = 20;
speed = 1.0;
alpha = 255;
alphaSpeed = 1.0;
}
void draw() {
alphaSpeed = 255.0/(release*frameRate);
fill(63, 127, 255, alpha);
noStroke();
pushMatrix();
translate(location.x, location.y);
ellipse(0, 0, radius, radius);
popMatrix();
radius += speed;
alpha -= alphaSpeed;
strokeWeight(1.0);
}
}
04 Sonic PiへOSCを送信 - 基本
import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress location;
void setup() {
size(400, 400);
frameRate(60);
//OSCのセットアップ
oscP5 = new OscP5(this, 12000);
//SonicPiの受信ポート4557に送信
location = new NetAddress("127.0.0.1", 4557);
}
void draw() {
}
void mousePressed() {
//ド(C)の音を演奏させる
OscMessage msg = new OscMessage("/run-code");
msg.add("fromP5");
msg.add("play 60");
oscP5.send(msg, location);
println(msg);
}
05 Sonic PiへOSC送信 - マウスで音程とパンを操作
import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress location;
void setup() {
size(640, 480);
//OSCのセットアップ
oscP5 = new OscP5(this, 12000);
//SonicPiの受信ポート4557に送信
location = new NetAddress("127.0.0.1", 4557);
background(0);
}
void draw() {
}
void mousePressed() {
//マウスのX座標で定位(パン)を決定
float pan = map(mouseX, 0, width, -1, 1);
//マウスのY座標でノートナンバーを決定
float note = map(mouseY, 0, width, 80, 50);
//OSCメッセージを送信
OscMessage msg = new OscMessage("/run-code");
msg.add("fromP5");
msg.add("play "+ note + ", pan: "+ pan);
oscP5.send(msg, location);
}
06 Sonic PiへOSC送信 - 表示の工夫
import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress location;
float note; //ノート番号
float pan; //パン
void setup() {
size(640, 480);
oscP5 = new OscP5(this, 12000);
location = new NetAddress("127.0.0.1", 4557);
}
void draw() {
background(0);
stroke(255);
//マウスの位置を表示
line(mouseX, 0, mouseX, height);
line(0, mouseY, width, mouseY);
ellipse(mouseX, mouseY, 10, 10);
//マウスの位置から、ノート番号とパンを決定
note = map(mouseY, 0, width, 80, 50);
pan = map(mouseX, 0, width, -1, 1);
//テキストで表示
text("note = " + note + ", pan = " + pan, mouseX + 10, mouseY-10);
}
void mousePressed() {
//OSCを送信
OscMessage msg = new OscMessage("/run-code");
msg.add("fromP5");
msg.add("play "+ note + ", pan: "+ pan);
oscP5.send(msg, location);
println(msg);
}
07 Sonic PiへOSC送信 - 自動演奏
import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress location;
float note;
float pan;
void setup() {
size(640, 480);
frameRate(60);
oscP5 = new OscP5(this, 12000);
location = new NetAddress("127.0.0.1", 4557);
}
void draw() {
background(0);
int speed = 8;
//8フレームに一度実行
if (frameCount % speed == 0) {
//ランダムなノート番号とパンを設定
note = random(50, 80);
pan = random(-1, 1);
//OSCを送信
OscMessage msg = new OscMessage("/run-code");
msg.add("fromP5"+frameCount);
msg.add("play "+ note + ", pan: "+ pan);
oscP5.send(msg, location);
println(msg);
}
}
08 Sonic PiへOSC送信 - 自動演奏 + ビジュアライズ
import oscP5.*;
import netP5.*;
OscP5 oscP5;
NetAddress location;
float note;
float pan;
ArrayList< Ring > rings;
int MAX = 16;
void setup() {
size(800, 600);
frameRate(60);
oscP5 = new OscP5(this, 12000);
location = new NetAddress("127.0.0.1", 4557);
rings = new ArrayList< Ring >();
}
void draw() {
background(0);
int speed = 8;
if (frameCount % speed == 0) {
note = int(random(50, 80));
pan = random(-1, 1);
OscMessage msg
= new OscMessage("/run-code");
msg.add("fromP5"+frameCount);
msg.add("play "+ note + ", pan: "+ pan);
oscP5.send(msg, location);
println(msg);
float x = map(pan, -1, 1,
width/4, width/4*3);
float y = map(note, 40, 90, height, 0);
PVector location = new PVector(x, y);
Ring r = new Ring(location);
r.release = 3.0;
rings.add(r);
}
for (int i = 0; i < rings.size(); i++) {
rings.get(i).draw();
}
if (rings.size() > MAX) {
rings.remove(0);
}
}
class Ring {
PVector location;
float radius;
float speed;
float alpha;
float alphaSpeed;
float release;
Ring(PVector _location) {
location = new PVector();
location = _location;
radius = 20;
speed = 1.0;
alpha = 255;
alphaSpeed = 1.0;
}
void draw() {
alphaSpeed = 255.0/(release*frameRate);
fill(63, 127, 255, alpha);
noStroke();
pushMatrix();
translate(location.x, location.y);
ellipse(0, 0, radius, radius);
popMatrix();
radius += speed;
alpha -= alphaSpeed;
strokeWeight(1.0);
}
}
今回は、作品制作に直接役に立ちそうな技術として、映像(カメラ)を用いたインタラクションについて取り上げます。
メディアアート作品では、カメラから取得した映像を用いてインタラクションを行う事例が沢山存在しています。映像を使ったインタラクションは、特別なセンサーを使用することなく、また鑑賞者に直接接触することなく高度なインタラクションが可能となり、多くの可能性を秘めた手法です。また、近年では映像の中から物体を認識したり、映像の中の微妙な差分や動きを検出したりといった、コンピュータ・ビジョン(Computer Vision = CV)の技術が発展し、高度な映像解析が活用できるようになりました。今回は、こうしたCVの技術の中でもオープンソースで多くの利用実績のあるOpenCVというCVのためのAddonのofxOpenCvと、ofxCvをopenFrameworksで活用する方法について紹介していきます。
さらに、OpenCVを活用した応用例として、フェイストラッキングの技術を使っていろいろ実験していきます。フェイストラッキングとは、カメラから入力した映像の中から人間の顔を検知して、その傾きや大きさ、さらには目や鼻、口、眉といった顔のそれぞれのパーツの位置や大きさを立体的に検知することのできる画像解析の技術です。この技術を利用して、顔の表情でプログラムをコントロールしたり、自分の顔に他人の顔を合成したりと様々な応用が可能となります。今回は、このフェイストラッキングに必要な環境設定とビルドの方法を解説した上で、その応用方法について考えていきます。
スライド資料
サンプルプログラム
今回のサンプルプログラムは、WSのGithubリポジトリのweek08を参照してください。
前回に引き続き、Processingにおけるオブジェクト指向プログラミング(OOP)の方法について解説していきます。今回はOOPの重要な概念の一つである「継承 (インヘリタンス) 」について考えていきます。継承とは、既存クラスの機能構造を共有する新たなクラス(サブクラス)を派生させることです。サブクラスからは親となるクラスのプロパティやメソッドをそのまま引き継ぐことが可能です。この継承の仕組みを効果的に活用することで、既存のクラスを再利用しながらそこに新たな機能を加えていくということが可能となります。
次回までにやってくること
次回からは、新たな内容に入ります。これまではProcessingを用いて主に視覚的な表現を扱ってきました。次回からは、Sonic Piという開発環境を用いて音や音楽を扱います。
次回までに以下のサイトからSonic Piの最新版を使用しているOSにあわせてダウンロードし、インストールしておいてください。
スライド資料
サンプルプログラム
上下左右に動きまわる円 (基本テンプレート)
// Spotクラスを宣言
Spot spot;
void setup() {
size(800, 600, P2D);
noStroke();
frameRate(60);
//位置ベクトルをランダムに生成
PVector loc = new PVector(width/2.0, height/2.0);
//速度ベクトルをランダムに生成
PVector vec = new PVector(random(-4, 4), random(-4, 4));
//インスタンス化して配列に格納
spot = new Spot(loc, vec, random(5, 30));
//背景を黒に
background(0);
}
void draw() {
// 画面をフェードさせる
blendMode(BLEND);
fill(0, 0, 0, 10);
rect(0, 0, width, height);
// 色を加算合成に
blendMode(ADD);
// 円の色を設定
fill(31, 127, 255, 127);
// Spotクラスのmove()メソッドを呼び出す
spot.move();
// Spotクラスのdraw()メソッドを呼び出す
spot.draw();
}
// Spotクラス
class Spot {
// プロパティ
PVector location; //位置 (ベクトル!)
PVector velocity; //速度 (ベクトル!)
float diameter; //直径
// コンストラクター
Spot(PVector _location, PVector _velocity, float _diameter) {
location = _location;
diameter = _diameter;
velocity = _velocity;
}
// 移動
void move() {
//位置ベクトル + 速度ベクトル = 次フレーム位置ベクトル
location.add(velocity);
//左右の壁でバウンドさせる
if (location.x < diameter / 2 || location.x > width - diameter / 2) {
location.x = constrain(location.x, diameter/2, width - diameter / 2);
velocity.x *= -1;
}
//上下の壁でバウンドさせる
if (location.y < diameter / 2 || location.y > height - diameter / 2) {
location.y = constrain(location.y, diameter/2, height - diameter / 2);
velocity.y *= -1;
}
}
// 描画
void draw() {
ellipse(location.x, location.y, diameter, diameter);
}
}
クラスの配列 – 大量の物体を同時に動かす
// 物体の数
int numSpots = 400;
// Spotクラスを配列として宣言
Spot[] spots = new Spot[numSpots];
void setup() {
size(800, 600, P2D);
noStroke();
frameRate(60);
// 配列の数だけSpotクラスをインスタンス化
for (int i = 0; i < spots.length; i++) {
//位置ベクトルをランダムに生成
PVector loc = new PVector(width/2.0, height/2.0);
//速度ベクトルをランダムに生成
PVector vec = new PVector(random(-4, 4), random(-4, 4));
//インスタンス化して配列に格納
spots[i] = new Spot(loc, vec, random(5, 30));
}
background(0);
}
void draw() {
// 画面をフェードさせる
blendMode(BLEND);
fill(0, 0, 0, 10);
rect(0, 0, width, height);
// 色を加算合成に
blendMode(ADD);
// 円の色を設定
fill(31, 127, 255, 63);
for (int i = 0; i < spots.length; i++) {
// Spotクラスのmove()メソッドを呼び出す
spots[i].move();
// Spotクラスのdraw()メソッドを呼び出す
spots[i].draw();
}
}
// Spotクラス
class Spot {
// プロパティ
PVector location; //位置 (ベクトル!)
PVector velocity; //速度 (ベクトル!)
float diameter; //直径
// コンストラクター
Spot(PVector _location, PVector _velocity, float _diameter) {
location = _location;
diameter = _diameter;
velocity = _velocity;
}
// 移動
void move() {
//位置ベクトル + 速度ベクトル = 次フレーム位置ベクトル
location.add(velocity);
//左右の壁でバウンドさせる
if (location.x < diameter / 2 || location.x > width - diameter / 2) {
location.x = constrain(location.x, diameter/2, width - diameter / 2);
velocity.x *= -1;
}
//上下の壁でバウンドさせる
if (location.y < diameter / 2 || location.y > height - diameter / 2) {
location.y = constrain(location.y, diameter/2, height - diameter / 2);
velocity.y *= -1;
}
}
// 描画
void draw() {
ellipse(location.x, location.y, diameter, diameter);
}
}
クラスの継承 – 重力を付加
// 物体の数
int numSpots = 400;
// Spotクラスを配列として宣言
GravitySpot[] spots = new GravitySpot[numSpots];
void setup() {
size(800, 600, P2D);
noStroke();
frameRate(60);
// 配列の数だけSpotクラスをインスタンス化
for (int i = 0; i < spots.length; i++) {
//位置ベクトルをランダムに生成
PVector loc = new PVector(width/2.0, height/2.0);
//速度ベクトルをランダムに生成
PVector vec = new PVector(random(-4, 4), random(-4, 4), 1.0);
//重力を0.1に設定
float gravity = 0.1;
//インスタンス化して配列に格納
spots[i] = new GravitySpot(loc, vec, random(5, 30), gravity);
}
background(0);
}
void draw() {
// 画面をフェードさせる
blendMode(BLEND);
fill(0, 0, 0, 10);
rect(0, 0, width, height);
// 色を加算合成に
blendMode(ADD);
// 円の色を設定
fill(31, 127, 255, 63);
for (int i = 0; i < spots.length; i++) {
// Spotクラスのmove()メソッドを呼び出す
spots[i].move();
// Spotクラスのdraw()メソッドを呼び出す
spots[i].draw();
}
}
// Spotクラス
class Spot {
// プロパティ
PVector location; //位置 (ベクトル!)
PVector velocity; //速度 (ベクトル!)
float diameter; //直径
// コンストラクター
Spot(PVector _location, PVector _velocity, float _diameter) {
location = _location;
diameter = _diameter;
velocity = _velocity;
}
// 移動
void move() {
//位置ベクトル + 速度ベクトル = 次フレーム位置ベクトル
location.add(velocity);
//左右の壁でバウンドさせる
if (location.x < diameter / 2 || location.x > width - diameter / 2) {
location.x = constrain(location.x, diameter/2, width - diameter / 2);
velocity.x *= -1;
}
//上下の壁でバウンドさせる
if (location.y < diameter / 2 || location.y > height - diameter / 2) {
location.y = constrain(location.y, diameter/2, height - diameter / 2);
velocity.y *= -1;
}
}
// 描画
void draw() {
ellipse(location.x, location.y, diameter, diameter);
}
}
//Spotを継承した、GravitySpot(重力を付加)
class GravitySpot extends Spot {
float gravity; //重力
//コンストラクター
GravitySpot(PVector _location, PVector _velocity, float _diameter, float _gravity) {
//Superクラス(Spotクラス)をインスタンス化
super(_location, _velocity, _diameter);
//重力を設定
gravity = _gravity;
}
void move() {
//速度に重力を付加
velocity.y += gravity;
//スーパークラスのmove()を呼び出し
super.move();
}
void draw() {
//スーパークラスのdraw()を呼び出し
super.draw();
}
}
クラスの継承 – 重力と摩擦力を付加
// 物体の数
int numSpots = 400;
// Spotクラスを配列として宣言
GravitySpot[] spots = new GravitySpot[numSpots];
void setup() {
size(800, 600);
noStroke();
frameRate(60);
// 配列の数だけSpotクラスをインスタンス化
for (int i = 0; i < spots.length; i++) {
//位置ベクトルをランダムに生成
PVector loc = new PVector(width/2.0, height/2.0);
//速度ベクトルをランダムに生成
PVector vec = new PVector(random(-4, 4), random(-4, 4), 1.0);
//円の大きさを5から30に
float diameter = random(5, 30);
//重力を0.1に設定
float gravity = 0.1;
//摩擦力を0.998に
float friction = 0.998;
//インスタンス化して配列に格納
spots[i] = new GravitySpot(loc, vec, diameter, gravity, friction);
}
background(0);
}
void draw() {
// 画面をフェードさせる
blendMode(BLEND);
fill(0, 0, 0, 10);
rect(0, 0, width, height);
// 色を加算合成に
blendMode(ADD);
// 円の色を設定
fill(31, 127, 255, 63);
for (int i = 0; i < spots.length; i++) {
// Spotクラスのmove()メソッドを呼び出す
spots[i].move();
// Spotクラスのdraw()メソッドを呼び出す
spots[i].draw();
}
}
// Spotクラス
class Spot {
// プロパティ
PVector location; //位置 (ベクトル!)
PVector velocity; //速度 (ベクトル!)
float diameter; //直径
// コンストラクター
Spot(PVector _location, PVector _velocity, float _diameter) {
location = _location;
diameter = _diameter;
velocity = _velocity;
}
// 移動
void move() {
//位置ベクトル + 速度ベクトル = 次フレーム位置ベクトル
location.add(velocity);
//左右の壁でバウンドさせる
if (location.x < diameter / 2 || location.x > width - diameter / 2) {
location.x = constrain(location.x, diameter/2, width - diameter / 2);
velocity.x *= -1;
}
//上下の壁でバウンドさせる
if (location.y < diameter / 2 || location.y > height - diameter / 2) {
location.y = constrain(location.y, diameter/2, height - diameter / 2);
velocity.y *= -1;
}
}
// 描画
void draw() {
ellipse(location.x, location.y, diameter, diameter);
}
}
//Spotを継承した、GravitySpot(重力と摩擦を付加)
class GravitySpot extends Spot {
float gravity; //重力
float friction; //摩擦力
//コンストラクター
GravitySpot(PVector _location,
PVector _velocity,
float _diameter,
float _gravity,
float _friction) {
//Superクラス(Spotクラス)をインスタンス化
super(_location, _velocity, _diameter);
//重力を設定
gravity = _gravity;
//摩擦力を設定
friction = _friction;
}
void move() {
//速度に摩擦力を掛ける
velocity.mult(friction);
//速度に重力を付加
velocity.y += gravity;
//スーパークラスのmove()を呼び出し
super.move();
}
void draw() {
//スーパークラスのdraw()を呼び出し
super.draw();
}
}
これまでは、主にProcessingの基礎をステップバイステップで取り上げてきましたが、ここから数回は作品制作に直接役に立ちそうな実践的なプログラミングテクニックを紹介します。今回は、その第一回目として、映像を使ったインタラクションについて取り上げます。
Processingを使用してインタラクティブな機能を実現するための手段は、センサーを使う方法や、KinectやLeap Motionなどのデバイスを使用する方法などいろいろ考えられます。今回は、最もシンプルな機材構成で可能な方法として、カメラの映像を解析してそこから動きや物体の輪郭を取り出す手法について取り上げます。
コンピュータで、映像から実世界の情報を取得して認識するための研究で「コンピュータ・ビジョン (Conputer Vision)」という分野が存在します。わかりやすく言うなら「ロボットの目」をつくるような研究です。このコンピュータ・ビジョンの様々な成果をオープンソースで公開しているOpenCVというライブラリーがあります。今回は、このOpenCVをProcessingで使用できるようにした、OpenCV for Processingライブラリーを使用したプログラミングを体験します。
スライド資料
サンプルプログラム
カメラキャプチャー基本
import gab.opencv.*;
import processing.video.*;
Capture cam; // ライブカメラ
void setup() {
//初期設定
size(640, 480); //画面サイズ
//キャプチャーするカメラのサイズ
cam = new Capture(this, 640, 480);
//キャプチャー開始
cam.start();
}
void draw() {
//カメラ画像を表示
image(cam, 0, 0 );
}
//キャプチャーイベント
void captureEvent(Capture c) {
cam.read();
}
ビデオ映像をピクセレイト
// ビデオライブラリのインポート
import processing.video.*;
Capture cam; // ムービープレイヤの宣言
boolean playing; // ムービーを再生しているか否か
void setup() {
// 画面初期設定
size(853, 480);
frameRate(60);
noStroke();
cam = new Capture(this); // カメラを初期化する
cam.start(); // キャプチャを開始する
}
void draw() {
background(0);
int skip = 20; // 色をピックアップする間隔を設定する
// 設定した間隔で画面をスキャン
for (int j = skip/2; j < height; j += skip) {
for (int i = skip/2; i < width; i += skip) {
// 指定した座標の色を読み込む
color col = cam.get(i, j);
// 明るさを抽出する
float br = brightness(col);
fill(col); // 塗りの色を設定する
// 明るさをサイズにして円を描く
ellipse(i, j, skip * br / 255.0, skip * br / 255.0);
}
}
}
// カメラのフレームが更新されたらイベントを実行する
void captureEvent(Capture c) {
cam.read();
}
OpenCV輪郭抽出
import gab.opencv.*;
import processing.video.*;
Capture cam; // ライブカメラ
OpenCV opencv; // OpenCV
ArrayList < Contour > contours; //輪郭の配列
void setup() {
//初期設定
size(640, 480); //画面サイズ
//キャプチャーするカメラのサイズ
cam = new Capture(this, 640, 480);
//OpenCVの画面サイズ
opencv = new OpenCV(this, 640, 480);
//キャプチャー開始
cam.start();
}
void draw() {
//カメラの画像をOpenCVに読み込み
opencv.loadImage(cam);
//カメラ画像を表示
image(cam, 0, 0 );
//閾値の設定(マウスのX座標で変化)
int threshold = int(map(mouseX, 0, width, 0, 100));
opencv.threshold(threshold);
//輪郭抽出
contours = opencv.findContours();
//描画設定
noFill();
strokeWeight(1);
//検出された輪郭の数だけ、輪郭線を描く
for (int i = 0; i < contours.size(); i++) {
stroke(0, 255, 0);
contours.get(i).draw();
}
}
//キャプチャーイベント
void captureEvent(Capture c) {
cam.read();
}
OpenCVによる顔検出
import gab.opencv.*;
import processing.video.*;
import java.awt.*;
Capture cam; //ビデオキャプチャー
OpenCV opencv; //OpenCV
void setup() {
size(640, 480);
//ビデオキャプチャー初期化
cam = new Capture(this, 640/2, 480/2);
//OpenCV初期化(ビデオキャプチャーの半分のサイズ)
opencv = new OpenCV(this, 640/2, 480/2);
//顔の学習データを読み込み
opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
//ビデオキャプチャー開始
cam.start();
}
void draw() {
//二倍サイズで表示
scale(2);
//画像を読み込み
opencv.loadImage(cam);
//カメラ画像を描画
image(cam, 0, 0 );
//顔を検出
Rectangle[] faces = opencv.detect();
//検出した顔の周囲を四角形で描画
noFill();
stroke(0, 255, 0);
strokeWeight(3);
for (int i = 0; i < faces.length; i++) {
rect(faces[i].x, faces[i].y, faces[i].width, faces[i].height);
}
}
//キャプチャーイベント
void captureEvent(Capture c) {
cam.read();
}
OpenCVによる顔検出 – 目線を入れてみる
import gab.opencv.*;
import processing.video.*;
import java.awt.*;
Capture cam; //ビデオキャプチャー
OpenCV opencv; //OpenCV
void setup() {
size(640, 480);
//ビデオキャプチャー初期化
cam = new Capture(this, 640/2, 480/2);
//OpenCV初期化(ビデオキャプチャーの半分のサイズ)
opencv = new OpenCV(this, 640/2, 480/2);
//顔の学習データを読み込み
opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);
//ビデオキャプチャー開始
cam.start();
}
void draw() {
//二倍サイズで表示
scale(2);
//画像を読み込み
opencv.loadImage(cam);
//カメラ画像を描画
image(cam, 0, 0 );
//顔を検出
Rectangle[] faces = opencv.detect();
//検出した顔の周囲を四角形で描画
fill(0);
for (int i = 0; i < faces.length; i++) {
//ちょうど目の場所にくるよう、場所とサイズを調整
float x = faces[i].x + faces[i].width * 0.15;
float y = faces[i].y + faces[i].height * 0.3;
float width = faces[i].width * 0.7;
float height = faces[i].height * 0.15;
//目線を描画
rect(x, y, width, height);
}
}
//キャプチャーイベント
void captureEvent(Capture c) {
c.read();
}
Optical Flowの描画
import gab.opencv.*;
import processing.video.*;
Capture cam; // ライブカメラ
OpenCV opencv; // OpenCV
void setup() {
//初期設定
size(640, 480); //画面サイズ
//キャプチャーするカメラのサイズ
cam = new Capture(this, 640/2, 480/2);
//OpenCVの画面サイズ
opencv = new OpenCV(this, 640/2, 480/2);
//キャプチャー開始
cam.start();
}
void draw() {
//描画スケール設定
scale(2.0);
//カメラの画像をOpenCVに読み込み
opencv.loadImage(cam);
//カメラ画像を表示
image(cam, 0, 0 );
//OpticalFlowを計算
opencv.calculateOpticalFlow();
//描画設定
stroke(255,0,0);
//OpticalFlowを描画
opencv.drawOpticalFlow();
}
//キャプチャーイベント
void captureEvent(Capture c) {
c.read();
}
OpticalFlow + Particle
import gab.opencv.*;
import processing.video.*;
Capture cam; // ライブカメラ
OpenCV opencv; // OpenCV
int NUM = 500;
ParticleVec3[] particles = new ParticleVec3[NUM];
void setup() {
//初期設定
size(640, 480, P3D); //画面サイズ
//キャプチャーするカメラのサイズ
cam = new Capture(this, 640/2, 480/2);
//OpenCVの画面サイズ
opencv = new OpenCV(this, 640/2, 480/2);
//キャプチャー開始
cam.start();
for (int i = 0; i < NUM; i++) {
particles[i] = new ParticleVec3();
particles[i].radius = 4.0;
particles[i].position.set(random(width/2), random(height/2), 0);
particles[i].minx = 0;
particles[i].miny = 0;
particles[i].maxx = width/2;
particles[i].maxy = height/2;
}
}
void draw() {
background(0);
blendMode(ADD);
//描画スケール設定
scale(2.0);
//カメラの画像をOpenCVに読み込み
opencv.loadImage(cam);
//カメラ画像を表示
//image(cam, 0, 0 );
//OpticalFlowを計算
opencv.calculateOpticalFlow();
//描画設定
stroke(255, 0, 0);
//OpticalFlowを描画
opencv.drawOpticalFlow();
noStroke();
fill(0, 127, 255);
for (int i = 0; i < NUM; i++) {
//パーティクルの位置を更新
particles[i].update();
//パーティクルを描画
particles[i].draw();
//画面の端で反対側から出現するように
particles[i].throughOffWalls();
//OpticalFlowから力を算出してパーティクルに反映する
if (particles[i].position.x > 0
&& particles[i].position.x < width/2
&& particles[i].position.y > 0
&& particles[i].position.y < height/2 ) {
PVector vec = opencv.getFlowAt(int(particles[i].position.x),
int(particles[i].position.y));
particles[i].addForce(vec.mult(0.1));
}
}
}
//キャプチャーイベント
void captureEvent(Capture c) {
c.read();
}
class ParticleVec3 {
PVector position;
PVector velocity;
PVector acceleration;
float friction;
float radius;
float mass;
float minx, miny, minz;
float maxx, maxy, maxz;
ParticleVec3() {
radius = 4.0;
friction = 0.01;
mass = 1.0;
position = new PVector(width/2.0, height/2.0, 0);
velocity = new PVector(0, 0, 0);
acceleration = new PVector(0, 0, 0);
minx = 0;
miny = 0;
minz = -height;
maxx = width;
maxy = height;
maxz = height;
}
void update() {
velocity.add(acceleration);
velocity.mult(1.0 - friction);
position.add(velocity);
acceleration.set(0, 0, 0);
}
void draw() {
pushMatrix();
translate(position.x, position.y, position.z);
ellipse(0, 0, radius * 2, radius * 2);
popMatrix();
}
void addForce(PVector force) {
force.div(mass);
acceleration.add(force);
}
void bounceOffWalls() {
if (position.x > maxx) {
position.x = maxx;
velocity.x *= -1;
}
if (position.x < minx) {
position.x = minx;
velocity.x *= -1;
}
if (position.y > maxy) {
position.y = maxy;
velocity.y *= -1;
}
if (position.y < miny) {
position.y = miny;
velocity.y *= -1;
}
if (position.z > maxz) {
position.z = maxz;
velocity.z *= -1;
}
if (position.z < minz) {
position.z = minz;
velocity.z *= -1;
}
}
void throughOffWalls() {
if (position.x < minx) {
position.x = maxx;
}
if (position.y < miny) {
position.y = maxy;
}
if (position.z < minz) {
position.z = maxz;
}
if (position.x > maxx) {
position.x = minx;
}
if (position.y > maxy) {
position.y = miny;
}
if (position.z > maxz) {
position.z = minz;
}
}
void addAttractionForce(PVector force, float radius, float scale) {
float length = PVector.dist(position, force);
PVector diff = new PVector();
diff = position.get();
diff.sub(force);
boolean bAmCloseEnough = true;
if (radius > 0) {
if (length > radius) {
bAmCloseEnough = false;
}
}
if (bAmCloseEnough == true) {
float pct = 1 - (length / radius);
diff.normalize();
diff.mult(scale);
diff.mult(pct);
acceleration.sub(diff);
}
}
}
前回の続き。
2013年、openFrameworksの創始者であるZach Liebermanさんが、パーソンズ大の先生を辞めて“School for Poetic Computing (sfpc)”という私塾を立ち上げた。その名前を最初に聞いた時、個人的には、正直ちょっと戸惑った。
それは、今、日本における「ポエム」という用語の意味が歪んでしまって、軽い蔑みの対象になってしまったところにも原因はあったのかもしれない。今日本で「ポエム」というと、いわゆる「意識高い系」のスローガンみたいな印象がついてしまっていてちょっと意味を曲解してしまう。しかし、真面目に「詩的なコンピューティング」と訳して考えてみても、それでもなかなかしっくりと理解できなかった。
しばらくして、Zachさんがほぼ毎日プログラムを作成してその成果を動画でアップしているというのを知り、その活動をフォローするようになった。その過程で徐々に彼の言う「詩的なコンピューティング」の意味が朧げながら理解できてきたような気がする。
Poetic Computingとは、コードで詩を生成することではない。それは、詩のより根源的な意味、ものごとの表面的な意味だけではなく美学的・喚起的な性質を用いて表現を行うということを、コードを通して実践しているのだということに気付いた。そこから徐々に自分なりに「詩的なコンピューティング」の意味がわかってきたような気がする。
それは、効率やスピードを重視するコンピューティングのカルチャーへのアンチテーゼなのかもしれない。さらには、何らかの対象物を抽象化して表現するということですら無い。
Desing by Numbersのジョン前田やProcessingのCasey ReasとBen Fryは、自分達の開発環境で生みだすコードを「スケッチ」と読んで、スケッチブックに自由に描くようにコーディングをすることを目指した。しかし、スケッチは何らかの対象物がありそれを描写するというニュアンスが含まれてしまう。もちろん、その過程でDaniel Shiffmanの “Nature of Code” のように自然を観察した結果としての優れたスケッチの成果が生まれた。
しかし、Poetic Computingが目指すのは、そうした自然の描写ではなく、より自由により感覚的にコードによって表現することを目指している。単に対象物を描写するのではなく、プログラムから生成される動きや形態そのものから美的な価値や意味を見出す行為なのだと思う。
もちろん、この考えをZachさんにぶつけたら、全然見当違いな可能性もあるのだけれど…
さらに考えを深めていきたい…
先日「計算美学しよう!」という研究グループで、ライブコーディングについての簡単なプレゼンとデモをした。その後のディスカッションで人工知能、人工生命、美学といった様々な専門家の議論を専ら横で聞いていていろいろ刺激され、アイデアが湧いてきた。
その後、TidalCyclesの開発者のAlex McLeanによる “Lessons from the Luddites (ラッダイト運動からの教訓)” という記事を読んで、研究会での議論ともかなり重なる部分もあり、人工知能や機械学習とライブコーディングの関係についていろいろ思いを巡らせた。アイデアを忘れてしまわないようにメモ的に記録してみる。
個人的には、現在のAIブームにはちょっと懐疑的な部分もある。現在の深層学習のアプローチ、巨大なデータを処理して効率的に学習しながら最適解を見つけていくような方法は、今後もどんどん発展し様々なジャンルに適用されていくのは間違いないと思う。その一方で、意識をもった「強いAI」が生まれてシンギュラリティによってAIが人類を凌駕するというのは、まだあまり実感できていない。
とはいえ、例えばコーディングの分野にしても、近い将来、単純に効率的に最適解を求めるようなプログラムなら、人間のプログラマーではなく機械学習によって自動的に生成されたプログラムのほうが遥かに優秀になる時代が来るのは確実だと思う。そういった時代では巨大なデータと膨大な計算資源をもった組織が圧倒的に有利になり、一人のハッカーが世界を変えるというのがどんどん難しくなっていく。巨大な計算資源とデータを押さえた組織が圧倒的に優位だ。
では、個人のプログラマーはもう無力なのか?
そういった背景を踏まえると、現在のライブコーディングやクリエイティブコーディングの動きがちょっと違った角度から見えてくる。例えば、最近になってZach Lieberman達が提唱している “Poetic Computation” は、計算を効率や速度といった観点からではなく美学として詩的な観点からコーディングを捉え直す試みなのだと思う。ライブコーディングは、さらにこの詩的なコーディングの考えを発展させて、コーディングする行為自体に美や詩情を見出す姿勢なのかもしれない。どちらも、機械学習時代のデータ量や効率や速度といった尺度から逃れて、コーディングの別の価値を生みだすためのアプローチと考えると、その意義が見えてくる。
単に機械学習のブームに乗るだけではなく (もちろんそれも重要だが)、機械学習ではカバーできない領域を開拓して表現していくことが、今、個人ベースのプログラマーにとって重要なことなのかもしれない。