メディア芸術演習 VI - メディア・アート II
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値化した画像を解析(輪郭抽出、重心を算出)
この手順に従って、実際にopenFrameworksでプログラミングしていきましょう。
物体検出の基本プログラム
まず、輪郭検出までの手順を、そのまま順番に実装した素直なプログラムを作ってみましょう。
ソース映像や、それぞれの解析過程の映像のためのメモリ領域を確保した上で、ofxOpenCvのメソッドを活用しながら、徐々に輪郭を抽出しています。
openFrameworksからofxOpenCvの機能を使用するには、addonsの中にofxOpenCvを加える必要があります。addonsフォルダ内が下記のようになるようファイルを追加してください。
アドオンが追加できたら、下記のソースを入力します。
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){ }
このプログラムはキーボード入力によってモードを切り替えられる仕組みになっています。実際にキー入力をしてみて、処理の過程を実感してみましょう。
- [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フォルダは下記のようになります。
アドオンの設定が完了したら、以下のプログラムを入力します。
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){ }
[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; //パーティクルの動的配列 vectorcircles; }; #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; }
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; //パーティクル vectorparticles; 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; } }
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を継承したクラス listparticles; 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; vectorfield; 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(); //座標を元に戻す }
openFrameworks, vector field study from Atsushi Tadokoro on Vimeo.
サンプルプログラムのダウンロード
今日とりあげた全てのプログラムは下記からダウンロード可能です。