yoppa.org



ActoinScript 3:画像ファイルをダウンロードして表示する

今日の授業は、年末の課題として出題したPicasa Webアルバムにアップロードした画像をFlashから扱う方法について学んでいきます。今回の授業内容は、最終課題に直結しますので、しっかりと理解するようにしましょう。

Picasa WebアルバムをFlashに読みこむ準備

Picasa Webアルバムは、API (Application Program Interface – 開発の際に使用できる命令や関数の集合のこと) を用いてWeb経由で情報を取得したり検索したりといった様々な操作が可能となっています。Picasaで使用できるAPIの詳細な資料は下記のページに掲載されていて、この内容を理解することで、Piacasaに掲載された写真の情報を利用して様々なアプリケーションやWebサービスの開発が可能となります。

しかし、このAPIをそのままFlashで使用するには、ある程度のプログラムの知識が必要となり、必ずしもFlashに最適化されたAPIとはいえません。

FlashからPicasa WebアルバムAPIを操作するのに便利なように、AS3で書かれたライブラリが開発されています。このAS3のライブラリを利用することで、より簡易にPicasaの情報をFlash(AS3)から利用できるになります。今回は以下のライブラリを利用します。

このページの「Dowlnloads」から「picasaflashapi-r421.swc」をダウンロードしてファイルを保存しておきます。

Picasa WebのユーザIDとアルバムIDを調べる

これからPicasaにアップロードしたWebアルバムの情報を取得するために、PicasaのアカウントとアルバムIDが必要となります。

まず写真をアップロードしたPicasa WebアルバムをWebブラウザで開きます。アルバムの一覧から使用するアルバムを選択し、アルバムの画面を開きます。

画面の左側に「RSS」というリンクが表示されるので、そのリンクをクリックしてRSSフィードを開きます。このRSSフィードのURLから、picasaのユーザIDとアルバムIDがわかります。例えば、RSSのURLが

の場合は、ユーザIDは「tadokoro」、アルバムIDは「5560743918853662497」になります。つまりこのURLは

となっています。このユーザIDとアルバムIDを記録しておきます。

Picasa Webアルバムの情報を取得する

まずは、APIを利用してPicasa Webアルバムの情報が取得できるか試してみます。以下の手順で制作します。

  • Flashファイル (AS 3.0) 形式でflaファイルを新規に作成、「index.fla」で保存
  • FLAファイルのドキュメントクラスの設定を「Main」にする
  • ActionScriot (AS) ファイルを新規に作成ファイル名を「Main.as」にしてFLAファイルと同じ場所に保存
  • 先程ダウンロードしたpicasaflashapiのライブラリ「picasaflashapi-r421.swc」を、FLAファイルと同じ場所にコピー

これらの手順を終えたら、「Main.as」に以下の記述を記入します。

package {
    import sk.prasa.webapis.picasa.PicasaResponder;
    import sk.prasa.webapis.picasa.PicasaService;
    import sk.prasa.webapis.picasa.events.PicasaDataEvent;
    import flash.system.Security;
    import flash.display.*;
    import flash.events.*;
    import flash.net.*;

    public class Main extends Sprite {
        //ユーザIDとアルバムIDを指定
        private var userID:String='tadokoro';
        private var alubumID:String='5560743918853662497';

        public function Main() {
            //クロスドメインポリシーファイルの読み込み
            Security.loadPolicyFile("http://photos.googleapis.com/data/crossdomain.xml");
            init();
        }
        private function init():void {
            //Picasa APIのサービスを新規に生成
            var service : PicasaService = new PicasaService();
            //使用可能な画像サイズ一覧 (Pixel)
            //32, 48, 64, 72, 144, 160, 200, 288, 320, 400, 512, 576, 
            //640, 720, 800, 912, 1024, 1152, 1280, 1440, 1600

            //最大の幅
            service.imgmax="912";
            //サムネイルのサイズ
            service.thumbsize="64";
            //最大読込枚数
            service.max_results=100;
            //ユーザIDとアルバムIDを指定して、情報を読み込み
            var responder:PicasaResponder=service.photos.list(userID,alubumID);
            //情報の読込みが完了した際に発生するイベントリスナーの登録
            responder.addEventListener(PicasaDataEvent.DATA, onCompleteHandler);
        }

        //情報を読込み
        private function onCompleteHandler(picsData:PicasaDataEvent):void {
            trace('写真データ :' + picsData.data );
            trace('エントリー :' + picsData.data.entries);
            trace('写真総数 :' + picsData.data.entries.length);

            //個別の写真のデータを取得する
            for (var i:int = 0; i < picsData.data.entries.length; i++) {
                trace('-------------------------------------------------------');
                trace('写真NO : ' + i);
                trace('写真ID : ' + picsData.data.entries[i].id);
                trace('写真のURL : ' + picsData.data.entries[i].media.content.url);
                trace('サムネイルURL : ' + picsData.data.entries[i].media.thumbnails[0].url);
                trace('写真の幅 : ' + picsData.data.entries[i].gphoto.width);
                trace('写真の高さ : ' + picsData.data.entries[i].gphoto.height);
            }
        }
    }
}

Flashの「出力」画面にアルバムにアップロードした写真の情報が取得していることを確認できます。

(GET) http://photos.googleapis.com/data/feed/api/user/tadokoro/albumid/5560743918853662497
写真データ :[object AtomFeed]
エントリー :[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry]
写真総数 :22
-------------------------------------------------------
写真NO : 0
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560743958909231746
写真のURL : http://lh6.ggpht.com/_S9tCNvdm3tU/TSu7KyWvIoI/AAAAAAAAE3Q/6r-SUDLf76s/s912/DSC_0044.jpg
サムネイルURL : http://lh6.ggpht.com/_S9tCNvdm3tU/TSu7KyWvIoI/AAAAAAAAE3Q/6r-SUDLf76s/s64-c/DSC_0044.jpg
写真の幅 : 4928
写真の高さ : 3264
-------------------------------------------------------
写真NO : 1
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560744066139826450
写真のURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7RB0gKRI/AAAAAAAAE3U/WKxbZjpigdw/s912/DSC_0082.jpg
サムネイルURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7RB0gKRI/AAAAAAAAE3U/WKxbZjpigdw/s64-c/DSC_0082.jpg
写真の幅 : 4928
写真の高さ : 3264
-------------------------------------------------------
写真NO : 2
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560744129672119666
写真のURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7UufyBXI/AAAAAAAAE3Y/Vzg6F-NCPLw/s912/DSC_0087.jpg
サムネイルURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7UufyBXI/AAAAAAAAE3Y/Vzg6F-NCPLw/s64-c/DSC_0087.jpg
写真の幅 : 4928
写真の高さ : 3264
-------------------------------------------------------
写真NO : 3
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560744232728263106
写真のURL : http://lh3.ggpht.com/_S9tCNvdm3tU/TSu7auaR-cI/AAAAAAAAE3c/YBL6f5z83PI/s912/DSC_0132.jpg
サムネイルURL : http://lh3.ggpht.com/_S9tCNvdm3tU/TSu7auaR-cI/AAAAAAAAE3c/YBL6f5z83PI/s64-c/DSC_0132.jpg
写真の幅 : 4928
写真の高さ : 3264
...

Webサービスの利用 – 写真共有サービスを利用する

今回は、外部のWebサービスと連携して、作成したWebページに動画や写真、音声などを掲載する方法について学びます。また、最終課題への準備として、Picasa Webアルバムを使用する環境設定を行います。


最終課題「オンライン・ポートフォリオ」制作について

ポートフォリオ作成について

この授業では、最終課題としてオンライン・ポートフォリオを作成します。これまでの授業の中でWebページの作成方法について様々なトピックをとりあげてきましたが、最終的にこうした内容をふまえて自分自身のオンラインポートフォリオを完成させてください。

ポートフォリオ作成の注意

バナー作成

展覧会のページのために、各自1つ自分のオンラインポートフォリオのバナーを作成します。バナーは以下のフォーマットで作成するようにしてください

  • 画像フォーマット:Gif または Jpeg
  • ファイル名:ログインID.gif または ログインID.jpg (例:k46000の場合、k46000.gif または k46000.jpg)
  • サイズ:フルバナー(468 x 60 pixel) と ハーフバナー (234 x 60 pixel) の2種類
  • GIFアニメーションなどは可ですが、極端にサイズが大きくならないよう注意
  • ポートフォリオを見てみたくなるように、魅力をアピールする

ファイルのアップロード、公開

ファイルは、IDDのWebサーバーにアップロードすること。最後に必ず以下のアドレスから閲覧できることを確認。

  • http://www.idd.tamabi.ac.jp/ログインID/
  • 例:ログインIDが、k46000の場合は、公開アドレスは http://www.idd.tamabi.ac.jp/k46000/
  • わからないことがあれば、IDDのITサポートページ内の「ネットワークガイド」→「ホームページ公開」を参照してください

今後の更新について

オンラインポートフォリオを課題として扱うのはこの授業で終わりですが、オンラインポートフォリオ自体はこれから卒業まで(卒業後も?)、スタジオの選択や卒業制作、就職活動などで重要な資料となります。この授業が終わったら放置してしまうことなく、絶えず更新をし続けて、iddでの活動がオンラインポートフォリオを閲覧すると全てわかるようなものを目指して活用していくようにしてください。


openFramewoks – OSC (Open Sound Control) を利用したネットワーク連携

今回は、ネットワークを使用したopenFrameworks同士の連携と、openFrameworksと他のアプリケーションとの連携について取り上げます。openFrameworksでネットワークを利用す方法はいくつかありますが、今回はその中で比較的扱い易いプロトコルである、Open Sound Control (OSC) についてとりあげたいと思います。

Open Sound Control (OSC)とは

Open Sound Controlは、カリフォルニア大学バークレー校にある CNMAT(The Center for New Music and Audio Technologies)が開発した、通信プロトコルです。その特徴について、CNMATのページではOSCについて下記のように要約されています。

Open Sound Control (OSC) is a protocol for communication among computers, sound synthesizers, and other multimedia devices that is optimized for modern networking technology. Bringing the benefits of modern networking technology to the world of electronic musical instruments, OSC’s advantages include interoperability, accuracy, flexibility, and enhanced organization and documentation.

(訳) Open Sound Control (OSC) は、コンピュータ、シンセサイザー、その他のマルチメディアデバイス同士でコミュニケーションするための通信プロトコルです。現代のネットワーク技術の成果を電子楽器の世界に適用することで、OSCは、相互運用性、正確さ、柔軟さ、また、拡張性に優れた性能を持ちます。

この説明からわかるように、OSCは当初は電子楽器を連携する目的で開発されました。すっかり古くなってしまったMIDI(Musical Instrument Digital Interface)の次世代を担うプロトコルとなることを狙いとしています。その通信の仕組みとして、インターネットで用いられている通信方式(TCP/IP、UDP/IP)を活用することで、柔軟な連携を可能にしています。

現在では、OSCはその簡易さと柔軟性から電子楽器やコンピュータ音楽の範囲を越えて、様々なアプリケーションやハードウェアに実装されています。主なものだけでも以下のリストのように数多くのアプリケーションやハードウェアで実装されています。

サウンド系アプリケーション

ビジュアル系アプリケーション

ハードウェア

OSCのプロトコル

OSCのプロトコルは大きくわけて2つのパートに分けられます。一つは、OSCメッセージ(OSC Message)、もう一つはOSC引数(OSC Arguments)です。

OSCメッセージは、送受信するOSCの情報内容をラベリングしたものです。つまり、ここでOSCの値が何を意味してるのかを表しています。OSCメッセージは、WWWなどのインターネットアプリケーションで用いられる、URL (Uniform Resorce Locator) と良く似ています。メッセージは階層構造を持つことが可能で、その階層をスラッシュ「/」で区切って表現します。例えば、マウスの状態をOSCで送受信する際に、その座標と、マウスボタンの状態の2つのメッセージを送りたい場合には、その両者を「mouse」というメッセージでまとめて、その下位メッセージとしって「position」と「button」というメッセージがあると考えます。すると、マウスの座標は「/mouse/position」マウスのボタンの状態は「/mouse/button」というメッセージとして両者を階層的に表現できます。このOSCメッセージはアプリケーションでOSCを使用する目的に応じて自由にデザインしていくことが可能となっています。

OSC引数 (OSC Argument) は実際の値を送信します。値は、整数(int32)、実数(float)、文字列(string)など様々な型を送受信することが可能です。また、複数の値を一度に送受信することも可能となっています。

KeynoteScreenSnapz001.png

OSCプロトコルの中身

osc_address.png

OSCメッセージのネームスペースの例

openFrameworksのアプリケーション同士でOSCを送受信する

では、まず初めにopenFrameworksのアプリケーション同士で、OSCを送受信してみましょう。まずは簡単なサンプルとして、マウスの位置とマウスボタンの状態をOSCで表現してみましょう。

OSCは、マウスの位置と、マウスボタンの状態で2種類使用することにします。それぞれ以下のようにメッセージをわりふります。

  • マウスの位置
    • メッセージ:/mouse/position
    • 引数:X座標 Y座標 (例: 320 240)
  • マウスボタンの状態
    • メッセージ:/mouse/button
    • 引数:ボタンの状態 (“up”, “down”)

openFrameworksのプログラムは、送信側(Sender)と受信側(Receiver)の2種類で構成されます。それぞれのプログラムでOSCを使用するためには、ofxOscアドオンをプロジェクトに追加する必要があります。追加した状態は下記のようになります。

それでは、受信側=サーバー(oscReceiveExample)、送信側=クライアント(oscSenderExample)それぞれをプログラムしていきましょう。

サーバー側 (oscReceiveExample)

testApp.h

#ifndef _TEST_APP
#define _TEST_APP


#include "ofMain.h"
#include "ofxOsc.h"

//ポート番号を設定
#define PORT 8000

//--------------------------------------------------------
class testApp : public ofBaseApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    void dumpOSC(ofxOscMessage m); //OSCメッセージを出力
    
private:
    //OSCメッセージを受信するインスタンス
    ofxOscReceiver	receiver;
    //マウス座標
    int remoteMouseX, remoteMouseY;
    //マウスボタンの状態 ("up", "down")
    string mouseButtonState;
    //string oscString;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //指定したポートで接続
	receiver.setup( PORT );
    
    //値を初期化
	mouseX = 0;
	mouseY = 0;
	mouseButtonState = "";

	ofBackground(0, 0, 0);
}

void testApp::update(){
	//現在順番待ちのOSCメッセージがあるか確認
	while( receiver.hasWaitingMessages() )
	{
        //次のメッセージを取得
		ofxOscMessage m;
		receiver.getNextMessage( &m );
        
        //マウスの位置を取得
		if ( m.getAddress() == "/mouse/position" ){
            remoteMouseX = m.getArgAsInt32( 0 );
			remoteMouseY = m.getArgAsInt32( 1 );
            
		} 
        //マウスボタンの状態を取得
        else if ( m.getAddress() == "/mouse/button" ) {
			mouseButtonState = m.getArgAsString( 0 ) ;
		}

        //OSCメッセージをそのままコンソールに出力
        dumpOSC(m);
    }
}

//OSCメッセージをコンソールに出力する関数
void testApp::dumpOSC(ofxOscMessage m) {
    string msg_string;
    msg_string = m.getAddress();
    for (int i=0; i<m.getNumArgs(); i++ ) {
        msg_string += " ";
        if(m.getArgType(i) == OFXOSC_TYPE_INT32)
            msg_string += ofToString( m.getArgAsInt32(i));
        else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT)
            msg_string += ofToString( m.getArgAsFloat(i));
        else if(m.getArgType(i) == OFXOSC_TYPE_STRING)
            msg_string += m.getArgAsString(i);
    }
    cout << msg_string << endl;    
}

void testApp::draw(){
    int radius;
    if (mouseButtonState == "down") {
        //マウスボタンが押されていたら、赤い円を描画
        radius = 20;
        ofSetColor(255, 127, 0);
    } else {
        //マウスボタンが押されていなければ、青い円を描画
        radius = 10;
        ofSetColor(0, 127, 255);
    }
    ofCircle(remoteMouseX, remoteMouseY, radius);
}

void testApp::keyPressed  (int key){}

void testApp::mouseMoved(int x, int y ){}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){}

void testApp::mouseReleased(int x, int y, int button){}

void testApp::windowResized(int w, int h){}

クライアント側 (oscSenderExample)

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxOsc.h"

#define HOST "localhost" //送信先ホストのIPを設定
#define PORT 8000 //送信先のポート番号を設定

//--------------------------------------------------------
class testApp : public ofBaseApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
private:
    //OSCメッセージの送信者
    ofxOscSender sender;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
	ofBackground(0, 0, 0);
    //指定したIPアドレスとポート番号でサーバーに接続
	sender.setup( HOST, PORT );
}

void testApp::update(){
}

void testApp::draw(){
    //現在のマウスの場所に円を描画
    ofSetColor(255, 255, 255);
    ofCircle(mouseX, mouseY, 10);
}

void testApp::keyPressed  (int key){}

void testApp::mouseMoved(int x, int y ){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/position" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
    //メッセージを送信
	sender.sendMessage( m );
}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/button" );
    //OSC引数として、マウス状態"down"を送信
	m.addStringArg( "down" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
	sender.sendMessage( m );
}

void testApp::mouseReleased(int x, int y, int button){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/button" );
    //OSC引数として、マウス状態"up"を送信
	m.addStringArg( "up" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
	sender.sendMessage( m );
}

void testApp::windowResized(int w, int h){}

oscSenderExampleDebugScreenSnapz001.png

送信用プログラムと受信用プログラムを起動して、送信側のウィンドウの上でマウスポインタを動かすと、受信側のプログラムで同じ動きが再現されます。

このプログラムは1台のコンピュータの中だけでなく、ネットワークに繋がったコンピュータ同士でOSCをやりとりすることが可能です。試しに隣の席のコンピュータのIPを調べて、送信側(oscSenderExample)のtestApp.hの7行目に記述してあるIP「”localhost”」を例えば「”192.168.1.10″」のように隣の席のIP番号で置き換えてみましょう。こうすることでネットワークを介して繋がったコンピュータ同士でOSCメッセージが送られていることを確認できます。

受信側のプログラムは、通信の状態をコンソールから確認することができます。XCodeメニューから「実行」→「コンソール」を選択してコンソール画面を表示した状態で実行すると、下記のようにOSCメッセージが受信されていることを確認できます。

/mouse/position 550 430
/mouse/position 547 430
/mouse/position 544 431
/mouse/position 543 431
/mouse/position 541 431
/mouse/position 540 432
/mouse/button down 540 432
/mouse/button up 540 432
/mouse/position 541 432
/mouse/position 543 431
/mouse/position 545 430
/mouse/position 547 429
...

複数で参加できるアプリケーション

次にもう少し複雑なサンプルとして、一つのサーバーに複数のクライアントからメッセージを送ることのできるアプリケーションを作成してみましょう。

クライアント側で画面をクリックすると、サーバー側の同じ場所から輪が波紋のように拡がっていくアプリケーションを作成してみましょう。拡がっていく円は別のクラスRingとして作成しています。クライアント側の参加者全員が同じIP番号を指定することで、サーバに向って複数のクライアントから同時にOSCメッセージを送ることが可能です。

サーバー側

testApp.h

#ifndef _TEST_APP
#define _TEST_APP


#include "ofMain.h"
#include "ofxOsc.h"
#include "Ring.h"

//ポート番号を設定
#define PORT 8000

class testApp : public ofBaseApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    void dumpOSC(ofxOscMessage m);
    vector <Ring *> rings; //拡大する輪"Ring"の配列
    
private:
    ofxOscReceiver	receiver;
    int remoteMouseX, remoteMouseY;
    string mouseButtonState;
    string oscString;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //指定したポートで接続
	receiver.setup( PORT );
    ofSetFrameRate(60);
    
    //値を初期化
	mouseX = 0;
	mouseY = 0;
	mouseButtonState = "";

	ofBackground(0, 0, 0);
}

void testApp::update(){
	//現在順番待ちのOSCメッセージがあるか確認
	while( receiver.hasWaitingMessages() )
	{
        //次のメッセージを取得
		ofxOscMessage m;
        oscString = m.getAddress();
		receiver.getNextMessage( &m );
        
        //マウスの位置を取得
		if ( m.getAddress() == "/mouse/position" ){
            remoteMouseX = m.getArgAsInt32( 0 );
			remoteMouseY = m.getArgAsInt32( 1 );
            
		} 
        //マウスボタンの状態を取得
        else if ( m.getAddress() == "/mouse/button" ) {
			mouseButtonState = m.getArgAsString( 0 ) ;
            remoteMouseX = m.getArgAsInt32( 1 );
			remoteMouseY = m.getArgAsInt32( 2 );
		}

        //OSCメッセージをそのままコンソールに出力
        dumpOSC(m);
    }
    
    //マウスアップされたら、新規にRingを追加
    if(mouseButtonState == "up"){
        rings.push_back(new Ring(ofPoint(remoteMouseX, remoteMouseY)));
        mouseButtonState = "";
    }
    
    //Ring更新
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
        (*it)->update();
        if ((*it)->dead) {
            delete (*it);
            it = rings.erase(it);
        } else {
            ++it;
        }
    }
}

//OSCメッセージをコンソールに出力する関数
void testApp::dumpOSC(ofxOscMessage m) {
    string msg_string;
    msg_string = m.getAddress();
    for (int i=0; i<m.getNumArgs(); i++ ) {
        msg_string += " ";
        if(m.getArgType(i) == OFXOSC_TYPE_INT32)
            msg_string += ofToString( m.getArgAsInt32(i));
        else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT)
            msg_string += ofToString( m.getArgAsFloat(i));
        else if(m.getArgType(i) == OFXOSC_TYPE_STRING)
            msg_string += m.getArgAsString(i);
    }
    cout << msg_string << endl;    
}

void testApp::draw(){
    //Ringを描画
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
        (*it)->draw();
    }
}

void testApp::keyPressed  (int key){}

void testApp::mouseMoved(int x, int y ){}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){}

void testApp::mouseReleased(int x, int y, int button){
    rings.push_back(new Ring(ofPoint(x, y)));
}

void testApp::windowResized(int w, int h){}

Ring.h

#ifndef _RING
#define _RING

#include "ofMain.h"

class Ring {
    
public:
    Ring(ofPoint pos); //コンストラクタ
    void update(); 
    void draw();
    
    ofPoint pos; //輪の中心位置
    float radius; //輪の半径
    float radiusSpeed; //輪の拡大スピード
    bool dead; //生死の判定

private:

};

#endif

Ring.cpp

#include "Ring.h"

Ring::Ring(ofPoint _pos)
{
    pos = _pos;
    radius = 0;
    radiusSpeed = 0.5;
    dead = false;
}

void Ring::update()
{
    radius += radiusSpeed;
    if (radius > ofGetWidth()) {
        dead = true;
    }
}

void Ring::draw()
{
    ofSetCircleResolution(64);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    ofEnableSmoothing();
    ofSetLineWidth(1);
    ofPushMatrix();
    ofTranslate(pos.x, pos.y);
    ofNoFill();
    ofSetColor(63, 127, 255, 127 - radius * 127 / ofGetHeight());
    ofCircle(0, 0, radius);
    ofFill();
    ofSetColor(63, 127, 255, 31 - radius * 31 / ofGetHeight());
    ofCircle(0, 0, radius);
    ofPopMatrix();
}

クライアント側

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxOsc.h"
#include "Ring.h"

#define HOST "192.168.1.10" //サーバのIPアドレスを指定する
#define PORT 8000

//--------------------------------------------------------
class testApp : public ofBaseApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    vector <Ring *> rings; //輪の動的配列
    
private:
    //OSCメッセージの送信者
    ofxOscSender sender;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
	ofBackground(0, 0, 0);
    ofSetFrameRate(60);
    //指定したIPアドレスとポート番号でサーバーに接続
	sender.setup( HOST, PORT );
}

void testApp::update(){
    //Ring更新
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
        (*it)->update();
        if ((*it)->dead) {
            delete (*it);
            it = rings.erase(it);
        } else {
            ++it;
        }
    }
}

void testApp::draw(){
    //Ringを描画
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
        (*it)->draw();
    }
}

void testApp::keyPressed  (int key){}

void testApp::mouseMoved(int x, int y ){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/position" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
    //メッセージを送信
	sender.sendMessage( m );
}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/button" );
    //OSC引数として、マウス状態"down"を送信
	m.addStringArg( "down" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
	sender.sendMessage( m );
}

void testApp::mouseReleased(int x, int y, int button){
    //OSCメッセージの準備
    ofxOscMessage m;
    //OSCアドレスの指定
    m.setAddress( "/mouse/button" );
    //OSC引数として、マウス状態"up"を送信
    m.addStringArg( "up" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
    m.addIntArg( x );
    m.addIntArg( y );
    sender.sendMessage( m );
    
    //Ringを追加
    rings.push_back(new Ring(ofPoint(x, y)));
}

void testApp::windowResized(int w, int h){}

※ Ring.hとRing.cppに関しては、サーバ側(oscRingReceiver)と同じ。

oscRingReceiverDebugScreenSnapz001.png

他のアプリケーションとの連携1:Pure Data (pd) と連携

「Open Sound Control とは」のセクションで解説したように、OSCはopenFrameworks以外にも多くのアプリケーションで実装され利用可能です。異なるアプリケーション同士を連携させることで、そのアプリケーションで得意な分野に特化させて、苦手な部分は他のアプリケーションに任せることが可能となります。例えばopenFrameworksで複雑な音響生成をするのはあまり向いていません。そこで、音響合成用のアプリケーションやDSP用の言語を用いて音響合成を行い、openFrameworksでは得意とする高速のグラフィック処理に専念するということが可能となります。

OSCが利用可能なアプリケーションは、Max/MSP、Pure Data、Csound、SuperCollider、ChucKなど数多く存在しています。今回は、Pure Data(pd)とSuperColliderをOpenFrameworksと連携させてみようと思います。

pdには、MillerPacketの開発した基本機能だけを収録したバージョン(pd-vanilla)と、様々なサードパーティーの機能拡張を含んだバージョン(pd-extended)があります。
pdでOSCを扱いたい際は、拡張機能を収録したpd-extendedを使用する必要があります。最新版のpd-extendedは、下記からダウンロードしてください。

PdでのOSCの受信は、とてもシンプルです。まず、「dumpOSC」オブジェクトを用いて、OSCを受信します。その際に第1引数としてポート番号を指定します。次に受信したOSCメッセージを、「OSCroute」オブジェクトで分解していきます。例えば、「/mouse/position」というOSCメッセージを取り出したい場合には、「OSCroute /mouse」として/mouse以下のメッセージを取り出した上で、「OSCroute /position」として/position以下の値を抽出します。あとは、パラメータの数に応じて「unpack」オブジェクトでOSC引数を分解します。

では、pdで簡単なFMシンセサイザーのパッチを作成してみましょう。そして、モジュレーターのオシレータの周波数とインデックスをそれぞれ、マウスのX座標とY座標でコントロールできるよう、OSCの受信側の設定をしています。マウスの座標は「/mouse/position」メッセージで受信するようになっています。

screen(2010-12-12 16.37.45).png

このpdのパッチにOSCメッセージを送出するopenFrameworksのプログラムは、openFrameworks同士でOSCを送受信していたものと全く同じやり方で、実現可能です。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxOsc.h"
#define HOST "localhost" //IPアドレスを入力
#define PORT 8000

//--------------------------------------------------------
class testApp : public ofBaseApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
private:
    //OSCメッセージの送信者
    ofxOscSender sender;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
	ofBackground(0, 0, 0);
    //指定したIPアドレスとポート番号でサーバーに接続
	sender.setup( HOST, PORT );
}

void testApp::update(){}

void testApp::draw(){
    //現在のマウスの場所に円を描画
    ofSetColor(255, 255, 255);
    ofCircle(mouseX, mouseY, 10);
}

void testApp::keyPressed  (int key){}

void testApp::mouseMoved(int x, int y ){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/position" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
    //メッセージを送信
	sender.sendMessage( m );
}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/button" );
    //OSC引数として、マウス状態"down"を送信
	m.addStringArg( "down" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
	sender.sendMessage( m );
}

void testApp::mouseReleased(int x, int y, int button){
    //OSCメッセージの準備
	ofxOscMessage m;
    //OSCアドレスの指定
	m.setAddress( "/mouse/button" );
    //OSC引数として、マウス状態"up"を送信
	m.addStringArg( "up" );
    //OSC引数として、現在のマウスの座標(x, y)を送信
	m.addIntArg( x );
	m.addIntArg( y );
	sender.sendMessage( m );

}

void testApp::windowResized(int w, int h){}

まず、Pd側のプログラムを起動してDSPをONにした状態で、openFrameworks側のプログラムを実行すると、マウスの位置によって音色が変化するFM合成された音響が生成されます。openFramworks同士の連携と同様、IPアドレスを設定することで、別のマシンから遠隔操作することも可能です。

PdをopenFrameworksプログラム内部に格納する – ofxPd

openFrameworksとPdをOSCを介して組み合わせる方法として、より便利なアドオンofxPdが開発されています。このアドオンを使用すると、Pd側のパッチでは、dumpOSCやOSCrouteなどを使用することなく、シンプルに「r パラメータ名」という形式で値を取得することが可能となります。

また、ofxPdを使用すると、Pdのプログラムを実行することなくopenFrameworksのプログラム内部にPdの音響合成機能を内包することが可能となります。ofxPdでpdのパッチを使用するには、作成したパッチをopenFrameworksのプロジェクトフォルダの「bin/data/」フォルダ内に格納します。

では、ofxPdで使用できるように、先程作成したFM合成のパッチに変更を加えます。変更点は、パッチを開いて200ミリ秒後に自動的に音響合成を開始できるようにしている部分と、パラメータを「dumpOSC」を経由することなく「r」オブジェクトから直接読み込むようにしている部分です。

screen(2010-12-12 17.31.02).png

openFrameworks側では、ofxOSCに加えてofxPdとofxThredをアドオンとしてプロジェクトに追加する必要があります。

アドオンの設定が完了したら、下記のプログラムを作成します。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxPd.h"
#include "ofxOsc.h"

class testApp : public ofBaseApp{
    
public:
    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    void audioRequested(float * output, int bufferSize, int nChannels);
    void audioReceived(float * input, int bufferSize, int nChannels );
    
    float * audioInputData;
    
    // Pdオブジェクト
    ofxPd pd;
    
    // Pdと通信するためのOSC
    ofxOscSender osc_sender;
    
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
	ofSetFrameRate(60);
	ofBackground(0, 0, 0);

	//OSCの初期化
	static const string HOST = "localhost";
	static const int PORT = 8000;
	osc_sender.setup( "localhost", PORT);
	
	// pdのための定数を定義
	// 出力と入力のチャンネル数
	static const int NUM_OUT_CHANNELS = 2;
	static const int NUM_IN_CHANNELS = 2;
	// サンプリングレイト
	static const int BITRATE = 44100;
	// バッファーサイズ
	static const int BUFFER_SIZE = 256;
	// 使用するバッファーの数
	static const int NUM_BUFFERS = 4;
	// Pdのブロックサイズ
	static const int PD_BLOCK_SIZE = 64;

    // Pdを初期化
	pd.setup( "", NUM_OUT_CHANNELS, NUM_IN_CHANNELS, BITRATE, PD_BLOCK_SIZE );
	// Pdファイルを読み込み
	pd.addOpenFile( "simple_fm.pd" );
	// Pdを開始
	pd.start();
	// オーディオ入力を定義
	audioInputData = new float[BUFFER_SIZE*NUM_IN_CHANNELS];
	// サウンド出力を初期化
	ofSoundStreamSetup( NUM_OUT_CHANNELS, NUM_IN_CHANNELS, this, BITRATE, BUFFER_SIZE, NUM_BUFFERS );
	// Pdから音を出力
	pd.startDSP();
}

void testApp::update(){}

void testApp::draw(){
    ofSetColor(0, 127, 255);
    ofCircle(mouseX, mouseY, 20);
}

void testApp::keyPressed  (int key){
}

void testApp::keyReleased  (int key){
}

void testApp::mouseMoved(int x, int y ){
	// マウスの座標をPdのパラメータとして送出
	pd.sendFloat( "modulator_freq", x );
	pd.sendFloat( "modulator_index", y );
}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){}

void testApp::mouseReleased(int x, int y, int button){}

void testApp::windowResized(int w, int h){}

void testApp::audioRequested(float * output, int bufferSize, int nChannels){
    //Pdの音を計算
	pd.renderAudio( audioInputData, output, bufferSize, nChannels );
}

void testApp::audioReceived(float * input, int bufferSize, int nChannels){
	memcpy(audioInputData, input, bufferSize*nChannels*sizeof(float));
}

openFramwroksのプログラムを実行すると、Pdを起動することなく音響が合成されます。openFrameworksのプログラムの中にPdが内包された状態になっています。このようにopenFrameworks単体に機能を統合することで、実際に作品内で使用する際にセットアップを容易で確実に行うことが可能となるでしょう。

openFrameworksとSuperColliderの連携 – ofxSuperCollider

次にopenFrameworksとSuperColliderを連携させてみましょう。

SuperColliderは、言語部分と音響合成部分が分離していて、その相互の連携にOSCを用いています。ですので、その仕組み自体がとてもOSCを用いた他のプログラムとの連携に適しています。具体的には、SuperColliderで楽器を定義する「SynthDef」という関数を使用して、楽器定義ををSuperColliderの音響合成サーバに格納します。あとは、openFrameworksからSuperColliderで行うのと同じ方法で、OSCのメッセージを送出することで音響を合成することが可能となります。

openFrameworksとSuperColliderの連携は、ofxOSCを用いてOSCの素のメッセージを送信しても可能ですが、SuperColliderとの連携に特化したアドオンofxSuperColliderを用いるとより簡単に連携を行うことが可能です。ofxSuperColliderを使用するには、ofxSuperColliderの他に、ofxOscが必要となります。

以下のサンプルは、画面上でマウスをクリックすると、SuperColliderで定義した楽器”newRing”を生成して演奏します。また、生成された音響全体にリバーブをかける楽器”reverb”も同時に用いています。

SuperColliderの楽器定義 (sc_inst.scd)

// SynthDef
(
SynthDef("reverb", {
	arg wet=1.0;
	var in, fx;
	in = In.ar(0, 2);
	fx = in;
	fx = GVerb.ar(fx, 80);
	ReplaceOut.ar(0, fx);
}).store;

SynthDef("baseSound", {
	arg note=40, amp=0.1, fadein=12.0;
	var env, out;
	env = EnvGen.kr(Env.new([0, amp], [fadein]));
	out = RLPF.ar(LFPulse.ar([note, note+7].midicps, 0.15), SinOsc.kr(0.1, 0, 10, 72).midicps, 0.1, 0.1);
	Out.ar(0, out*env);
}).store;

SynthDef("newRing", {
	arg note=40, amp=0.5, pan = 0.0, decay=4.0;
	var env1, out1, env2, out2, mix;
	out1 = RLPF.ar(LFPulse.ar([note, note+7].midicps, 0.15), SinOsc.kr(0.1, 0, 10, 72).midicps, 0.1, 0.1);
	out2 = SinOsc.ar([(note+48).midicps, (note+55).midicps]);
	env1 = EnvGen.kr(Env.perc(decay/4.0, decay/4.0*3.0, amp, -4), doneAction: 2);
	env2 = EnvGen.kr(Env.adsr(0.001, 0.4, 0.0, decay, amp*0.1, -4));
	mix = (out1 * env1) + (out2 * env2);
	mix = CombN.ar(mix, 0.31, 0.31, 2, 0.5, mix);
	Out.ar(0, mix);
}).store;
)

// test
s.sendMsg("/s_new", "reverb", x = s.nextNodeID, 1, 1);
s.sendMsg("/s_new", "baseSound", x = s.nextNodeID, 1, 1);
s.sendMsg("/s_new", "newRing", x = s.nextNodeID, 1, 1, "note", 42);

testApp.h

#ifndef _TEST_APP
#define _TEST_APP


#include "ofMain.h"
#include "ofxSuperCollider.h"
#include "Ring.h"

class testApp : public ofBaseApp{
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    ofxSCSynth *reverb; // SC楽器 "reverb"
    ofxSCSynth *newRing; // SC楽器 "newRing"
    ofxSCSynth *baseSound;  // SC楽器 "baseSound"
    vector <Ring *> rings; //拡大する輪"Ring"の配列
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    ofSetFrameRate(60);
    ofBackground(0, 0, 0);
    
    reverb = new ofxSCSynth("reverb");
    reverb->create();
    baseSound = new ofxSCSynth("baseSound");
    baseSound->create();
}

void testApp::update(){
    //Ring更新
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
        (*it)->update();
        if ((*it)->dead) {
            delete (*it);
            it = rings.erase(it);
        } else {
            ++it;
        }
    }
}

void testApp::draw(){
    //Ringを描画
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
        (*it)->draw();
    }
}

void testApp::keyPressed(int key){}

void testApp::keyReleased(int key){}

void testApp::mouseMoved(int x, int y ){}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){}

void testApp::mouseReleased(int x, int y, int button){
    //newRingの楽器を新規に生成し演奏
    int note[8] = {28, 35, 40, 47, 52, 59, 64, 71};
    newRing = new ofxSCSynth("newRing");
    newRing->set("note", note[int(ofRandom(0, 8))]);
    newRing->set("pan", (x - ofGetWidth() / 2.0) / ofGetWidth() * 2.0);
    newRing->create();
    
    //Ringを追加
    rings.push_back(new Ring(ofPoint(x, y)));
}

void testApp::windowResized(int w, int h){}

Ring.h

#ifndef _RING
#define _RING

#include "ofMain.h"

class Ring {
    
public:
    Ring(ofPoint pos);
    void update();
    void draw();
    
    ofPoint pos;
    float radius;
    float radiusSpeed;
    bool dead;
    
private:

};
#endif

Ring.cpp


#include "Ring.h"

Ring::Ring(ofPoint _pos)
{
    //初期設定
    pos = _pos;
    radius = 0;
    radiusSpeed = 0.5;
    dead = false;
}

void Ring::update()
{
    //輪の半径を拡大
    radius += radiusSpeed;
    //もし画面の幅より半径が大きくなったら、死亡
    if (radius > ofGetWidth()) {
        dead = true;
    }
}

void Ring::draw()
{
    //輪を描く
    ofSetCircleResolution(64);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    ofEnableSmoothing();
    ofSetLineWidth(2);
    ofPushMatrix();
    ofTranslate(pos.x, pos.y);
    ofNoFill();
    ofSetColor(63, 127, 255, 200);
    ofCircle(0, 0, radius);
    ofPopMatrix();
}

oscRingReceiverDebugScreenSnapz001.png

このプログラムで音を生成するには、まずSuperColliderを起動し、サーバーを起動します。その上で楽器の定義を選択して「Enter」キーを押して楽器定義をサーバーに格納します。その後、openFrameworks側でプログラムを実行し、画面をクリックすると、拡がる輪とともに、SCで生成した音響が生成されます。

SuperColliderの楽器定義をopenFrameworksに内包する – ofxSuperColliderServer

SuperColliderの楽器もまた、openFrameworksのプログラムに内包することが可能です。SuperColliderをopenFrameworksに内包するには、ofxSuperColliderServerというアドオンを使用します。このアドオンは楽器定義ファイルをopenFrameworksのプログラムに直接読み込んでSuperColliderによる音響合成を可能とするとても便利なアドオンです。使用方法もとても簡単で、ofxSuperColliderServerのアドオンを読み込んだ後初期化を行うだけで、あとはofxSuperCllider単体で行った際と同じ方法でプログラムします。

先程のプログラムを書き換えて、openFramework単体で音響合成が可能なようにしてみましょう。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP


#include "ofMain.h"
#include "ofxOsc.h"
#include "Ring.h"
#include "ofxSuperCollider.h"
#include "ofxSuperColliderServer.h"

//ポート番号を設定
#define PORT 8000

//--------------------------------------------------------
class testApp : public ofBaseApp{
    
public:    
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    void dumpOSC(ofxOscMessage m);
    vector <Ring *> rings; //拡大する輪"Ring"の配列
    ofxSCSynth *reverb; // SC楽器 "reverb"
    ofxSCSynth *newRing; // SC楽器 "newRing"
    ofxSCSynth *baseSound;  // SC楽器 "baseSound"
    
private:
    ofxOscReceiver	receiver;
    int remoteMouseX, remoteMouseY;
    string mouseButtonState;
    string oscString;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //指定したポートで接続
	receiver.setup( PORT );
    ofSetFrameRate(60);
    
    //値を初期化
	mouseX = 0;
	mouseY = 0;
	mouseButtonState = "";

	ofBackground(0, 0, 0);
    
    ofxSuperColliderServer::init();
    reverb = new ofxSCSynth("reverb");
    reverb->create();
    baseSound = new ofxSCSynth("baseSound");
    baseSound->create();
}

void testApp::update(){
	//現在順番待ちのOSCメッセージがあるか確認
	while( receiver.hasWaitingMessages() )
	{
        //次のメッセージを取得
		ofxOscMessage m;
        oscString = m.getAddress();
		receiver.getNextMessage( &m );
        
        //マウスの位置を取得
		if ( m.getAddress() == "/mouse/position" ){
            remoteMouseX = m.getArgAsInt32( 0 );
			remoteMouseY = m.getArgAsInt32( 1 );
            
		} 
        //マウスボタンの状態を取得
        else if ( m.getAddress() == "/mouse/button" ) {
			mouseButtonState = m.getArgAsString( 0 ) ;
            remoteMouseX = m.getArgAsInt32( 1 );
			remoteMouseY = m.getArgAsInt32( 2 );
		}

        //OSCメッセージをそのままコンソールに出力
        dumpOSC(m);
    }
    
    //マウスアップされたら、新規にRingを追加
    if(mouseButtonState == "up"){
        rings.push_back(new Ring(ofPoint(remoteMouseX, remoteMouseY)));
        mouseButtonState = "";
        
        //SCで音を鳴らす
        int note[8] = {28, 35, 40, 47, 52, 59, 64, 71};
        newRing = new ofxSCSynth("newRing");
        newRing->set("note", note[int(ofRandom(0, 8))]);
        newRing->set("pan", (remoteMouseX - ofGetWidth() / 2.0) / ofGetWidth() * 2.0);
        newRing->create();
    }
    
    //Ring更新
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
        (*it)->update();
        if ((*it)->dead) {
            delete (*it);
            it = rings.erase(it);
        } else {
            ++it;
        }
    }
}

//OSCメッセージをコンソールに出力する関数
void testApp::dumpOSC(ofxOscMessage m) {
    string msg_string;
    msg_string = m.getAddress();
    for (int i=0; i<m.getNumArgs(); i++ ) {
        msg_string += " ";
        if(m.getArgType(i) == OFXOSC_TYPE_INT32)
            msg_string += ofToString( m.getArgAsInt32(i));
        else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT)
            msg_string += ofToString( m.getArgAsFloat(i));
        else if(m.getArgType(i) == OFXOSC_TYPE_STRING)
            msg_string += m.getArgAsString(i);
    }
    cout << msg_string << endl;    
}

void testApp::draw(){
    //Ringを描画
    for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
        (*it)->draw();
    }
}

void testApp::keyPressed  (int key){}

void testApp::mouseMoved(int x, int y ){}

void testApp::mouseDragged(int x, int y, int button){}

void testApp::mousePressed(int x, int y, int button){}

void testApp::mouseReleased(int x, int y, int button){
    rings.push_back(new Ring(ofPoint(x, y)));
    int note[8] = {28, 35, 40, 47, 52, 59, 64, 71};
    newRing = new ofxSCSynth("newRing");
    newRing->set("note", note[int(ofRandom(0, 8))]);
    newRing->set("pan", (x - ofGetWidth() / 2.0) / ofGetWidth() * 2.0);
    newRing->create();
}

void testApp::windowResized(int w, int h){}

サンプルプログラムのダウンロード

今回紹介した全てのプログラムは、下記からダウンロード可能です。


AS3 マウスイベントを扱う

先週に引き続き、マウスを利用したインタラクションについて取り上げます。

今回は、マウスの位置だけでなく、より詳細なマウスの状態を取得する方法について紹介します。

マウスイベント

マウスからの入力は、先週とりあつかったマウスポインタの位置だけでなく、マウスのボタンの状態や対象との関係によって様々な状態を表現することが可能です。ActionScript 3では、マウスの状態をイベントとして受けとることができます。マウスのイベントを処理するイベントリスナーを生成することで、きめ細かなマウスの入力の処理が可能となります。

ActionScript 3で取り扱うことのできるイベントは下記の通りです。

  • MouseEvent.CLICK
  • MouseEvent.DOUBLE_CLICK
  • MouseEvent.MOUSE_DOWN
  • MouseEvent.MOUSE_MOVE
  • MouseEvent.MOUSE_OUT
  • MouseEvent.MOUSE_OVER
  • MouseEvent.MOUSE_UP
  • MouseEvent.MOUSE_WHEEL
  • MouseEvent.ROLL_OUT
  • MouseEvent.ROLL_OVER

これらのイベントがどのような時に発生するのか、また、これらのイベントを活用することでどのようなことが出来るのかを探っていきましょう。

スクリプトのテンプレート

今日の授業で使用するスクリプトのテンプレートです。まず、AS3形式のFlashファイルを用意し、ドキュメントクラスを「Main」に指定します。その上で下記のコードを新規に作成したActionScriptファイルにペーストして、ファイル名を「Main.as」にしてFlaファイルと同じ場所に保存してください。

package {
  import flash.display.*;
  import flash.events.*;

  public class Main extends Sprite {
  //ここにグローバルな変数を記入

    public function Main() {
    //初期設定の項目を記入
           
      //フレーム更新のイベントリスナーの登録
      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    //イベントハンドラ
    function enterFrameHandler(event:Event):void {

    }
  }
}

マウスイベントを取得する

まず初めに、マウスイベントを取得して、trace() 関数を使用して状態を出力するサンプルを作成してみましょう。またいつものように、AS3形式でFLAファイルを作成し、ドキュメントクラスを「Main」にします。その上で同じ場所に「Main.as」というファイル名でActionScript形式のファイルを作成し、下記のスクリプトを記入します。

package {
    import flash.display.*;
    import flash.events.*;

    public class Main extends Sprite {
        //ここにグローバルな変数を記入

        public function Main() {
            //初期設定の項目を記入

            //マウスイベントリスナー
            stage.addEventListener(MouseEvent.CLICK, clickHandler);
            stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
            stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
        }

        //クリック
        private function clickHandler(event:MouseEvent):void {
            trace("clickHandler");
        }
        //マウスボタンを押した瞬間
        private function mouseDownHandler(event:MouseEvent):void {
            trace("mouseDownHandler");
        }
        //マウスボタンを離した瞬間
        private function mouseUpHandler(event:MouseEvent):void {
            trace("mouseUpHandler");
        }
        //マウスボタンを動かした瞬間
        private function mouseMoveHandler(event:MouseEvent):void {
            trace("mouseMoveHandler");
        }
    }
}
/wp-content/uploads/2010/12/Main1.swf, 550, 400

ムービーを実行すると、マウスの動作に対応して、「出力」画面にメッセージが表示されると思います。マウスボタンを何も押さずにマウスポインタを動かしたときには、MouseEvent.MOUSE_MOVEのメッセージ。マウスボタンを押した瞬間に、MouseEvent.MOUSE_DOWN。そして、押していたマウスボタンを離した瞬間にMouseEvent.MOUSE_UPと、MouseEvent.CLICKが出力されるのがわかります。

ムービークリップとマウスとの関係を取得する

先程の例は、ステージ上でのマウスの動作を取得していました。AS3のマウスイベントでは、ステージ上に配置したムービークリップとの関係性を表現するために、MOUSE_DOWNやMOUSE_UPなどの他に、様々なイベントが用意されています。

ムービークリップとの関連を調べるために、ステージ上に「MyButton」というクラス名でリンケージを設定してボタンの図形を描きます。これをステージ中央に配置してマウスでのインタラクションを試してみましょう。

screen(2010-12-07 20.52.00).png

package {
    import flash.display.*;
    import flash.events.*;

    public class Main extends Sprite {
        //ここにグローバルな変数を記入
        var myButton:MyButton = new MyButton();

        public function Main() {
            //初期設定の項目を記入
            //画面中心にボタンを配置
            myButton.x=stage.stageWidth/2;
            myButton.y=stage.stageHeight/2;
            this.addChild(myButton);

            //ボタン(myButton)に対するマウスイベントリスナー
            myButton.addEventListener(MouseEvent.CLICK, clickHandler);
            myButton.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            myButton.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
            myButton.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
            myButton.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
        }

        //クリック
        private function clickHandler(event:MouseEvent):void {
            trace("clickHandler");
        }
        //マウスボタンを押した瞬間
        private function mouseDownHandler(event:MouseEvent):void {
            trace("mouseDownHandler");
            myButton.scaleX = myButton.scaleY = 0.9;
        }
        //マウスボタンを離した瞬間
        private function mouseUpHandler(event:MouseEvent):void {
            trace("mouseUpHandler");
            myButton.scaleX = myButton.scaleY = 1.0;
        }
        //ロールオーバー
        private function rollOverHandler(event:MouseEvent):void {
            trace("rollOverHandler");
            myButton.alpha=0.5;
        }
        //ロールアウト
        private function rollOutHandler(event:MouseEvent):void {
            trace("rollOutHandler");
            myButton.alpha=1.0;
        }
    }
}
/wp-content/uploads/2010/12/Main2.swf, 550, 400

この例ではステージ状に配置した、MyButton(インスタンス名はmyButton)というムービークリップシンボルと、マウスの状態を表示しています。注意深く観察すると、MouseEvent.CLICK、MouseEvent.MOUSE_UP、MouseEvent.MOUSE_DOWN、MouseEvent.ROLL_OVER、MouseEvent.ROLL_OUT という合計5つのイベントが、それぞれ独自のタイミングで発生していることがわかると思います。

  • MouseEvent.ROLL_OVER:マウスポイタが、対象のムービクリップ(myButton)と重なった瞬間
  • MouseEvent.ROLL_OUT:マウスポイタが、対象のムービクリップ(myButton)から離れた瞬間
  • MouseEvent.MOUSE_DOWN:マウスポイタが、対象のムービクリップ(myButton)の上でボタンを押した瞬間
  • MouseEvent.MOUSE_UP:マウスポイタが、対象のムービクリップ(myButton)の上でボタンを離した瞬間
  • MouseEvent.CLICK:マウスポイタが、対象のムービクリップ(myButton)の上でボタンを離した瞬間。ただし、対象のムービクリップの外でマウスボタンを押して、ボタンを押したままボタンの上まで移動して離しても、CLICKイベントは発生しない。

この中で、MOUSE_UPとCLICKの違いがわかりずらいかもしれません。両者は共にマウスボタンを離した瞬間に発生するのですが、1つだけ違いがあります。対象とするムービークリップの外でマウスボタンを押し(= MouseEvent.MOUSE_DOWN)、そのままボタンを押した状態でムービークリップまで移動してきて、マウスボタンを離します。その際には、CLICKイベントは発生するのですが、MOUSE_UPイベントは発生しません。

ムービークリップをマウスで掴んで移動する

では、もう少し高度な例として、ムービークリップをマウスポインタで掴んで移動できるようなインタラクションを作成してみましょう。

マウスで対象のムービークリップを掴んで移動するには、MOUSE_DOWNイベントとMOUSE_UPイベントをうまく使い分ける必要があります。MOUSE_DOWNイベントが発生した際に、物体を掴んだと判断し、MOUSE_UPイベントが発生した瞬間に掴んでいた物体を離したと解釈すると自然なインタラクションとなります。

マウスでムービークリップを移動したように見せる方法は、マウスの座標を取得して常にその座標とムービークリップの座標が一致するようにすることで実現可能ですが、もう少し簡易な方法が存在します。例えば、ムービークリップシンボルが「hoo」だとすると以下のメソッドでマウスのドラッグによる移動の開始/終了を設定可能です。

  • hoo.startDrag():ターゲットのムービクリップのマウスドラッグでの移動を開始
  • hoo.stopDrag():ターゲットのムービクリップのマウスドラッグでの移動を終了

この機能を活用して、以下のようにプログラムしてみましょう。

package {
    import flash.display.*;
    import flash.events.*;

    public class Main extends Sprite {
        //ここにグローバルな変数を記入
        var myObject:MyObject = new MyObject();

        public function Main() {
            //初期設定の項目を記入
            //画面中心にボタンを配置
            myObject.x=stage.stageWidth/2;
            myObject.y=stage.stageHeight/2;
            this.addChild(myObject);

            //フレーム更新のイベントリスナーの登録
            this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);

            //マウスイベントリスナー
            myObject.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            myObject.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
            myObject.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
            myObject.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
        }

        //イベントハンドラ
        function enterFrameHandler(event:Event):void {
        }

        //マウスボタンを押した瞬間
        private function mouseDownHandler(event:MouseEvent):void {
            event.target.startDrag();
        }
        //マウスボタンを離した瞬間
        private function mouseUpHandler(event:MouseEvent):void {
            event.target.stopDrag();
        }
        //ロールオーバー
        private function rollOverHandler(event:MouseEvent):void {
            event.target.alpha=0.5;
        }
        //ロールアウト
        private function rollOutHandler(event:MouseEvent):void {
            event.target.alpha=1.0;
        }
    }
}
/wp-content/uploads/2010/12/Main3.swf, 550, 400

応用:ふくわらい

最後に複数の物体をそれぞれマウスで掴んで移動できるような例を紹介します。

目や鼻、口などを画像で作成して、それをマウスで移動可能にすることで「ふくわらい」を作成してみましょう。

package {
    import flash.display.*;
    import flash.events.*;

    public class Main extends Sprite {
        //ここにグローバルな変数を記入
        var eyeR:EyeR = new EyeR();
        var eyeL:EyeL = new EyeL();
        var nose:Nose = new Nose();
        var mouth:Mouth = new Mouth();

        public function Main() {
            //初期設定の項目を記入
            //ランダムに顔のパーツを配置
            eyeR.x=Math.random()*stage.stageWidth;
            eyeR.y=Math.random()*stage.stageHeight;
            this.addChild(eyeR);

            eyeL.x=Math.random()*stage.stageWidth;
            eyeL.y=Math.random()*stage.stageHeight;
            this.addChild(eyeL);

            nose.x=Math.random()*stage.stageWidth;
            nose.y=Math.random()*stage.stageHeight;
            this.addChild(nose);
            
            mouth.x=Math.random()*stage.stageWidth;
            mouth.y=Math.random()*stage.stageHeight;
            this.addChild(mouth);

            //マウスイベントリスナー
            eyeR.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            eyeR.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
            eyeR.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
            eyeR.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
            
            eyeL.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            eyeL.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
            eyeL.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
            eyeL.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
            
            nose.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            nose.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
            nose.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
            nose.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
            
            mouth.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
            mouth.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
            mouth.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
            mouth.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
        }

        //マウスボタンを押した瞬間
        private function mouseDownHandler(event:MouseEvent):void {
            event.target.startDrag();
        }
        //マウスボタンを離した瞬間
        private function mouseUpHandler(event:MouseEvent):void {
            event.target.stopDrag();
        }
        //ロールオーバー
        private function rollOverHandler(event:MouseEvent):void {
            event.target.alpha=0.5;
        }
        //ロールアウト
        private function rollOutHandler(event:MouseEvent):void {
            event.target.alpha=1.0;
        }
    }
}
/wp-content/uploads/2010/12/Main4.swf, 550, 400


openFrameworks – addon を使う 3 : OpenCVを利用した映像認識

今回は、OpenCVという映像解析の技術を応用して、ライブ映像を用いたインタラクティブな表現に挑戦します。

「OpenCV」とは、「Open Computer Vision Library」の略で、オープンソースでコンピュータビジョンの技術を利用可能なライブラリです。米Intel社で開発され、画像処理・画像認識用のC/C++言語のライブラリとして配布されています。商用・非商用を問わず無料で使用することが可能です(BSDライセンス)。

OpenCVのコアとなる技術「コンピュータビジョン」とは、ひとことで言うと「ロボットの目」を作るという研究分野です。「コンピュータビジョン」の実現のために、画像のセンシングのためのハードウェアの研究から情報を認識するための人工知能の研究まで、広範囲な分野の研究が行われています。

OpenCVは、この「コンピュータビジョン」の研究の中でソフトウェアを用いて画像処理を行う用途に使用されています。映像をリアルタムに解析した結果からその構造を解析し、モーション検知や物体追跡、さらには機械学習(顔認識)など、様々な応用が可能となっています。

openFrameworksでは、ofxOpenCvというアドオンを使用することで、簡単にOpenCVの機能を活用することが可能です。

openFrameworks + OpenCVを活用した作品実例

OpenCVによる画像解析を用いてインタラクションを実現したメディアアート作品は、近年、数多く発表されています。ここ数年のメディアアートのメインストリームの一つといっても過言ではないかもしれません。現在活発に情報がやりとりされている「Kinect Hack」に関しても、こうしたリアルタイムの映像解析を用いたインタラクティブアートの一つの進化形とも考えられます。

OpenCVを分析のエンジンとして使用した過去の有名な作品をいくつか紹介します。

Graffiti Research Lab. “L.A.S.E.R. Tag”

“The EyeWriter”

Chris O’shea “Hand from Above”

YesYesNo “Night Lights”

Marco Tempest “Magic Projection”

Theodore Watson “Knee Deep”

Knee Deep – Cinekid 2009 from Theo Watson on Vimeo.

Theodore Watson “Boards Interactive Magazine”

Boards Interactive Magazine – Walkthrough from Theo Watson on Vimeo.

映像から物体を認識するアルゴリズム

では、実際にOpenCVによる映像解析を用いて、移動する物体を識別する方法について探っていきましょう。OpenCVのライブラリを使用することで比較的簡単に映像ソースから変化した部分だけを識別し、さらにその輪郭を抽出することが可能です。このデータを応用することで、様々なインタラクティブなメディアアート作品を作成していくことが可能となります。まずは、輪郭抽出までの手順を順番に理解していきましょう。

カラーの映像ソースから、移動する物体の輪郭を抽出するまでには、いくつかの手順を踏む必要があります。おおまかな流れは以下のような順番になっています。

  • カメラから映像をキャプチャー
  • グレースケールに変換
  • 背景画像を登録
  • 背景画像と現在の画像の差分を計算
  • 差分の画像を2値化
  • 2値化した画像を解析(輪郭抽出、重心を算出)

KeynoteScreenSnapz001.png

この手順に従って、実際にopenFrameworksでプログラミングしていきましょう。

物体検出の基本プログラム

まず、輪郭検出までの手順を、そのまま順番に実装した素直なプログラムを作ってみましょう。

ソース映像や、それぞれの解析過程の映像のためのメモリ領域を確保した上で、ofxOpenCvのメソッドを活用しながら、徐々に輪郭を抽出しています。

openFrameworksからofxOpenCvの機能を使用するには、addonsの中にofxOpenCvを加える必要があります。addonsフォルダ内が下記のようになるようファイルを追加してください。

XcodeScreenSnapz001.png

アドオンが追加できたら、下記のソースを入力します。

test.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxOpenCv.h"

//あらかじめ録画したビデオを使用する場合には、ここをコメントアウト
#define _USE_LIVE_VIDEO

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    #ifdef _USE_LIVE_VIDEO
    //ライブカメラを使用する際には、カメラ入力を準備
    ofVideoGrabber 		vidGrabber;
    #else
    //あらかじめ録画した映像を使用する際には、ビデオプレイヤーを準備
    ofVideoPlayer 		vidPlayer;
    #endif

    ofxCvColorImage colorImg; //オリジナルのカラー映像
    ofxCvGrayscaleImage grayImage; //グレースケールに変換後
    ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像
    ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分
    ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス
    
    bool bLearnBakground; //背景画像を登録したか否か
    bool showCvAnalysis; //解析結果を表示するか
    int threshold; //2値化の際の敷居値
    int videoMode; //表示する画像を選択
};

#endif

test.cpp

#include "testApp.h"

void testApp::setup(){
    //画面の基本設定
    ofBackground(0, 0, 0);
    ofEnableAlphaBlending();
    ofSetFrameRate(60);

    //カメラ使用の場合
    #ifdef _USE_LIVE_VIDEO
    //カメラから映像を取り込んで表示
    vidGrabber.setVerbose(true);
    vidGrabber.initGrabber(320,240);
    #else
    //カメラ不使用の場合ムービーファイルを読み込んで再生
    vidPlayer.loadMovie("fingers.mov");
    vidPlayer.play();
    #endif
    
    //使用する画像の領域を確保
    colorImg.allocate(320,240);
    grayImage.allocate(320,240);
    grayBg.allocate(320,240);
    grayDiff.allocate(320,240);
    
    //変数の初期化
    bLearnBakground = true;
    showCvAnalysis = false;
    threshold = 20;
    videoMode = 0;
}

void testApp::update(){
    //新規フレームの取り込みをリセット
    bool bNewFrame = false;
    
    #ifdef _USE_LIVE_VIDEO
    //カメラ使用の場合はカメラから新規フレーム取り込み
    vidGrabber.grabFrame();
    //新規にフレームが切り替わったか判定
    bNewFrame = vidGrabber.isFrameNew();
    #else
    //カメラ不使用の場合は、ビデオプレーヤーから新規フレーム取り込み
    vidPlayer.idleMovie();
    //新規にフレームが切り替わったか判定
    bNewFrame = vidPlayer.isFrameNew();
    #endif
    
    //フレームが切り替わった際のみ画像を解析
    if (bNewFrame){
        #ifdef _USE_LIVE_VIDEO
        //取り込んだフレームを画像としてキャプチャ
        colorImg.setFromPixels(vidGrabber.getPixels(), 320,240);
        //左右反転
        colorImg.mirror(false, true);
        #else
        //取り込んだフレームを画像としてキャプチャ
        colorImg.setFromPixels(vidPlayer.getPixels(), 320,240);
        #endif
        
        //カラーのイメージをグレースケールに変換
        grayImage = colorImg;
        
        //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする
        if (bLearnBakground == true){
            grayBg = grayImage;
            bLearnBakground = false;
        }
        
        //グレースケールのイメージと取り込んだ背景画像との差分を算出
        grayDiff.absDiff(grayBg, grayImage);
        //画像を2値化(白と黒だけに)する
        grayDiff.threshold(threshold);
        //2値化した画像から輪郭を抽出する
        contourFinder.findContours(grayDiff, 25, grayDiff.width * grayDiff.height, 10, false, false);
    }
}

void testApp::draw(){
    //現在のモードに応じて、表示する映像を切り替え
    switch (videoMode) {
            
        case 1:
            //グレースケール映像
            grayImage.draw(0, 0, ofGetWidth(), ofGetHeight());
            break;
            
        case 2:
            //背景画像
            grayBg.draw(0, 0, ofGetWidth(), ofGetHeight());
            break;
            
        case 3:
            //2値化された差分映像
            grayDiff.draw(0, 0, ofGetWidth(), ofGetHeight());
            break;
            
        default:
            //カラー映像
            colorImg.draw(0, 0, ofGetWidth(), ofGetHeight());
            break;
    }
    
    //画面に対する映像の比率を計算
    float ratioX = ofGetWidth()/320;
    float ratioY = ofGetHeight()/240;
    
    //解析結果を表示する場合
    if (showCvAnalysis) {
        //検出した解析結果を表示
        for (int i = 0; i < contourFinder.nBlobs; i++){
            ofPushMatrix();
            //画面サイズいっぱいに表示されるようリスケール
            glScalef((float)ofGetWidth() / (float)grayDiff.width, 
                     (float)ofGetHeight() / (float)grayDiff.height, 
                     1.0f);
            contourFinder.blobs[i].draw(0,0);
            ofFill();
            ofSetColor(255, 0, 0);
            ofEllipse(contourFinder.blobs[i].centroid.x, contourFinder.blobs[i].centroid.y, 4, 4);
            ofPopMatrix();
        }
    }
    
    //ログと操作説明を表示
    ofSetColor(255, 255, 255);
    ofDrawBitmapString("FPS: "+ofToString(ofGetFrameRate()), 20, 20);
    ofDrawBitmapString("Threshold: "+ofToString(threshold), 20, 35);
    ofDrawBitmapString("Number of Blobs: "+ofToString(contourFinder.nBlobs), 20, 50);
    ofDrawBitmapString("[0] Show original video", 20, 65);
    ofDrawBitmapString("[1] Show grayscale video", 20, 80);
    ofDrawBitmapString("[2] Show captured background", 20, 95);
    ofDrawBitmapString("[3] Show difference from background", 20, 110);
    ofDrawBitmapString("[space] Captuer background", 20, 125);
    ofDrawBitmapString("[a] Analysis on / off", 20, 140);
    
}

void testApp::keyPressed  (int key){
    //キー入力でモード切り替え
    switch (key){
        case '0':
            //カラー映像表示
            videoMode = 0;
            break;
            
        case '1':
            //グレースケール映像表示
            videoMode = 1;
            break;
            
        case '2':
            //背景画像表示
            videoMode = 2;
            break;
            
        case '3':
            //2値化した差分映像
            videoMode = 3;
            break;
            
        case 'a':
            //解析結果の表示の on / off
            showCvAnalysis ? showCvAnalysis=false : showCvAnalysis=true;
            break;
            
        case 'f':
            //フルスクリーンに
            ofSetFullscreen(true);
            break;
            
        case ' ':
            //背景画像を新規に取り込む
            bLearnBakground = true;
            break;
            
        case '+':
            //2値化の閾値を増加
            threshold ++;
            if (threshold > 255) threshold = 255;
            break;
            
        case '-':
            //2値化の閾値を減少
            threshold --;
            if (threshold < 0) threshold = 0;
            break;
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

OpenCvBasicDebugScreenSnapz001.png

このプログラムはキーボード入力によってモードを切り替えられる仕組みになっています。実際にキー入力をしてみて、処理の過程を実感してみましょう。

  • [0]キー:オリジナルのカラー映像
  • [1]キー:グレースケールに変換した映像
  • [2]キー:背景としてキャプチャーした静止画
  • [3]キー:背景画像と現在のグレースケールの画像の差分をとって、2値化した映像
  • [space]キー:背景画像を新規にキャプチャーする
  • [a]キー:輪郭抽出の解析結果を表示する
  • [+]キー:2値化の閾値のレベルを増加
  • [-]キー:2値化の閾値のレベルを減少

輪郭抽出(ContorFinder)のためのパラメータ詳細

映像からモーションを検出し輪郭を抽出するためには、様々な画像処理が行われています。実際には処理を行う対象、周囲の環境、カメラの性能、明るさなど様々な要因の影響を受けるため、実際に分析結果をみながら細かくパラメータを調整していく必要があります。パラメータには下記のようなものがあります。

  • 閾値 (threshold) - グレースケールの映像を白黒に2値化する際に、白と黒を分ける境目の値
  • 物体の最小サイズ (minBlobSize) - どこまで小さい物体(Blob)まで検出するか
  • 物体の最大サイズ (minBlobSize) - どこまで大きい物体(Blob)まで検出するか
  • 最大認識数(maxNumBlobs) - 物体を最大何個検出するか
  • 穴検出 (findHoles) - 物体の中に空いた穴を検出するか
  • 近似 (useApproximation) - 近似値を使用するか

これらの値を、実際に検出結果を確かめながら適切にチューニングすることで、より正確な検出や計算量の軽減などを行うことが可能となります。

より実用的なプログラムへ - GUIの実装

次に、この輪郭抽出のプログラムをより実用的なものにしていきたいと思います。先程解説したように、正確な輪郭の抽出や、計算量を抑えた分析のためには、適切なパラメータのチューニングが必要となります。しかし、調整の際に毎回プログラムを修正してビルドをやりなおすのは効率が良くありません。

このように、状況に応じて細かなパラメータを調整した際に便利なアドオンを紹介します。ofxSimpleGuiTooというアドオンは、プログラム内で用いられているパラメータやbool値のtrue/falseの切り替えなどを、簡単なGUIを通してプログラムを作動させながらリアルタイムに行うことが可能です。さらに、分析中の画像を複数枚同時に小窓の中に表示することができるので、今回のようなOpenCVを用いた画像解析のプログラムにはうってつけです。

パラメータを設定した結果は、最終的にXML形式の外部ファイルとして保存され、次回プログラムを起動した際には調整したパラメータを再度読み込んで前回の状態を復元する機能もあります。この機能を活用することで、展示する場所の状況を記録しおくことが可能となり、作品制作の際の強力なサポートとなるでしょう。

ofxSimpleGuiTooを使用するためには、同時に付随するアドオンをいくつか読み込む必要があります。下記にofxSimpleGuiTooを作動させるためのアドオンを列挙します。

  • ofxSimpleGuiToo - GUI本体
  • ofxMSAInteractiveObject - インタラクティブな図形を作成するためのアドオン
  • ofxXmlSettings - 設定した項目をXML外部ファイルとして保存し、次回起動の際に再度読み込むための機能

これに加えて、ofxOpenCvも必要となるので、「グループとファイル」の中のaddonsフォルダは下記のようになります。

XcodeScreenSnapz002.png

アドオンの設定が完了したら、以下のプログラムを入力します。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "ofxOpenCv.h"
#include "ofxSimpleGuiToo.h"

//あらかじめ録画したビデオを使用する場合には、ここをコメントアウト
//#define _USE_LIVE_VIDEO

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //ライブカメラを使用する際には、カメラ入力を準備
    ofVideoGrabber 		vidGrabber;
    ofVideoPlayer 		vidPlayer;

    ofxCvColorImage colorImg; //オリジナルのカラー映像
    ofxCvGrayscaleImage grayImage; //グレースケールに変換後
    ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像
    ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分
    ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス
    
    bool bLearnBakground; //背景画像を登録したか否か
    bool showCvAnalysis; //解析結果を表示するか
    int threshold; //2値化の際の閾値
    bool liveVideo; //カメラ入力を使用するか否か

    //輪郭検出時のパラメータ
    float minBlobSize; //物体の最小サイズ
    float maxBlobSize; //物体の最大サイズ
    int maxNumBlobs; //認識する物体の最大数
    bool findHoles; //穴を検出するか
    bool useApproximation; //近似値を使用するか
    
    //GUI
    ofxSimpleGuiToo gui; 
};
#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //画面の基本設定
    ofBackground(0, 0, 0);
    ofEnableAlphaBlending();
    ofSetFrameRate(60);

    //カメラから映像を取り込んで表示
    vidGrabber.setVerbose(true);
    vidGrabber.initGrabber(320,240);

    vidPlayer.loadMovie("fingers.mov");
    vidPlayer.play();
    
    //使用する画像の領域を確保
    colorImg.allocate(320,240);
    grayImage.allocate(320,240);
    grayBg.allocate(320,240);
    grayDiff.allocate(320,240);
    
    //変数の初期化
    bLearnBakground = false;
    showCvAnalysis = false;
    
    //GUIを設定
    gui.setup();
    gui.config->gridSize.x = 250;
    gui.addContent("grayImage", grayImage); //グレーに変換した映像
    gui.addContent("grayBg", grayBg); //キャプチャーした背景
    gui.addContent("grayDiff", grayDiff); //2値化した差分画像
    gui.addFPSCounter(); //FPS
    gui.addSlider("threshold", threshold, 0, 400); //2値化の閾値
    gui.addToggle("use live video", liveVideo); //カメラを使うかどうか
	gui.addToggle("findHoles", findHoles); //穴を検出するか
	gui.addToggle("useApproximation", useApproximation); //近似法を使うか
	gui.addSlider("minBlobSize", minBlobSize, 0, 1); //検出する物体の最小サイズ
	gui.addSlider("maxBlobSize", maxBlobSize, 0, 1); //検出する物体の最大サイズ
	gui.addSlider("maxNumBlobs", maxNumBlobs, 1, 100); //検出する物体の最大数
    gui.loadFromXML();
}

void testApp::update(){
    //新規フレームの取り込みをリセット
    bool bNewFrame = false;
    
    if (liveVideo) {
        //カメラ使用の場合はカメラから新規フレーム取り込み
        vidGrabber.grabFrame();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidGrabber.isFrameNew();
    } else {
        vidPlayer.idleMovie();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidPlayer.isFrameNew();
    }
    
    //フレームが切り替わった際のみ画像を解析
    if (bNewFrame){
        if (liveVideo) {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidGrabber.getPixels(), 320,240);
            //左右反転
            colorImg.mirror(false, true);
        } else {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidPlayer.getPixels(), 320,240);    
        }

        //カラーのイメージをグレースケールに変換
        grayImage = colorImg;
        
        //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする
        if (bLearnBakground == true){
            grayBg = grayImage;
            bLearnBakground = false;
        }
        
        //グレースケールのイメージと取り込んだ背景画像との差分を算出
        grayDiff.absDiff(grayBg, grayImage);
        //画像を2値化(白と黒だけに)する
        grayDiff.threshold(threshold);
        //2値化した画像から輪郭を抽出する
        contourFinder.findContours(grayDiff,
                                   minBlobSize * minBlobSize * grayDiff.getWidth() * grayDiff.getHeight(),
                                   maxBlobSize * maxBlobSize * grayDiff.getWidth() * grayDiff.getHeight(),
                                   maxNumBlobs, findHoles, useApproximation);
    }
}

void testApp::draw(){
    //解析結果を表示
    contourFinder.draw(0, 0, ofGetWidth(), ofGetHeight());
    //GUIを表示
    gui.draw();
}

void testApp::keyPressed  (int key){
    //キー入力でモード切り替え
    switch (key){
        case 'f':
            //フルスクリーンのon/off
            ofToggleFullscreen();
            
        case 'g':
            //Gui表示のon/off
            gui.toggleDraw();
            break;
            
        case ' ':
            //背景画像を新規に取り込む
            bLearnBakground = true;
            break;
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

OpenCvGuiScreenSnapz003.png

[g]キーでGUIの表示/非表示の切り替えができます。GUIを表示した解析のパラメータを細かく設定し、完了したらGUIを隠して使用します。

輪郭抽出の活用 – 移動点の中心から湧き出すパーティクル

では次に、この輪郭を検出した物体の情報を活用してみましょう。検出した物体(Blob)は、その図形をとりかこむ長方形の座標と重心点の座標を取得することが可能です。この重心の点を利用して、そこからパーティクルが湧き出てくるような表現に挑戦してみましょう。

パーティクル表現の基本原理は、以前の講義「openFrameworksで、オブジェクト指向プログラミング(OOP) 後編」で作成したパーティクルを活用しています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "MyCircle.h"

#include "ofxOpenCv.h"
#include "ofxSimpleGuiToo.h"

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //ライブカメラを使用する際には、カメラ入力を準備
    ofVideoGrabber 		vidGrabber;
    ofVideoPlayer 		vidPlayer;

    ofxCvColorImage colorImg; //オリジナルのカラー映像
    ofxCvGrayscaleImage grayImage; //グレースケールに変換後
    ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像
    ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分
    ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス
    
    bool bLearnBakground; //背景画像を登録したか否か
    bool showCvAnalysis; //解析結果を表示するか
    int threshold; //2値化の際の敷居値

    //輪郭検出時のパラメータ
    float minBlobSize;
    float maxBlobSize;
    int maxNumBlobs;
    bool findHoles;
    bool useApproximation;
    bool liveVideo;
    
    //GUI
    ofxSimpleGuiToo gui;
    
    //パーティクルの動的配列
    vector  circles;    
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //画面の基本設定
    ofBackground(0, 0, 0);
    ofEnableAlphaBlending();
    ofSetFrameRate(60);

    //カメラから映像を取り込んで表示
    vidGrabber.setVerbose(true);
    vidGrabber.initGrabber(320,240);

    vidPlayer.loadMovie("fingers.mov");
    vidPlayer.play();
    
    //使用する画像の領域を確保
    colorImg.allocate(320,240);
    grayImage.allocate(320,240);
    grayBg.allocate(320,240);
    grayDiff.allocate(320,240);
    
    //変数の初期化
    bLearnBakground = false;
    showCvAnalysis = false;

    //GUIを設定
    gui.setup();
    gui.config->gridSize.x = 250;
    gui.addContent("grayImage", grayImage);
    gui.addContent("grayDiff", grayDiff);
    gui.addContent("findConter", contourFinder);
    gui.addFPSCounter();
    gui.addSlider("threshold", threshold, 0, 400);
    gui.addToggle("use live video", liveVideo);
	gui.addToggle("findHoles", findHoles);
	gui.addToggle("useApproximation", useApproximation);
	gui.addSlider("minBlobSize", minBlobSize, 0, 1);
	gui.addSlider("maxBlobSize", maxBlobSize, 0, 1);
	gui.addSlider("maxNumBlobs", maxNumBlobs, 1, 100);
    gui.loadFromXML();
}


void testApp::update(){
    //新規フレームの取り込みをリセット
    bool bNewFrame = false;
    
    if (liveVideo) {
        //カメラ使用の場合はカメラから新規フレーム取り込み
        vidGrabber.grabFrame();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidGrabber.isFrameNew();
    } else {
        vidPlayer.idleMovie();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidPlayer.isFrameNew();
    }
    
    //フレームが切り替わった際のみ画像を解析
    if (bNewFrame){
        if (liveVideo) {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidGrabber.getPixels(), 320,240);
            //左右反転
            colorImg.mirror(false, true);
        } else {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidPlayer.getPixels(), 320,240);    
        }

        //カラーのイメージをグレースケールに変換
        grayImage = colorImg;
        
        //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする
        if (bLearnBakground == true){
            grayBg = grayImage;
            bLearnBakground = false;
        }
        
        //グレースケールのイメージと取り込んだ背景画像との差分を算出
        grayDiff.absDiff(grayBg, grayImage);
        //画像を2値化(白と黒だけに)する
        grayDiff.threshold(threshold);
        //2値化した画像から輪郭を抽出する
        contourFinder.findContours(grayDiff,
                                   minBlobSize * minBlobSize * grayDiff.getWidth() * grayDiff.getHeight(),
                                   maxBlobSize * maxBlobSize * grayDiff.getWidth() * grayDiff.getHeight(),
                                   maxNumBlobs, findHoles, useApproximation);
        
        //検出した解析結果からBlobの中心位置を求め
        //中心位置にパーティクルを追加
        for (int i = 0; i < contourFinder.nBlobs; i++){
            circles.push_back(new MyCircle(ofPoint(contourFinder.blobs[i].centroid.x, 
                                                   contourFinder.blobs[i].centroid.y), 
                                           ofRandom(0.5, 4.0), 0.4, 0.1, 10));
        }
    }
    
    //パーティクル更新
    for(vector ::iterator it = circles.begin(); it != circles.end();){
        (*it)->update();
        if ((*it)->dead) {
            delete (*it);
            it = circles.erase(it);
        } else {
            ++it;
        }
    }
}

void testApp::draw(){
    ofFill();
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    ofPushMatrix();
    //画面サイズいっぱいに表示されるようリスケール
    glScalef((float)ofGetWidth() / (float)grayDiff.width, 
             (float)ofGetHeight() / (float)grayDiff.height, 
             1.0f);
    //ビデオ映像表示
    ofSetColor(255, 255, 255, 127);
    colorImg.draw(0, 0);
    
    //パーティクルを表示
    for(vector ::iterator it = circles.begin(); it != circles.end(); ++it){
        (*it)->draw();
    }
    ofPopMatrix();
    //GUIを表示
    gui.draw();
}

void testApp::keyPressed  (int key){
    //キー入力でモード切り替え
    switch (key){
        case 'f':
            //フルスクリーンのon/off
            ofToggleFullscreen();
            
        case 'g':
            //Gui表示のon/off
            gui.toggleDraw();
            break;
            
        case ' ':
            //背景画像を新規に取り込む
            bLearnBakground = true;
            break;
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

CustomCircle.h

#ifndef _MY_CIRCLE
#define _MY_CIRCLE

#include "ofMain.h"

class MyCircle {
    
public: 
    //コンストラクタ
    MyCircle(ofPoint pos, float radius, float maxSpeed = 0.4, float phaseSpeed = 0.06, float lifeLength = 20.0);
    //デストラクタ
    ~MyCircle();
    //半径の更新
    void update();
    //円を描く
    void draw();
    //アクセサ
    void setPos(ofPoint pos);
    void setRadius(float radius);
    //円の移動スピード
    ofPoint speed;
    //移動スピードの最大値
    float maxSpeed;
    //収縮を制御する正弦波の位相
    float phase;
    //収縮スピード
    float phaseSpeed;
    //生死の判定
    bool dead;
    //寿命
    float lifeLength;
    
private:
    //プロパティはprivateで宣言
    //円の位置
    ofPoint pos;
    //円の半径
    float radius;
};

#endif

CustomCircle.cpp

#include "MyCircle.h"

//コンストラクタ
MyCircle::MyCircle(ofPoint _pos, float _radius, float _maxSpeed, float _phaseSpeed, float _lifeLength) 
{
    pos = _pos;
    radius = _radius;
    phaseSpeed = _phaseSpeed;
    maxSpeed = _maxSpeed;
    lifeLength = _lifeLength;
    
    //スピードを設定
    speed.x = ofRandom(-maxSpeed, maxSpeed);
    speed.y = ofRandom(-maxSpeed, maxSpeed);
    //初期位相
    phase = ofRandom(0, PI*2);
    //生死
    dead = false;
}

//デストラクタ
MyCircle::~MyCircle(){}

void MyCircle::update()
{
    //円の半径の伸び縮みの位相を更新
    phase += phaseSpeed;
    if (phase > lifeLength) {
        dead = true;
    }
    //位置を更新
    pos += speed;
}

void MyCircle::draw()
{
    //パーティクルを描く
    float r = sin(phase)*radius;
    ofSetColor(127, 255, 255, 11); 
    ofCircle(pos.x, pos.y, radius*2.0);
    ofSetColor(31, 127, 255, 127); 
    ofCircle(pos.x, pos.y, r);
    ofSetColor(255, 255, 255); 
    ofCircle(pos.x, pos.y, r*0.25);
}

void MyCircle::setPos(ofPoint _pos)
{
    pos = _pos;
}

void MyCircle::setRadius(float _radius)
{
    radius = _radius;
}

OpenCvParticleScreenSnapz002.png

OpenCVを応用したプログラム – ベクトル場(Vector Field)

次に、単純な座標の情報だけを活用するのではなく、映像全体の動きや物体の移動する流れにより密接に絡みあった表現を追求していきたと思います。

ここで、ベクトル場(Vector Field)という概念を導入したいと思います。ベクトル場とは、空間内のベクトル的な量の分布を表したものです。例えば、流体の速さと向きや、磁力や重力の強さと向きなどが空間内にどのように分布しているかを表現するために用いられます。

OpenCVで解析した映像の変化を、このベクトル場としてとらえ、空間内でのベクトルの量と向きに変換してみます。すると、単純な座標ではなく映像の動きを流れとして感じさせるような表現が可能となってきます。

このプログラムのvectorFieldクラスは、映像の差分情報からベクトル場を計算しています。画面全体のベクトル場を算出して、その中にパーティクルをランダムに配置し、ベクトル場の流れをパーティクル一つ一つに力として伝えています。その結果、パーティクルは映像の動きに押し流されるように、空間内を漂います。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "particle.h"
#include "vectorField.h"

#include "ofxOpenCv.h"
#include "ofxSimpleGuiToo.h"

//あらかじめ録画したビデオを使用する場合には、ここをコメントアウト
//#define _USE_LIVE_VIDEO

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //ライブカメラを使用する際には、カメラ入力を準備
    ofVideoGrabber 		vidGrabber;
    ofVideoPlayer 		vidPlayer;

    ofxCvColorImage colorImg; //オリジナルのカラー映像
    ofxCvGrayscaleImage grayImage; //グレースケールに変換後
    ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像
    ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分
    ofxCvGrayscaleImage grayDiffSmall; //縮小した差分イメージ
    ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス
    
    bool bLearnBakground; //背景画像を登録したか否か
    bool showCvAnalysis; //解析結果を表示するか
    int threshold; //2値化の際の敷居値
    bool liveVideo; //カメラ入力を使用するか
    bool drawColorImg; //ビデオ表示のon/off
    bool drawVectorFirld; //VectorField表示のon/off
    bool bForceInward; //重力の向き

    //GUI
    ofxSimpleGuiToo gui; 
    
    //ベクトル場
    vectorField VF;

    //パーティクル
    vector  particles;
    int particleNum;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //画面の基本設定
    ofBackground(0, 0, 0);
    ofSetFrameRate(60);
    
    //カメラから映像を取り込んで表示
    vidGrabber.setVerbose(true);
    vidGrabber.initGrabber(320,240);
    
    vidPlayer.loadMovie("fingers.mov");
    vidPlayer.play();
    
    //使用する画像の領域を確保
    colorImg.allocate(320,240);
    grayImage.allocate(320,240);
    grayBg.allocate(320,240);
    grayDiff.allocate(320,240);
    grayDiffSmall.allocate(60, 40);
    
    //変数の初期化
    bLearnBakground = true;
    showCvAnalysis = false;
    
    //ベクトル場の初期化
    VF.setupField(60, 40, ofGetWidth(), ofGetHeight());
    
    //パーティクル生成
    particleNum = 8000;
    for (int i = 0; i < particleNum; i++) {
        particle* p = new particle();
        p->setInitialCondition(ofRandom(0,ofGetWidth()),ofRandom(0,ofGetHeight()),0,0);
        particles.push_back(p);
    }
    
    //GUIの初期設定
    gui.setup();
    gui.config->gridSize.x = 250;
    gui.addContent("grayImage", grayImage);
    gui.addContent("grayDiff", grayDiff);
    gui.addContent("grayDiffSmall", grayDiffSmall);
    gui.addFPSCounter();
    gui.addSlider("threshold", threshold, 0, 400);
    gui.addToggle("use live video", liveVideo);
    gui.addToggle("draw video", drawColorImg);
    gui.addToggle("draw vector field", drawVectorFirld);
    gui.addToggle("force inward", bForceInward);
    gui.loadFromXML();
}


void testApp::update(){
    //新規フレームの取り込みをリセット
    bool bNewFrame = false;
    
    if (liveVideo) {
        vidPlayer.stop();
        //カメラ使用の場合はカメラから新規フレーム取り込み
        vidGrabber.grabFrame();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidGrabber.isFrameNew();
    } else {
        vidPlayer.play();
        vidPlayer.idleMovie();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidPlayer.isFrameNew();
    }
    
    //フレームが切り替わった際のみ画像を解析
    if (bNewFrame){
        if (liveVideo) {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidGrabber.getPixels(), 320,240);
            //左右反転
            colorImg.mirror(false, true);
        } else {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidPlayer.getPixels(), 320,240);    
        }
        
        //カラーのイメージをグレースケールに変換
        grayImage = colorImg;
        
        //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする
        if (bLearnBakground == true){
            grayBg = grayImage;
            bLearnBakground = false;
        }
        
        //グレースケールのイメージと取り込んだ背景画像との差分を算出
        grayDiff.absDiff(grayBg, grayImage);
        //画像を2値化(白と黒だけに)する
        grayDiff.threshold(threshold);
        //縮小した差分イメージに画像をコピー
        grayDiffSmall.scaleIntoMe(grayDiff);
        //ぼかし
        grayDiffSmall.blur(5);
        //ベクトル場に差分イメージを適用
        VF.setFromPixels(grayDiffSmall.getPixels(), bForceInward, 0.05f);
    }
    
    //ベクトル場に発生した力を計算し、パーティクルにかかる力を算出
    for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){
		(*it)->resetForce();
		ofxVec2f frc;
		frc = VF.getForceFromPos((*it)->pos.x, (*it)->pos.y);
		(*it)->addForce(frc.x, frc.y);
		(*it)->addDampingForce();
		(*it)->update();
        
	}
}

void testApp::draw(){
    //ofEnableAlphaBlending();
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    
    if (drawColorImg) {
        //カラーイメージを描く
        ofSetColor(255, 255, 255, 63);
        colorImg.draw(0, 0, ofGetWidth(), ofGetHeight());
    }
    
    if (drawVectorFirld) {
        //ベクトル場を描く
        ofSetColor(255, 255, 255, 127);
        VF.draw();
    }
    
    //パーティクルを描く
    ofFill();
    ofSetColor(63, 127, 255, 200);
    for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){
        (*it)->draw();
    }
    
    //GUIを描く
    gui.draw();
}

void testApp::keyPressed  (int key){
    //キー入力でモード切り替え
    switch (key){
            
        case 'f':
            //フルスクリーンのon/off
            ofToggleFullscreen();
            
        case 'g':
            //Gui表示のon/off
            gui.toggleDraw();
            break;
            
        case 't':
            //重力を反転
            bForceInward = !bForceInward;
            break;
            
        case ' ':
            //背景画像を新規に取り込む
            bLearnBakground = true;
            //particleをリセット
            for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){
                (*it)->setInitialCondition(ofRandom(0,ofGetWidth()), ofRandom(0,ofGetHeight()), 0,0);
            }
            break;
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

vectorField.h

#include "testApp.h"

void testApp::setup(){
    //画面の基本設定
    ofBackground(0, 0, 0);
    ofSetFrameRate(60);
    
    //カメラから映像を取り込んで表示
    vidGrabber.setVerbose(true);
    vidGrabber.initGrabber(320,240);
    
    vidPlayer.loadMovie("fingers.mov");
    vidPlayer.play();
    
    //使用する画像の領域を確保
    colorImg.allocate(320,240);
    grayImage.allocate(320,240);
    grayBg.allocate(320,240);
    grayDiff.allocate(320,240);
    grayDiffSmall.allocate(60, 40);
    
    //変数の初期化
    bLearnBakground = true;
    showCvAnalysis = false;
    
    //ベクトル場の初期化
    VF.setupField(60, 40, ofGetWidth(), ofGetHeight());
    
    //パーティクル生成
    particleNum = 8000;
    for (int i = 0; i < particleNum; i++) {
        particle* p = new particle();
        p->setInitialCondition(ofRandom(0,ofGetWidth()),ofRandom(0,ofGetHeight()),0,0);
        particles.push_back(p);
    }
    
    //GUIの初期設定
    gui.setup();
    gui.config->gridSize.x = 250;
    gui.addContent("grayImage", grayImage);
    gui.addContent("grayDiff", grayDiff);
    gui.addContent("grayDiffSmall", grayDiffSmall);
    gui.addFPSCounter();
    gui.addSlider("threshold", threshold, 0, 400);
    gui.addToggle("use live video", liveVideo);
    gui.addToggle("draw video", drawColorImg);
    gui.addToggle("draw vector field", drawVectorFirld);
    gui.addToggle("force inward", bForceInward);
    gui.loadFromXML();
}

void testApp::update(){
    //新規フレームの取り込みをリセット
    bool bNewFrame = false;
    
    if (liveVideo) {
        vidPlayer.stop();
        //カメラ使用の場合はカメラから新規フレーム取り込み
        vidGrabber.grabFrame();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidGrabber.isFrameNew();
    } else {
        vidPlayer.play();
        vidPlayer.idleMovie();
        //新規にフレームが切り替わったか判定
        bNewFrame = vidPlayer.isFrameNew();
    }
    
    //フレームが切り替わった際のみ画像を解析
    if (bNewFrame){
        if (liveVideo) {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidGrabber.getPixels(), 320,240);
            //左右反転
            colorImg.mirror(false, true);
        } else {
            //取り込んだフレームを画像としてキャプチャ
            colorImg.setFromPixels(vidPlayer.getPixels(), 320,240);    
        }
        
        //カラーのイメージをグレースケールに変換
        grayImage = colorImg;
        
        //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする
        if (bLearnBakground == true){
            grayBg = grayImage;
            bLearnBakground = false;
        }
        
        //グレースケールのイメージと取り込んだ背景画像との差分を算出
        grayDiff.absDiff(grayBg, grayImage);
        //画像を2値化(白と黒だけに)する
        grayDiff.threshold(threshold);
        //縮小した差分イメージに画像をコピー
        grayDiffSmall.scaleIntoMe(grayDiff);
        //ぼかし
        grayDiffSmall.blur(5);
        //ベクトル場に差分イメージを適用
        VF.setFromPixels(grayDiffSmall.getPixels(), bForceInward, 0.05f);
    }
    
    //ベクトル場に発生した力を計算し、パーティクルにかかる力を算出
    for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){
		(*it)->resetForce();
		ofxVec2f frc;
		frc = VF.getForceFromPos((*it)->pos.x, (*it)->pos.y);
		(*it)->addForce(frc.x, frc.y);
		(*it)->addDampingForce();
		(*it)->update();
        
	}
}

void testApp::draw(){
    //ofEnableAlphaBlending();
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    
    if (drawColorImg) {
        //カラーイメージを描く
        ofSetColor(255, 255, 255, 63);
        colorImg.draw(0, 0, ofGetWidth(), ofGetHeight());
    }
    
    if (drawVectorFirld) {
        //ベクトル場を描く
        ofSetColor(255, 255, 255, 127);
        VF.draw();
    }
    
    //パーティクルを描く
    ofFill();
    ofSetColor(63, 127, 255, 200);
    for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){
        (*it)->draw();
    }
    
    //GUIを描く
    gui.draw();
}

void testApp::keyPressed  (int key){
    //キー入力でモード切り替え
    switch (key){
            
        case 'f':
            //フルスクリーンのon/off
            ofToggleFullscreen();
            
        case 'g':
            //Gui表示のon/off
            gui.toggleDraw();
            break;
            
        case 't':
            //重力を反転
            bForceInward = !bForceInward;
            break;
            
        case ' ':
            //背景画像を新規に取り込む
            bLearnBakground = true;
            //particleをリセット
            for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){
                (*it)->setInitialCondition(ofRandom(0,ofGetWidth()), ofRandom(0,ofGetHeight()), 0,0);
            }
            break;
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

vectorField.cpp

#include "vectorField.h"



//------------------------------------------------------------------------------------
vectorField::vectorField(){
	
	/*fieldWidth  = 60;
     fieldHeight = 40;
	 */
}

//------------------------------------------------------------------------------------
void vectorField::setupField(int innerW, int innerH, int outerW, int outerH){
	
	fieldWidth			= innerW;
	fieldHeight			= innerH;
	externalWidth		= outerW;
	externalHeight		= outerH;
	
	field.clear();
	
	fieldSize = fieldWidth * fieldHeight;
	for (int i = 0; i < fieldSize; i++){
		ofxVec2f pt;  
		pt.set(0,0);
		field.push_back(pt);
	}
}


//------------------------------------------------------------------------------------
void vectorField::clear(){
    for (int i = 0; i < fieldSize; i++){
        field[i].set(0,0);
    }
}


//------------------------------------------------------------------------------------
void vectorField::fadeField(float fadeAmount){
	for (int i = 0; i < fieldSize; i++){
        field[i].set(field[i].x*fadeAmount,field[i].y*fadeAmount);
    }
}

//------------------------------------------------------------------------------------
void vectorField::randomizeField(float scale){
	for (int i = 0; i < fieldSize; i++){
        // random between -1 and 1
        float x = (float)(ofRandom(-1,1)) * scale;
        float y = (float)(ofRandom(-1,1)) * scale;
        field[i].set(x,y);
    }
}

//------------------------------------------------------------------------------------
void vectorField::draw(){
	
    float scalex = (float)externalWidth / (float)fieldWidth;
    float scaley = (float)externalHeight / (float)fieldHeight;
	
    for (int i = 0; i < fieldWidth; i++){
        for (int j = 0; j < fieldHeight; j++){
            
            // pos in array
            int pos = j * fieldWidth + i;
            // pos externally
            float px = 	i * scalex;
            float py = 	j * scaley;
            float px2 = px + field[pos].x * 5;
            float py2 = py + field[pos].y * 5;
			
            ofLine(px,py, px2, py2);
			
			
			// draw an baseline to show direction
			// get the line as vector, calculate length, then normalize. 
			// rotate and draw based on length
			
			ofxVec2f line;
			line.set(px2-px, py2-py);
			float length = line.length();
			line.normalize();
			line.rotate(90);  // these are angles in degrees
			ofLine(px - line.x*length*0.2, 
                   py - line.y*length*0.2, 
                   px + line.x*length*0.2, 
                   py + line.y*length*0.2);
			
			
        }
    }
}


//------------------------------------------------------------------------------------
ofxVec2f vectorField::getForceFromPos(float xpos, float ypos){
	
	ofxVec2f frc;
	frc.set(0,0);
	
	// convert xpos and ypos into pcts = 
	float xPct = xpos / (float)externalWidth;
	float yPct = ypos / (float)externalHeight;
	
	// if we are less then 0 or greater then 1 in x or y, return no force.
	if ((xPct < 0 || xPct > 1) || (yPct < 0 || yPct > 1)){
		return frc;	
	} 
	
    // where are we in the array
    int fieldPosX = (int)(xPct * fieldWidth);
    int fieldPosY = (int)(yPct * fieldHeight);
    
    // saftey :)
    fieldPosX = MAX(0, MIN(fieldPosX, fieldWidth-1));
    fieldPosY = MAX(0, MIN(fieldPosY, fieldHeight-1));
    
    // pos in array
    int pos = fieldPosY * fieldWidth + fieldPosX;
	
	frc.set(field[pos].x * 0.1, field[pos].y * 0.1 );  // scale here as values are pretty large.
	return frc;
}

//------------------------------------------------------------------------------------
void vectorField::addInwardCircle(float x, float y, float radius, float strength){
	
    // x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
				
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x += unit_px * strongness;
                field[pos].y += unit_py * strongness;
            }
        }
    }
}


//------------------------------------------------------------------------------------
void vectorField::addOutwardCircle(float x, float y, float radius, float strength){
	
	
	// x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
    
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
            
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
                
				float pct = 1.0f - (distance / fieldRadius);
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x -= unit_px * strongness;
                field[pos].y -= unit_py * strongness;
            }
        }
    }
}

//------------------------------------------------------------------------------------
void vectorField::addClockwiseCircle(float x, float y, float radius, float strength){
	
	
	
    // x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
				
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x += unit_py * strongness;   /// Note: px and py switched, for perpendicular
                field[pos].y -= unit_px * strongness;
            }
        }
    }
}



//------------------------------------------------------------------------------------
void vectorField::addCounterClockwiseCircle(float x, float y, float radius, float strength){
	
	
	
    // x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
				
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x -= unit_py * strongness;   /// Note: px and py switched, for perpendicular
                field[pos].y += unit_px * strongness;
            }
        }
    }
}


//------------------------------------------------------------------------------------
void vectorField::addVectorCircle(float x, float y, float vx, float vy, float radius, float strength){
	
	
	
	// x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
                float strongness = strength * pct;
                field[pos].x += vx * strongness;   
                field[pos].y += vy * strongness;
            }
        }
    }
}

//------------------------------------------------------------------------------------
void vectorField::setFromPixels(unsigned char * pixels, bool bMoveTowardsWhite, float strength){
	
	clear();
	
	for (int i = 1; i < fieldWidth-1; i++){
        for (int j = 1; j < fieldHeight-1; j++){
			int pos = j * fieldWidth + i;
			// calc the difference in horiz and vert
			int nw = pixels[ (j-1) * fieldWidth + (i-1) ];
			int n_ = pixels[ (j-1) * fieldWidth + (i  ) ];
			int ne = pixels[ (j-1) * fieldWidth + (i+1) ];
			int _e = pixels[ (j  ) * fieldWidth + (i-1) ];
			int _w = pixels[ (j  ) * fieldWidth + (i+1) ];
			int sw = pixels[ (j+1) * fieldWidth + (i-1) ];
			int s_ = pixels[ (j+1) * fieldWidth + (i  ) ];
			int se = pixels[ (j+1) * fieldWidth + (i+1) ];
			
			float diffx = (nw + _w + sw) - (ne + _e + se);
			float diffy = (nw + n_ + ne) - (sw + s_ + se);
			if (bMoveTowardsWhite){
				diffx *= -1;
				diffy *= -1;
			}
			field[pos].x = diffx * strength;
			field[pos].y = diffy * strength;
			
        }
    }
}


//------------------------------------------------------------------------------------
vectorField::~vectorField(){
    
}

particle.h

#ifndef PARTICLE_H
#define PARTICLE_H

#include "ofMain.h"
#include "ofxVectorMath.h"

class particle
{
public:
    ofxVec2f pos;
    ofxVec2f vel;
    ofxVec2f frc;   // frc is also know as acceleration (newton says "f=ma")
    
    particle();
    virtual ~particle(){};
    
    void resetForce();
    void addForce(float x, float y);
    void addDampingForce();
    void setInitialCondition(float px, float py, float vx, float vy);
    void update();
    void draw();
    
    void bounceOffWalls();
    
    float damping;
    
protected:
private:
};

#endif // PARTICLE_H

particle.cpp

#include "particle.h"
#include "ofMain.h"

//------------------------------------------------------------
particle::particle(){
	setInitialCondition(0,0,0,0);
	damping = 0.08f;
}

//------------------------------------------------------------
void particle::resetForce(){
    // we reset the forces every frame
    frc.set(0,0);
}

//------------------------------------------------------------
void particle::addForce(float x, float y){
    // add in a force in X and Y for this frame.
    frc.x = frc.x + x;
    frc.y = frc.y + y;
}

//------------------------------------------------------------
void particle::addDampingForce(){
	
	// the usual way to write this is  vel *= 0.99
	// basically, subtract some part of the velocity 
	// damping is a force operating in the oposite direction of the 
	// velocity vector
	
    frc.x = frc.x - vel.x * damping;
    frc.y = frc.y - vel.y * damping;
}

//------------------------------------------------------------
void particle::setInitialCondition(float px, float py, float vx, float vy){
    pos.set(px,py);
	vel.set(vx,vy);
}

//------------------------------------------------------------
void particle::update(){	
	vel = vel + frc;
	pos = pos + vel;
}

//------------------------------------------------------------
void particle::draw(){
    ofCircle(pos.x, pos.y, 3);
}


//------------------------------------------------------------
void particle::bounceOffWalls(){
	
	// sometimes it makes sense to damped, when we hit
	bool bDampedOnCollision = true;
	bool bDidICollide = false;
	
	// what are the walls
	float minx = 0;
	float miny = 0;
	float maxx = ofGetWidth();
	float maxy = ofGetHeight();
	
	if (pos.x > maxx){
		pos.x = maxx; // move to the edge, (important!)
		vel.x *= -1;
		bDidICollide = true;
	} else if (pos.x < minx){
		pos.x = minx; // move to the edge, (important!)
		vel.x *= -1;
		bDidICollide = true;
	}
	
	if (pos.y > maxy){
		pos.y = maxy; // move to the edge, (important!)
		vel.y *= -1;
		bDidICollide = true;
	} else if (pos.y < miny){
		pos.y = miny; // move to the edge, (important!)
		vel.y *= -1;
		bDidICollide = true;
	}
	
	if (bDidICollide == true && bDampedOnCollision == true){
		vel *= 0.3;
	}
	
}

OpenCvGuiScreenSnapz002.png

OpenCVを応用したプログラム - Box2Dとの融合

最後にこのベクトル場を、より厳密な物理世界に適用してみましょう。

今度のプログラムは、一つ一つのパーティクルが単純なものではなく、物理演算を適用した物体に変更しています。物理演算には前回使用したBox2Dを活用しています。ベクトル場に配置されたパーティクルは、その力の影響を受けながら、重力やパーティクル同士の衝突などの物理法則が適用されています。このことにより、とてもリアルな粒の動きが再現されています。

testApp.h

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"
#include "vectorField.h"
#include "CustomCircle.h"

#include "ofxOpenCv.h"
#include "ofxSimpleGuiToo.h"
#include "ofxBox2d.h"

class testApp : public ofBaseApp {
    
public:
    void setup();
    void update();
    void draw();
    
    void keyPressed  (int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y );
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void windowResized(int w, int h);
    
    //カメラ入力を準備
    ofVideoGrabber vidGrabber;

    ofxCvColorImage colorImg; //オリジナルのカラー映像
    ofxCvGrayscaleImage grayImage; //グレースケールに変換後
    ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像
    ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分
    ofxCvGrayscaleImage grayDiffSmall; //縮小した差分イメージ
    ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス
    
    bool bLearnBakground; //背景画像を登録したか否か
    bool showCvAnalysis; //解析結果を表示するか
    int threshold; //2値化の際の敷居値
    bool liveVideo; //カメラ入力を使用するか
    bool drawColorImg; //ビデオ表示のon/off
    bool drawVectorFirld; //VectorField表示のon/off
    bool bForceInward; //重力の向き

    //GUI
    ofxSimpleGuiToo gui; 
    
    //ベクトル場
    vectorField VF;
    
    //box2d
    ofxBox2d box2d;
    float gravity; //重力
    float force; //押し出す力
    float vectorThreshold; //力を適用する閾値

    //ofxBox2dCircleを継承したクラス
    list  particles; 
    int particleNum;
};

#endif

testApp.cpp

#include "testApp.h"

void testApp::setup(){
    //画面の基本設定
    ofBackground(0, 0, 0);
    ofSetFrameRate(60);
    
    //カメラから映像を取り込んで表示
    vidGrabber.setVerbose(true);
    vidGrabber.initGrabber(320,240);
    
    //使用する画像の領域を確保
    colorImg.allocate(320,240);
    grayImage.allocate(320,240);
    grayBg.allocate(320,240);
    grayDiff.allocate(320,240);
    grayDiffSmall.allocate(60, 40);
    
    //変数の初期化
    bLearnBakground = true;
    showCvAnalysis = false;
    
    //ベクトル場の初期化
    VF.setupField(60, 40, ofGetWidth(), ofGetHeight());
    
    //GUIの初期設定
    gui.setup();
    gui.config->gridSize.x = 250;
    gui.addContent("grayImage", grayImage);
    gui.addContent("grayDiff", grayDiff);
    gui.addContent("grayDiffSmall", grayDiffSmall);
    gui.addFPSCounter();
    gui.addSlider("threshold", threshold, 0, 400);
    gui.addSlider("gravity", gravity, 0.0, 1.0);
    gui.addSlider("force", force, 0.0, 20.0);
    gui.addSlider("vector threshold", vectorThreshold, 0.0, 2.0);
    gui.addToggle("use live video", liveVideo);
    gui.addToggle("draw video", drawColorImg);
    gui.addToggle("draw vector field", drawVectorFirld);
    gui.addToggle("force inward", bForceInward);
    gui.loadFromXML();
    
    //Box2D初期設定
    box2d.init();
    box2d.setGravity(0, gravity);
    box2d.createBounds();
    box2d.setFPS(8);
    
    //パーティクル生成
    particleNum = 500;
    for (int i = 0; i < particleNum; i++) {
        CustomCircle* p = new CustomCircle();
        p->setPhysics(1.0, 0.0, 0.2);
        p->setup(box2d.getWorld(), 
                 ofRandom(0, ofGetWidth()), 
                 ofRandom(0, ofGetHeight()), 
                 ofRandom(7, 14), false);
        p->disableCollistion();
        particles.push_back(p);
    }
}


void testApp::update(){
    box2d.setGravity(0, gravity);
    box2d.update();
    
    //新規フレームの取り込みをリセット
    bool bNewFrame = false;
    //カメラから新規フレーム取り込み
    vidGrabber.grabFrame();
    //新規にフレームが切り替わったか判定
    bNewFrame = vidGrabber.isFrameNew();
    
    //フレームが切り替わった際のみ画像を解析
    if (bNewFrame){
        //取り込んだフレームを画像としてキャプチャ
        colorImg.setFromPixels(vidGrabber.getPixels(), 320,240);
        //左右反転
        colorImg.mirror(false, true);
        //カラーのイメージをグレースケールに変換
        grayImage = colorImg;
        //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする
        if (bLearnBakground == true){
            grayBg = grayImage;
            bLearnBakground = false;
        }
        //グレースケールのイメージと取り込んだ背景画像との差分を算出
        grayDiff.absDiff(grayBg, grayImage);
        //画像を2値化(白と黒だけに)する
        grayDiff.threshold(threshold);
        //縮小した差分イメージに画像をコピー
        grayDiffSmall.scaleIntoMe(grayDiff);
        //ぼかし
        grayDiffSmall.blur(5);
        //ベクトル場に差分イメージを適用
        VF.setFromPixels(grayDiffSmall.getPixels(), bForceInward, 0.05f);
        
        //ベクトル場に発生した力を計算し、パーティクルにかかる力を算出
        for(list ::iterator it = particles.begin(); it != particles.end(); ++it){
            ofxVec2f frc;
            frc = VF.getForceFromPos((*it)->getPosition().x, (*it)->getPosition().y);
            //設定した閾値を越えたら、VFの力を加える
            if (frc.length() > vectorThreshold) {
                (*it)->addForce(ofPoint(frc.x * force, frc.y * force), 1.0);
            }
            (*it)->update();        
        }
    }
}

void testApp::draw(){
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    
    if (drawColorImg) {
        //カラーイメージを描く
        ofSetColor(255, 255, 255, 127);
        colorImg.draw(0, 0, ofGetWidth(), ofGetHeight());
    }
    
    if (drawVectorFirld) {
        //ベクトル場を描く
        ofSetColor(255, 255, 255, 127);
        VF.draw();
    }
    
    //パーティクルを描く
    for(list ::iterator it = particles.begin(); it != particles.end(); ++it){
        (*it)->draw();
    }
    
    //GUIを描く
    gui.draw();
}

void testApp::keyPressed  (int key){
    //キー入力でモード切り替え
    switch (key){
        case 'f':
            //フルスクリーンのon/off
            ofToggleFullscreen();
            
        case 'g':
            //Gui表示のon/off
            gui.toggleDraw();
            break;
            
        case 't':
            //重力を反転
            bForceInward = !bForceInward;
            break;
            
        case ' ':
            //背景画像を新規に取り込む
            bLearnBakground = true;
            break;
    }
}

void testApp::keyReleased(int key){
}

void testApp::mouseMoved(int x, int y){
}

void testApp::mouseDragged(int x, int y, int button){
}

void testApp::mousePressed(int x, int y, int button){
}

void testApp::mouseReleased(int x, int y, int button){
}

void testApp::windowResized(int w, int h){
}

vectorField.h

#ifndef VECTORFIELD_H
#define VECTORFIELD_H


#include "ofMain.h"
#include "ofxVectorMath.h"

class vectorField {
    
public:
    
    // the internal dimensions of the field:    (ie, 60x40, etc)
    int fieldWidth;
    int fieldHeight;
    int fieldSize;   // total number of "pixels", ie w * h
    
    // the external dimensions of the field:   (ie, 1024x768)
    int externalWidth;
    int externalHeight;
    
    vector  field;
    
    vectorField();
    virtual ~vectorField();
    
    void setupField(int innerW, int innerH, int outerW, int outerH);   // pass in internal dimensions and outer dimensions
    
    void clear();
    void fadeField(float fadeAmount);
    void randomizeField(float scale);
    void draw();
    
    ofxVec2f	getForceFromPos(float xpos, float ypos);
    
    void addOutwardCircle(float x, float y, float radius, float strength);
    void addInwardCircle(float x, float y, float radius, float strength);
    void addClockwiseCircle(float x, float y, float radius, float strength);
    void addCounterClockwiseCircle(float x, float y, float radius, float strength);
    void addVectorCircle(float x, float y, float vx, float vy, float radius, float strength);
    
    void setFromPixels(unsigned char * pixels, bool bMoveTowardsWhite, float strength);
    
protected:
private:
};

#endif // VECTORFIELD_H

vectorField.cpp

#include "vectorField.h"



//------------------------------------------------------------------------------------
vectorField::vectorField(){
	
	/*fieldWidth  = 60;
    fieldHeight = 40;
	 */
}

//------------------------------------------------------------------------------------
void vectorField::setupField(int innerW, int innerH, int outerW, int outerH){
	
	fieldWidth			= innerW;
	fieldHeight			= innerH;
	externalWidth		= outerW;
	externalHeight		= outerH;
	
	field.clear();
	
	fieldSize = fieldWidth * fieldHeight;
	for (int i = 0; i < fieldSize; i++){
		ofxVec2f pt;  
		pt.set(0,0);
		field.push_back(pt);
	}
}


//------------------------------------------------------------------------------------
void vectorField::clear(){
    for (int i = 0; i < fieldSize; i++){
        field[i].set(0,0);
    }
}


//------------------------------------------------------------------------------------
void vectorField::fadeField(float fadeAmount){
	for (int i = 0; i < fieldSize; i++){
        field[i].set(field[i].x*fadeAmount,field[i].y*fadeAmount);
    }
}

//------------------------------------------------------------------------------------
void vectorField::randomizeField(float scale){
	for (int i = 0; i < fieldSize; i++){
        // random between -1 and 1
        float x = (float)(ofRandom(-1,1)) * scale;
        float y = (float)(ofRandom(-1,1)) * scale;
        field[i].set(x,y);
    }
}

//------------------------------------------------------------------------------------
void vectorField::draw(){
	
    float scalex = (float)externalWidth / (float)fieldWidth;
    float scaley = (float)externalHeight / (float)fieldHeight;
	
    for (int i = 0; i < fieldWidth; i++){
        for (int j = 0; j < fieldHeight; j++){

            // pos in array
            int pos = j * fieldWidth + i;
            // pos externally
            float px = 	i * scalex;
            float py = 	j * scaley;
            float px2 = px + field[pos].x * 5;
            float py2 = py + field[pos].y * 5;
			
            ofLine(px,py, px2, py2);
			
			
			// draw an baseline to show direction
			// get the line as vector, calculate length, then normalize. 
			// rotate and draw based on length
			
			ofxVec2f line;
			line.set(px2-px, py2-py);
			float length = line.length();
			line.normalize();
			line.rotate(90);  // these are angles in degrees
			ofLine(px - line.x*length*0.2, py - line.y*length*0.2, px + line.x*length*0.2, py + line.y*length*0.2);
			
			
        }
    }
}


//------------------------------------------------------------------------------------
ofxVec2f vectorField::getForceFromPos(float xpos, float ypos){
	
	ofxVec2f frc;
	frc.set(0,0);
	
	// convert xpos and ypos into pcts = 
	float xPct = xpos / (float)externalWidth;
	float yPct = ypos / (float)externalHeight;
	
	// if we are less then 0 or greater then 1 in x or y, return no force.
	if ((xPct < 0 || xPct > 1) || (yPct < 0 || yPct > 1)){
		return frc;	
	} 
	
    // where are we in the array
    int fieldPosX = (int)(xPct * fieldWidth);
    int fieldPosY = (int)(yPct * fieldHeight);

    // saftey :)
    fieldPosX = MAX(0, MIN(fieldPosX, fieldWidth-1));
    fieldPosY = MAX(0, MIN(fieldPosY, fieldHeight-1));

    // pos in array
    int pos = fieldPosY * fieldWidth + fieldPosX;
	
	frc.set(field[pos].x * 0.1, field[pos].y * 0.1 );  // scale here as values are pretty large.
	return frc;
}

//------------------------------------------------------------------------------------
void vectorField::addInwardCircle(float x, float y, float radius, float strength){
	
    // x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
				
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x += unit_px * strongness;
                field[pos].y += unit_py * strongness;
            }
        }
    }
}


//------------------------------------------------------------------------------------
void vectorField::addOutwardCircle(float x, float y, float radius, float strength){
	
	
	// x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);

	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){

            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
               
				float pct = 1.0f - (distance / fieldRadius);
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x -= unit_px * strongness;
                field[pos].y -= unit_py * strongness;
            }
        }
    }
}

//------------------------------------------------------------------------------------
void vectorField::addClockwiseCircle(float x, float y, float radius, float strength){
	
	
	
    // x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
				
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x += unit_py * strongness;   /// Note: px and py switched, for perpendicular
                field[pos].y -= unit_px * strongness;
            }
        }
    }
}



//------------------------------------------------------------------------------------
void vectorField::addCounterClockwiseCircle(float x, float y, float radius, float strength){
	
	
	
    // x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
				
                float strongness = strength * pct;
                float unit_px = ( fieldPosX - i) / distance;
                float unit_py = ( fieldPosY - j) / distance;
                field[pos].x -= unit_py * strongness;   /// Note: px and py switched, for perpendicular
                field[pos].y += unit_px * strongness;
            }
        }
    }
}


//------------------------------------------------------------------------------------
void vectorField::addVectorCircle(float x, float y, float vx, float vy, float radius, float strength){
	
	
	
	// x y and radius are in external dimensions.  Let's put them into internal dimensions:
	// first convert to pct:
	
	float pctx			= x / (float)externalWidth;
	float pcty			= y / (float)externalHeight;
	float radiusPct		= radius / (float)externalWidth;   
	
	// then, use them here: 
    int fieldPosX		= (int)(pctx * (float)fieldWidth);
    int fieldPosY		= (int)(pcty * (float)fieldHeight);
	float fieldRadius	= (float)(radiusPct * fieldWidth);
	
	// we used to do this search through every pixel, ie: 
	//    for (int i = 0; i < fieldWidth; i++){
	//    for (int j = 0; j < fieldHeight; j++){
	// but we can be smarter :)
	// now, as we search, we can reduce the "pixels" we look at by 
	// using the x y +/- radius.
	// use min and max to make sure we don't look over the edges 
	
	int startx	= MAX(fieldPosX - fieldRadius, 0);    
	int starty	= MAX(fieldPosY - fieldRadius, 0);
	int endx	= MIN(fieldPosX + fieldRadius, fieldWidth);
	int endy	= MIN(fieldPosY + fieldRadius, fieldHeight);
	
	
    for (int i = startx; i < endx; i++){
        for (int j = starty; j < endy; j++){
			
            int pos = j * fieldWidth + i;
            float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) +
                                         (fieldPosY-j)*(fieldPosY-j));
            
			if (distance < 0.0001) distance = 0.0001;  // since we divide by distance, do some checking here, devide by 0 is BADDDD
			
			if (distance < fieldRadius){
				
				float pct = 1.0f - (distance / fieldRadius);
                float strongness = strength * pct;
                field[pos].x += vx * strongness;   
                field[pos].y += vy * strongness;
            }
        }
    }
}

//------------------------------------------------------------------------------------
void vectorField::setFromPixels(unsigned char * pixels, bool bMoveTowardsWhite, float strength){
	
	clear();
	
	for (int i = 1; i < fieldWidth-1; i++){
        for (int j = 1; j < fieldHeight-1; j++){
			int pos = j * fieldWidth + i;
			// calc the difference in horiz and vert
			int nw = pixels[ (j-1) * fieldWidth + (i-1) ];
			int n_ = pixels[ (j-1) * fieldWidth + (i  ) ];
			int ne = pixels[ (j-1) * fieldWidth + (i+1) ];
			int _e = pixels[ (j  ) * fieldWidth + (i-1) ];
			int _w = pixels[ (j  ) * fieldWidth + (i+1) ];
			int sw = pixels[ (j+1) * fieldWidth + (i-1) ];
			int s_ = pixels[ (j+1) * fieldWidth + (i  ) ];
			int se = pixels[ (j+1) * fieldWidth + (i+1) ];
			
			float diffx = (nw + _w + sw) - (ne + _e + se);
			float diffy = (nw + n_ + ne) - (sw + s_ + se);
			if (bMoveTowardsWhite){
				diffx *= -1;
				diffy *= -1;
			}
			field[pos].x = diffx * strength;
			field[pos].y = diffy * strength;
			
        }
    }
}


//------------------------------------------------------------------------------------
vectorField::~vectorField(){

}

CustomCircle.h

#include "ofxVectorMath.h"
#include "ofxBox2d.h"

//ofxBox2dCircleを継承したクラスCustomCircleを定義
class CustomCircle : public ofxBox2dCircle {
public:
    void draw(); //円を描画する
};

CustomCircle.cpp

#include "CustomCircle.h"

//ofxBox2dCircleを継承した、オリジナルの円を描く
void CustomCircle::draw()
{
    float radius = getRadius(); //半径を取得
    glPushMatrix(); //座標を変更
    glTranslatef(getPosition().x, getPosition().y, 0); //物体の位置に座標を移動
    //円を描く
    ofFill();
    ofSetColor(127, 255, 255, 11); 
    ofCircle(0, 0, radius*2.0);
    ofSetColor(31, 127, 255, 127); 
    ofCircle(0, 0, radius);
    ofSetColor(255, 255, 255, 63); 
    ofCircle(0, 0, radius*0.5);
    glPopMatrix(); //座標を元に戻す
}

VectorFieldBox2DScreenSnapz001.png

openFrameworks, vector field study from Atsushi Tadokoro on Vimeo.

サンプルプログラムのダウンロード

今日とりあげた全てのプログラムは下記からダウンロード可能です。

CSSによる表現 – より細かなセレクタの指定、ポジショニング

今回は、CSSでよりきめ細かくスタイルの適用範囲を指定する方法を学んでいきます。


AS3によるインタラクション – マウスによるインタラクション 1 : マウスの位置を検知する

インタラクションとは

これまでのサンプルは、プログラムを実行すると、静止画であってもアニメーションであっても、ユーザがプログラムを終了するまではプログラムされた通りに自動的に再生されていました。言い換えると、このプログラムにユーザが介入する要素は、プログラムの起動と終了のみと言えるでしょう。

Flashでは、様々な手段でプログラムを実行している最中にも、ユーザがプログラムに対して介入することが可能です。ユーザがアクションを起こし、それに反応してプログラムは何らかのリアクションをする。ユーザはさらにその反応を見て、別のアクションを起こす。そうした、2つ以上の要素(この場合は、人間とコンピュータ上で動いているプログラム)の相互作用のことを、インタラクション(interaction)と呼びます。

プログラムにインタラクションを付加することで、より意外性に富んだ豊かな表現が可能となります。また、インタラクションをデザインしていくことは、これまでの伝統的なデザインの枠を越えた、新たな領域と言えるでしょう。どのようにしたら、効果的なインタラクションを実現できるのか、どのようにデザインすれば自然なインタラクションが生まれるのか、といったようにまだまだ未知の領域が残された刺激的な世界が拡がっています。

マウスによるインタラクション

一口にマウスによるインタラクションといっても、マウスの位置とマウスボタンが押されているorいない、という組合せによって、様々なユーザの動作が存在します。このそれぞれの動作とFlashの状態を組み合わせることで、様々なインタラクションが実現できます。Flashで用いられるマウスの動作には以下のものがあげられます。

  • 現在のマウスカーソルの位置を検知 -mouseX、mouseY
  • マウスボタンが押された瞬間を検知 – MouseEvent.MOUSE_DOWN
  • 押されていたマウスボタンを離した瞬間を検知 – MouseEvent.MOUSE_UP
  • マウスカーソルを動かした瞬間を検知 – mouseEvent.MOUSE_MOVE
  • マウスカーソルがステージに配置されたオブジェクトと重なって瞬間を検知 – mouseEvent.MOUSE_OVER
  • マウスカーソルがステージに配置されたオブジェクトから離れた瞬間を検知 – mouseEvent.MOUSE_OUT

今日はこれらのマウスに関する状態を検知する機能から、マウスの位置を知るためのmouseX、mouseYに注目して学んでいきます。

スクリプトのテンプレート

今日の授業で使用するスクリプトのテンプレートです。まず、AS3形式のFlashファイルを用意し、ドキュメントクラスを「Main」に指定します。その上で下記のコードを新規に作成したActionScriptファイルにペーストして、ファイル名を「Main.as」にしてFlaファイルと同じ場所に保存してください。

package {
  import flash.display.*;
  import flash.events.*;

  public class Main extends Sprite {
  //ここにグローバルな変数を記入

    public function Main() {
    //初期設定の項目を記入
           
      //フレーム更新のイベントリスナーの登録
      this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    //イベントハンドラ
    function enterFrameHandler(event:Event):void {

    }
  }
}

マウスの位置を検知する 1

まず、一番簡単なマウスを利用したインタラクションとして、マウスの位置の検出を試してみましょう。まず、Flaファイル側でムービークリップシンボルを生成し、クラス名「Ball」という名前でリンケージを作成します。

では、まずはこのムービークリップBallの位置を、マウスカーソルの位置で移動してみましょう。マウスカーソルの位置を検知するには、mouseXとmouseYというプロパティを用います。画面全体(stage)から見たマウスカーソルの位置は、以下のように指定します。

  • stage.mouseX – 画面全体を基準としたマウスカーソルのX座標
  • stage.mouseY – 画面全体を基準としたマウスカーソルのX座標

このプロパティを画面に配置したムービークリップの(x, y)座標に代入すると、マウスカーソルの位置にムービクリップが移動します。

package 
{
    import flash.display.*;
    import flash.events.*;

    public class Main extends Sprite
    {
       //ここにグローバルな変数を記入
       var ball:Ball = new Ball();

       public function Main()
       {
           //初期設定の項目を記入
           this.addChild(ball);

           //フレーム更新のイベントリスナーの登録
           this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
       }

       //イベントハンドラ
       function enterFrameHandler(event:Event):void
       {
           ball.x = stage.mouseX;
           ball.y = stage.mouseY;
       }
    }
}
/wp-content/uploads/2010/11/webmov101201_01.swf, 400, 300

マウスの位置を検知 – マウスカーソルの位置で位置と大きさを変化させる

単純にマウスカーソルの位置とムービークリップの位置を一致させるだけでなく、ムービクリップの別のプロパティとマウスカーソルの位置を関連付けてみましょう。例えば、マウスカーソルのX座標の位置はムービクリップのX座標のままで、マウスカーソルのY座標はムービークリップのサイズに適用するように変化させてみましょう。

また、ムービクリップをもう1つ配置して、X座標を画面の中心で反転するように配置してみましょう。

package 
{
    import flash.display.*;
    import flash.events.*;

    public class Main extends Sprite
    {
        //ここにグローバルな変数を記入
        var ball1:Ball = new Ball();
        var ball2:Ball = new Ball();


        public function Main()
        {
            //初期設定の項目を記入
            this.addChild(ball1);
            this.addChild(ball2);

            //フレーム更新のイベントリスナーの登録
            this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
        }

        //イベントハンドラ
        function enterFrameHandler(event:Event):void
        {
            ball1.x = stage.mouseX;
            ball2.x = stage.stageWidth - stage.mouseX;
            ball1.y = stage.stageHeight / 2;
            ball2.y = stage.stageHeight / 2;
            ball1.scaleX = ball1.scaleY = stage.mouseY / stage.stageHeight;
            ball2.scaleX = ball2.scaleY =  1.0 - stage.mouseY / stage.stageHeight;
        }
    }
}
/wp-content/uploads/2010/11/webmov101201_02.swf, 400, 300

マウスの位置を検知 – カーソルを追いかける動き

マウスカーソルの位置を利用したインタラクションの応用例として、マウスカーソルの位置にすぐに移動するのではなく、すこし遅れたタイミングでなめらかな動きでカーソルを追い掛ける動きをつくってみましょう。

なめらかな動きを実現するために、マウスカーソルの座標と、現在のムービークリップの座標の差分をとって、常に一定の数値で割り算した値だけ移動するようにします。すると、物体は徐々に減速しながら限りなくカーソルの場所に近付いていくという動きになります。割り算する数値を調整すると、減速するスピードを変化させることが可能です。この動きは簡単な数式で効果的なので、多くのFlashサイトで見掛けます。

package 
{
    import flash.display.*;
    import flash.events.*;
    import flashx.textLayout.formats.Float;

    public class Main extends Sprite
    {
        //ここにグローバルな変数を記入
        var ball:Ball = new Ball();
        var interpolate:Number;


        public function Main()
        {
            //初期設定の項目を記入
            this.addChild(ball);
            interpolate = 0.1;

            //フレーム更新のイベントリスナーの登録
            this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
        }

        //イベントハンドラ
        function enterFrameHandler(event:Event):void
        {
            ball.x += (stage.mouseX - ball.x) * interpolate; 
            ball.y += (stage.mouseY - ball.y) * interpolate;
        }
    }
}
/wp-content/uploads/2010/11/webmov101201_03.swf, 400, 300

マウスの位置を検知 – カーソルを追いかける動きを沢山の物体で

応用例として、先週学んだ配列を利用して、カーソルをなめらかに追い掛ける動きを、たくさんの物体でやってみましょう。

package 
{
    import flash.display.*;
    import flash.events.*;
    import flashx.textLayout.formats.Float;

    public class Main extends Sprite
    {
        //ここにグローバルな変数を記入
        var balls:Array = new Array();
        var ballNum:Number;
        var interpolate:Number;


        public function Main()
        {
            //20コのボールを生成
            ballNum = 20;
            //ボールのパラメータを設定
            for (var i:int = 0; i < ballNum; i++)
            {
                var b:Ball = new Ball();
                b.alpha = 0.1;
                b.scaleX = b.scaleY = 3.0 - i / ballNum * 2.0;
                this.addChild(b);
                balls.push(b);
            }

            //フレーム更新のイベントリスナーの登録
            this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
        }

        //イベントハンドラ
        function enterFrameHandler(event:Event):void
        {
            //20コのボールが補完しながらカーソルを追う
            for (var i:int = 0; i < ballNum; i++)
            {
                balls[i].x += (stage.mouseX - balls[i].x) * 0.02 * (i + 1);
                balls[i].y += (stage.mouseY - balls[i].y) * 0.02 * (i + 1);
            }
        }
    }
}
/wp-content/uploads/2010/11/webmov101201_04.swf, 400, 300