このページは,諸般の事情で Mac のみに対応した新しい内容にしてあります.ご注意ください.
ここでは,より高度なアドオンの扱いかたを,ofxOpenCv と ofxOpenNI を題材として学んでいきます.ofxOpenCv は openFrameworks に標準添付された画像処理用のアドオンです.ofxOpenNI は(前回に取り上げた ofxTrueTypeFontUC と同じように)外部から入手(ダウンロード)した Kinect 用のアドオンです.
注意:準備作業が抜けている人をよく見かけます.Xcode を正しくインストールしてください.Apple の Developer サイトから無料で入手できます.openFrameworks は最新版をダウンロードしてください.ここでは Xcode 8.1 + openFrameworks 0.9.8 の利用を想定しています.
ofxOpenCv は openFrameworks から OpenCV を利用できるようにするアドオンです.OpenCV は広く普及している画像処理ライブラリです.顔検出をはじめ,輪郭抽出やノイズ除去など,さまざまな画像処理に関する関数からなります.
Xcode を起動し,OF_PATH > examples > addons > opencvExample > opencvExample.xcodeproj を開いてください.下図のように,すでに addons・ ofxOpenCv などがプロジェクトに組み込まれています.またインクルードファイル(そしてライブラリファイル)の参照先もすでに登録されています.
デバッグ開始(緑矢印のボタン)を押せば,コンパイルに多少時間がかかるかもしれませんが,やがて,以下のようなアプリケーションが動作するはずです.
このプログラムは,手の動きを影絵のようにカメラで撮影した動画ファイルを読み込み,それを再生(左上)しながら,グレースケール画像に変換(右上)し,あらかじめ記録しておいた背景画像(左中)との差分を2値化(右中)し,さらに右下に,その輪郭を緑色で,またその全体を包み込む長方形を赤色で表示しています.
発展課題: ofApp.h の 7 行目の行頭にある // を外し,この行(#define _USE_LIVE_VIDEO)を有効にして,再度プログラムを実行してください.カメラが必要です.この行によって _USE_LIVE_VIDEO が「定義」されます.すると,ofApp.h や ofApp.cpp の中で,
#ifdef _USE_LIVE_VIDEO この部分がプログラムとして有効になる #else この部分は無視される #endif
のように,Xcode から見たプログラムを変えることができます.カメラの有る無しで,プログラムを自動的に変更しているわけです.(#で始まる行は,Cプログラムの一部ではなく,Cコンパイラに送り込むソースプログラムを改変するための,プリプロセッサ指令と呼ばれるものです.)
カメラを有効にしたプログラムを実行し,背景だけを撮影している状態でスペースを押してください.これで背景(左中)が登録されます.手や顔を撮影し,+ / − のキーで2値化のための閾値を調整してください.
OpenCV を代表する機能のひとつに,Haar 検出器を使った顔検出があげられます.まずは,サンプルプログラムを動かしてみましょう.今回は,dojoApps にコピーして動かします.OF_PATH > examples > addons > opencvHaarFinderExample を dojoApp にコピーし,Xcode から開いて,実行してみてください.
ほぼ9割以上の確率で,人間の顔(正立した正面顔)を検出できています.誤り検出も比較的少ない感じです.Haar 検出器は,顔の部分的な明暗パターンをチェックし,それを顔の各部分について逐次実行していきます.ハズレが出たら顔でないとして終了,最後までチェックにパスしたら顔です.このような処理を,画像の各位置について,さまざまな大きさの顔を想定して,実行しています.
つぎに,このプログラムをカメラ入力に対応するように改造してみましょう.ofApp.h と ofApp.cpp をつぎのように変更します.
#pragma once
#include "ofMain.h"
#include "ofxCvHaarFinder.h"
class ofApp : public ofBaseApp {
private:
ofVideoGrabber camera;
ofImage image;
ofxCvHaarFinder finder;
public:
(... 変更なし ...)
};
#include "ofApp.h" void ofApp::setup() { ofSetWindowShape(320, 240); camera.setDeviceID(0); camera.initGrabber(320, 240); finder.setup("haarcascade_frontalface_default.xml"); } void ofApp::update() { camera.update(); // camera -> image image.setFromPixels(camera.getPixels().getData(), 320, 240, OF_IMAGE_COLOR); // face detection finder.findHaarObjects(image, 40, 40); } void ofApp::draw() { // draw image ofSetColor(255, 255, 255); image.draw(0, 0); // draw markers ofSetColor(255, 0, 0); ofSetLineWidth(3); ofNoFill(); for(int i = 0; i < finder.blobs.size(); i++) { ofRectangle cur = finder.blobs[i].boundingRect; ofDrawRectangle(cur.x, cur.y, cur.width, cur.height); } } (... 以下変更なし ...)
finder は Haar 検出器(ofxCvHaarFinder クラスのインスタンス)です.ofApp::setup() の中で,顔の特徴を記述した xml ファイルを読み込んでいます.このファイルを入れ替えれば,任意の物体(たとえば歩行者や車)などを検出することも可能です.
320x240 という小さめの画像ですが,顔検出は計算量が大きいため,せいぜい毎秒あたり数フレームの処理速度になっていると思います.ちなみに,finder.findHaarObjects(image, 40, 40); の 40 は,検出すべき顔の最小の幅と高さです.40x40 よりも小さな顔は検出対象になりません.デフォルトは 0x0 なので,この変更により計算量はかなり減らしています.また,finder.blobs.size() によって検出された顔の数を,また finder.blobs[i].boundingRect は i 番目の顔の矩形領域(ofRectangle)を返します.ofRectangle は,メンバ変数として float x, y, width, height をもつクラスです.
発展課題1: 毎秒あたりに処理されるフレーム数(fps: frames per second)を計算し,画面に表示するように改造します.ofGetElapsedTimeMillis() は,プログラムが起動してからの経過時間をミリ秒(ms)を単位とする整数値で返す関数です.
#pragma once
#include "ofMain.h"
#include "ofxCvHaarFinder.h"
class ofApp : public ofBaseApp {
private:
ofVideoGrabber camera;
ofImage image;
ofxCvHaarFinder finder;
int msec;
public:
(... 変更なし ...)
};
#include "ofApp.h" void ofApp::setup() { (... 変更なし ...) finder.setup("haarcascade_frontalface_default.xml"); msec = ofGetElapsedTimeMillis(); } void ofApp::update() { (... 変更なし ...) } void ofApp::draw() { // draw image (... 変更なし ...) // compute fps int msecNow = ofGetElapsedTimeMillis(); float fps = 1000.0 / (msecNow - msec); msec = msecNow; // draw fps and markers ofSetColor(255, 0, 0); char buf[100]; sprintf(buf, "%5.2f fps", fps); ofDrawBitmapString(buf, 20, 20); ofSetLineWidth(3); ofNoFill(); for(int i = 0; i < finder.blobs.size(); i++) { ofRectangle cur = finder.blobs[i].boundingRect; ofDrawRectangle(cur.x, cur.y, cur.width, cur.height); } } (... 以下変更なし ...)
発展課題2: このままではあまり芸がないので,顔を検出したら,その目の位置あたりを塗りつぶすように改造します.以下のように ofApp::draw() を改造してみてください.
void ofApp::draw () { // draw image (... 変更なし ...) // compute fps (... 変更なし ...) // draw fps and markers (... 変更なし ...) //ofNoFill(); for(int i = 0; i < finder.blobs.size(); i++) { ofRectangle cur = finder.blobs[i].boundingRect; ofDrawRectangle(cur.x, cur.y + cur.height * 0.3, cur.width, cur.height * 0.2); } }
ofxOpenCv を使ったアプリケーションを新しく開発するには,OF_PATH > projectGenerator_osx > projectGenerator.app を使うと便利です.これを起動し,プロジェクトの名前を決め,必要なアドオンにチェックを入れ,最後に "GENERATE PROJECT" のボタンを押します.すると OF_PATH > apps > myApps の中に新しいプロジェクトのフォルダが用意され,その中に必要なファイルが生成されます.
たとえば,カメラ画像を取り込み,そのネガ(ピクセルごとに RGB の各値を 255 から減じたもの)を表示することを考えます.下図の左が元のカメラ画像,右が反転した画像です.
プログラム(ofApp.h, ofApp.cpp)は,つぎのようになります.ofApp.h でインクルードするファイルは "ofxOpenCv.h" とするのがよいでしょう.ofxOpenCv における画像クラスが,ofImage ではなく,ofxCvColorImage や ofxCvGrayscaleImage となることに注意してください.ofVideoGrabber や ofImage との画像のやりとりは,setFromPixels メソッドを使うとよいでしょう.
#pragma once #include "ofMain.h" #include "ofxOpenCv.h" class ofApp : public ofBaseApp { private: ofVideoGrabber camera; ofxCvColorImage image; public: (... 変更なし ...) };
#include "ofApp.h" void ofApp::setup() { // window ofSetWindowShape(1280, 480); // camera camera.initGrabber(640, 480); // allocate images image.allocate(640, 480); } void ofApp::update() { // camera camera.update(); image.setFromPixels(camera.getPixels().getData(), 640, 480); // inversion image.invert(); } void ofApp::draw() { camera.draw(0, 0, 640, 480); image.draw(640, 0, 640, 480); } (... 以下変更なし ...)
もうひとつの例として,ofxOpenCv の内部にある OpenCV の機能を利用するプログラムを紹介します.カメラ画像をグレースケール画像に変換し,その各ピクセルを4階調に変換します.さらに Canny フィルタによってエッジを検出し,そのエッジを黒線で上書きすることで,マンガ調の画像をつくりだすというものです.
そのプログラムは,下のようになります.cvCanny() は OpenCV の関数です.画像オブジェクトのメソッドの形式ではなく,画像データ等を引数とした関数となっている点に注意してください.ofxOpenCv の画像オブジェクト(grayImage など)を OpenCV の関数に渡すには,grayImage.getCvImage() とする必要があります.Canny() は,grayImage.getCvImage() を入力画像データとし,エッジ検出した結果(黒背景にエッジ部分が白線で描かれたもの)が edgeImage.getCvImage() に出力されます.edgeImage.dialate() はそのエッジを太くするためのものです.また,for 文の中では,4階調への変換とエッジの描画(黒線として描画)を行っています.ピクセルデータへのアクセス方法は,ofImage の場合と同じです.
#pragma once
#include "ofMain.h"
#include "ofxOpenCv.h"
class ofApp : public ofBaseApp {
private:
ofVideoGrabber camera;
ofxCvColorImage colorImage;
ofxCvGrayscaleImage grayImage, edgeImage;
public:
(... 変更なし ...)
};
#include "ofApp.h" void ofApp::setup() { // window ofSetWindowShape(1280, 480); // camera camera.initGrabber(640, 480); // allocate images grayImage.allocate(640, 480); edgeImage.allocate(640, 480); } void ofApp::update() { // camera camera.update(); colorImage.setFromPixels(camera.getPixels().getData(), 640, 480); grayImage = colorImage; grayImage.blur(); // canny filter cvCanny(grayImage.getCvImage(), edgeImage.getCvImage(), 20, 100); edgeImage.flagImageChanged(); edgeImage.dilate(); // posterization unsigned char *grayData = grayImage.getPixels().getData();; unsigned char *edgeData = edgeImage.getPixels().getData();; for (int i = 0; i < 640*480; i++) { if (edgeData[i] == 0) grayData[i] = (grayData[i] / 64) * 64 + 63; else grayData[i] = 0; } grayImage.flagImageChanged(); } void ofApp::draw() { // draw camera image camera.draw(0, 0, 640, 480); // draw manga image grayImage.draw(640, 0, 640, 480); } (... 以下変更なし ...)
ofImage のピクセルデータを書き換えた場合,最後に update() を呼び出す必要がありましたが,同様に,ofxOpenCv の画像オブジェクトを OpenCV 関数によって変更したり,あるいはピクセルデータを直接書き換えた場合は,最後に flagImageChanged() メソッドを呼び出す必要があります.
ofxOpenCv で提供されているのは OpenCV の一部ですが,顔検出や輪郭検出などを簡単に利用できます.一方,OpenCV が提供する多彩な画像演算機能(フィルタなど cv で始まる関数群)も,上述した方法によって利用することができます.これらを組み合わせて,魅力的なアプリケーションを開発してください.日本語で書かれた OpenCV に関するウェブサイトとして,OpenCV.jp があります.マニュアルやサンプルプログラムなど豊富ですので,参考にしてください.
Kinect はピクセルごとの深度(対象物までの距離)を計測できる特殊なカメラです.深度画像のほかに,通常のカラー画像を取得するカメラもあり,その両方を同時に取得することができます.実売1万円強の安価なデバイスですが,インタラクティブなシステムをつくる上でさまざまな活用が可能です.ここでは,ofxOpenNI というアドオンを使って,Kinect から深度画像・カラー画像を取得する方法や,深度画像からスケルトン情報(人物の各関節の位置情報)を取得する方法などを解説します.
Mac OS から ofxOpenNI を利用するには,以下の作業を(やはり安定したインターネット接続環境のもとで)行なってください.
- gameoverhack/ofxOpenNI をダウンロードし解凍する.
- 解凍したらフォルダ名を ofxOpenNI として OF_PATH > addons の中に入れる.
ちょっと面倒ですが,つぎにあげる手順にしたがって作業を進めれば,ofxOpenNI を使ったアプリケーションを製作できます.
- projectGenerator(OF_PATH > projectGenerator_osx > projectGenerator.app を開き,ofxOpenNI に組み込んだ新しいプロジェクト kinectExample1 を生成します.
- このプロジェクトフォルダ(OF_PATH > apps > myApps > kinectExample1)の中の,bin/data の中に,OF_PATH > addons > ofxOpenNI > examples > opeNI-SimpleExample > bin/data の中にある openni をコピーします.
- また,OF_PATH > addons > ofxOpenNI > mac > copy_to_data_openni_path > lib フォルダを,OF_PATH > apps > myApps > kinectExample1 > bin/data/openni の中にコピーします.この bin/dta/openni の中には,lib と config の2つのフォルダが入っているはずです.
- つぎに,OF_PATH > apps > myApps > kinectExample1 > kinectExample1.xcodeproj を開き,左端カラムにある「Project Navigator」にある addons の三角を解きます.addons の中に ofxOpenNI フォルダがあり,さらにその中に(閉じていれば開くと)src フォルダが入っています.この ofxOpenNI フォルダを右クリックし,2つの外部フォルダを登録("Add Files to kinectExample1")します.このとき,Options ボタンを押して,"Create groups" がオンになっていることを確認してください.加えるフォルダは,ひとつは OF_PATH > addons > ofxOpenNI > include フォルダです.もうひとつは OF_PATH > apps > myApps > kinectExample1 > bin/data/openni の中にある lib フォルダです.
- さらに,左端カラムにある「Project Navigator」の上段にある kinectExample1 をクリックし,"Build Settings" を開きます."Search Path" の欄まで降りていき,"Library Search Path" の三角を解きます.内部に "Debug" と "Release" があるので,それぞれをダブルクリックし,ポップアップしたリストの最下段までスクロールして,$(BUILT_PRODUCTS_DIR)/data/openni/lib の1行を書き加えます.すでに加えられていた場合は,スラッシュが抜けていないか確認してください.
-
最後に,ofxOpenNI に含まれる古い関数名 ofGetGLTypeFromPixelFormat を新しいもの ofGetGLFormatFromPixelFormat に修正します.修正箇所は,以下のとおりです.
- OF_PATH > addons > ofxOpenNI > src > ofxOpenNI.cpp の 1189行目・1191行目・1219行目・1221行目
- OF_PATH > addons > ofxOpenNI > src > ofxOpenNITypes.cpp の 274行目・680行目
これで準備完了です.あとは,以下にあげる ofApp.h と ofApp.cpp 等の内容を書いていけば,オリジナルの ofxOpenNI アプリケーションを作成することができます.つぎにあげる
ofxOpenNI の最もシンプルな利用は,距離画像の取得です.Kinect は,ミリメートルを単位として,画像(640x480)の各ピクセル位置について,物体までの距離をセンシングすることができます.その結果を表示するプログラム(ofApp.h, ofApp.cpp)は,つぎのようになります.
#pragma once #include "ofMain.h" #include "ofxOpenNI.h" class ofApp : public ofBaseApp { private: ofxOpenNI kinect; public: (... 変更なし ...) };
#include "ofApp.h" void ofApp::setup() { // window ofSetBackgroundColor(0, 0, 0); ofSetWindowShape(640, 480); ofSetFrameRate(30); // setup ofxOpenNI kinect.setup(); kinect.setRegister(true); kinect.setMirror(true); // flip horizontally kinect.addDepthGenerator(); // required for depth image // start kinect kinect.start(); } void ofApp::update() { // you need to call update() kinect.update(); } void ofApp::draw() { // draw depth image kinect.drawDepth(0, 0, 640, 480); // depth image (in color) } (... 以下変更なし ...)
まず,ofApp::setup() の中で kinect.setup() と kinect.setRegistered(true) を呼び出します.kinect.setMirror() での設定(鏡像=左右反転)はお好み次第です.kinect.addDepthGenerator() によって,距離情報の取得が kinect に組み込まれます.以上の設定の後,kinect.start() を呼び出してください.これで Kinect が動作を始めます.その後は,kinect.update() を呼び出すたびに,距離画像が取得され,kinect.draw() によって疑似カラーをつけて距離画像が描画されます.
Kinect による距離画像を活用する一例として,マウスクリックした位置について物体までの距離を表示することを考えます.文字表示には ofTrueTypeFont を使います.あらかじめ,Macintosh HD > Library > Fonts > Microsoft > Arial.ttf を bin/data にコピーしておいてください.プログラム(ofApp.h, ofApp.cpp)の変更点は,つぎのとおりです.
#pragma once
#include "ofMain.h"
#include "ofxOpenNI.h"
class ofApp : public ofBaseApp {
private:
ofxOpenNI kinect;
ofTrueTypeFont font;
char buffer[100];
public:
(... 変更なし ...)
};
#include "ofApp.h" void ofApp::setup() { // window ofSetBackgroundColor(0, 0, 0); ofSetWindowShape(640, 480); ofSetFrameRate(30); // setup ofxOpenNI kinect.setup(); kinect.setRegister(true); kinect.setMirror(true); kinect.addDepthGenerator(); // start kinect kinect.start(); // font and buffer font.loadFont("Arial.ttf", 20); sprintf(buffer, ""); } void ofApp::update() { kinect.update(); } void ofApp::draw() { // draw depth image ofSetColor(255, 255, 255); kinect.drawDepth(0, 0, 640, 480); // draw depth data ofSetColor(255, 127, 127); font.drawString(buffer, 20, 40); } (... 途中省略 ...) void ofApp::mousePressed(int x, int y, int button){ unsigned short *depthData = kinect.getDepthRawPixels().getData(); unsigned short depthMM = depthData[y * 640 + x]; sprintf(buffer, "%d mm", depthMM); } (... 以下変更なし ...)
マウスクリックされた位置 (x, y) について,Kinect から得られる距離データ(unsigned short)を取得しています.mm を単位とする非負整数で,10000mm まで計測できるはずです.この数値を文字配列 buffer に書き入れ,ofApp::draw() で画面表示するようにしています.画面表示が見づらい場合は,以下のように ofApp::draw() を書き換えてみてください.文字の周りに黒い縁取りを与えています.
void ofApp::draw () {
// draw depth image
ofSetColor(255, 255, 255);
kinect.drawImage(0, 0, 640, 480);
// draw depth data
ofSetColor(0, 0, 0);
for (int y = -2; y <= 2; y++)
for (int x = -2; x <= 2; x++)
font.drawString(buffer, 20 + x, 40 + y);
ofSetColor(255, 127, 127);
font.drawString(buffer, 20, 40);
}
つぎに,距離による人物の切り抜き手法を紹介します.これは,たとえば Kinect から 1m 以上,2m 未満の距離にある人物(あるいは物体)の映像だけを切り出し,あらかじめ用意しておいた背景に描き入れるというものです.まず最初に,距離画像と RGB 画像を並べて表示するプログラムをつくります.
#pragma once #include "ofMain.h" #include "ofxOpenNI.h" class ofApp : public ofBaseApp { private: ofxOpenNI kinect; public: (... 変更なし ...) };
#include "ofApp.h" void ofApp::setup() { // window ofSetBackgroundColor(0, 0, 0); ofSetWindowShape(1280, 480); ofSetFrameRate(30); // setup ofxOpenNI kinect.setup(); kinect.setRegister(true); kinect.setMirror(true); kinect.addDepthGenerator(); // required for depth image kinect.addImageGenerator(); // required for RGB image // start kinect kinect.start(); } void ofApp::update() { kinect.update(); } void ofApp::draw() { kinect.drawDepth(0, 0, 640, 480); kinect.drawImage(640, 0, 640, 480); } (... 以下変更なし ...)
実行時の距離画像と RGB 画像をよく見比べると,たとえば天井にある黒枠について,両者の位置がわずかにズレているのがわかります.これを補正するには,ofApp::setup() をつぎのように赤字部分を追加してください.オマジナイのように複雑ですが,RGB 画像の「視点」を距離画像にコピーしています.この結果,距離画像の一部が無効(距離 0)になってしまいますが,RGB 画像とのズレはほとんど 0 になるはずです.
void ofApp::setup() {
// window
ofSetBackgroundColor(0, 0, 0);
ofSetWindowShape(1280, 480);
ofSetFrameRate(30);
// setup ofxOpenNI
kinect.setup();
kinect.setRegister(true);
kinect.setMirror(true);
kinect.addDepthGenerator(); // required for depth image
kinect.addImageGenerator(); // required for RGB image
// align depth image to RGB image
kinect.getDepthGenerator().GetAlternativeViewPointCap().SetViewPoint(kinect.getImageGenerator());
// start kinect
kinect.start();
}
つぎに kirinuki という RGBA の ofImage オブジェクトをつくり,そこに Kinect からの RGB 画像をコピーするようにします.ただし kirinuki のピクセル型は RGBA,つまりアルファチャネルつきで,1 ピクセル 4 バイトです.実行結果は,上のプログラムと全く変わらないはずです.
#pragma once
#include "ofMain.h"
#include "ofxOpenNI.h"
class ofApp : public ofBaseApp {
private:
ofxOpenNI kinect;
ofImage kirinuki;
public:
(... 変更なし ...)
};
#include "ofApp.h" void ofApp::setup() { (... 変更なし ...) // start kinect kinect.start(); // kirinuki image (RGBA) kirinuki.allocate(640, 480, OF_IMAGE_COLOR_ALPHA); } void ofApp::update() { kinect.update(); // kirinuki (1000-2000mm) unsigned char *kirinukiData = kirinuki.getPixels().getData(); unsigned char *imageData = kinect.getImagePixels().getData(); for (int k = 0; k < 640*480; k++) { kirinukiData[k * 4 + 0] = imageData[k * 3 + 0]; kirinukiData[k * 4 + 1] = imageData[k * 3 + 1]; kirinukiData[k * 4 + 2] = imageData[k * 3 + 2]; kirinukiData[k * 4 + 3] = 255; } kirinuki.update(); } void ofApp::draw() { kinect.drawDepth(0, 0, 640, 480); kirinuki.draw(640, 0, 640, 480); } (... 以下変更なし ...)
各ピクセルのアルファチャネルには 255(不透明)をセットしていますが,これを 0 にすればそのピクセルは透明になります.この選択を,そのピクセルの距離に応じて決めてやればよいでしょう.つぎのように ofApp::update() を変更してください.距離が 1000mm〜2000mm となるピクセルだけを表示するようにしています.
void ofApp::update() { kinect.update(); // kirinuki (1000-2000mm) unsigned char *kirinukiData = kirinuki.getPixels().getData(); unsigned char *imageData = kinect.getImagePixels().getData(); unsigned short *depthData = kinect.getDepthRawPixels().getData(); for (int k = 0; k < 640*480; k++) { kirinukiData[k * 4 + 0] = imageData[k * 3 + 0]; kirinukiData[k * 4 + 1] = imageData[k * 3 + 1]; kirinukiData[k * 4 + 2] = imageData[k * 3 + 2]; // set visible or invisible if (1000 <= depthData[k] && depthData[k] < 2000) kirinukiData[k * 4 + 3] = 255; // visible else kirinukiData[k * 4 + 3] = 0; // invisible } kirinuki.update(); }
最後に,せっかく透明化したので,適当な背景を入れてみましょう.適当な写真ファイル(例 photo.png)を用意して,bin/data に入れておいてください.プログラムはつぎのように変更します.
#pragma once
#include "ofMain.h"
#include "ofxOpenNI.h"
class ofApp : public ofBaseApp {
private:
ofxOpenNI kinect;
ofImage kirinuki, scenery;
public:
(... 変更なし ...)
};
#include "ofApp.h" void ofApp::setup() { (... 変更なし ...) // kirinuki image (RGBA) kirinuki.allocate(640, 480, OF_IMAGE_COLOR_ALPHA); // background scenery scenery.loadImage("photo.png"); } void ofApp::update() { (... 変更なし ...) } void ofApp::draw() { kinect.drawDepth(0, 0, 640, 480); scenery.draw(640, 0, 640, 480); kirinuki.draw(640, 0, 640, 480); } (... 以下変更なし ...)
背景画像 scenery を描画してから,同じ位置に,距離に応じて切り出した RGB 画像を上書きしています.透明部分(α = 0 の部分)は,ウラ側の背景が透過して見えるはずです.
Kinect の魅力のひとつは,人のスケルトン(各関節の3次元位置)を取得できることでしょう.ここでは,Kinect から得られたスケルトン情報を,ユーザ独自の方法で利用する方法を紹介します.つぎのプログラムは,RGB 画像の上にスケルトンを上書きするものです.
#pragma once #include "ofMain.h" #include "ofxOpenNI.h" class ofApp : public ofBaseApp { private: ofxOpenNI kinect; public: (... 変更なし ...) };
#include "ofApp.h" void ofApp::setup() { // window ofSetBackgroundColor(0, 0, 0); ofSetWindowShape(640, 480); ofSetFrameRate(30); // setup ofxOpenNI kinect.setup(); kinect.setRegister(true); kinect.setMirror(true); kinect.addImageGenerator(); // required for RGB image kinect.addDepthGenerator(); // required for depth image kinect.addUserGenerator(); // required for skeleton tracking kinect.setMaxNumUsers(1); // max num of skeleton to track // start kinect kinect.start(); } void ofApp::update() { kinect.update(); } void ofApp::draw() { // draw RGB image (weak) ofSetColor(100, 100, 100); kinect.drawImage(0, 0, 640, 480); // draw skeleton (strong) ofSetColor(255, 255, 255); kinect.drawSkeletons(0, 0, 640, 480); } (... 以下変更なし ...)
なお,複数ユーザを同時にトラッキングするには,setMaxNumUsers(n) に上限となる人数を与えてください.ユーザごとのスケルトンは,ひとり目は drawSkeleton(x, y, w, h, 0),ふたり目は drawSkeleton(x, y, w, h, 1) のように,描き分けることができます.
スケルトンを自分独自の方法で描画するには,以下のプログラム(ofApp::draw())を参考にしてください.ここでは無背景とし,関節に赤いボールを,関節間に黄色い線分を表示するようにしています.
void ofApp::draw() {
// normal color
ofSetColor(255, 255, 255);
// draw depth/RGB/skeletons images
kinect.drawDepth(640, 0, 320, 240);
// draw user
if (kinect.getNumTrackedUsers() > 0) {
// skeleton data
ofxOpenNIUser user = kinect.getTrackedUser(0);
// draw limbs
ofSetLineWidth(5);
ofSetColor(255, 255, 127);
for (int i = 0; i < user.getNumLimbs(); i++) {
ofxOpenNILimb limb = user.getLimb((enum Limb) i);
if (limb.isFound()) {
float x1 = limb.getStartJoint().getProjectivePosition().x;
float y1 = limb.getStartJoint().getProjectivePosition().y;
float x2 = limb.getEndJoint().getProjectivePosition().x;
float y2 = limb.getEndJoint().getProjectivePosition().y;
ofLine(x1, y1, x2, y2);
}
}
// draw joints
ofSetColor(255, 127, 127);
for (int i = 0; i < user.getNumJoints(); i++) {
ofxOpenNIJoint joint = user.getJoint((enum Joint) i);
if (joint.isFound()) {
float x = joint.getProjectivePosition().x;
float y = joint.getProjectivePosition().y;
ofDrawCircle(x, y, 20);
}
}
}
}
関節の番号や名前,骨(関節間の線分)の番号や名前は,下表のようになっています.(旧版の ofxOpenNI から変更されているようです.注意してください)
ID | 関節(joint)の名前 | 意味(どこか) | |
---|---|---|---|
0 | JOINT_TORSO | へそ(体の中心) | |
1 | JOINT_NECK | 首の根元 | |
2 | JOINT_HEAD | 頭の中心 | |
3 | JOINT_LEFT_SHOULDER | 左肩 | |
4 | JOINT_LEFT_ELBOW | 左肘 | |
5 | JOINT_LEFT_HAND | 左手首 | |
6 | JOINT_RIGHT_SHOULDER | 右肩 | |
7 | JOINT_RIGHT_ELBOW | 右肘 | |
8 | JOINT_RIGHT_HAND | 右手首 | |
9 | JOINT_LEFT_HIP | 左足(もも)の付け根 | |
10 | JOINT_LEFT_KNEE | 左膝 | |
11 | JOINT_LEFT_FOOT | 左足首 | |
12 | JOINT_RIGHT_HIP | 右足(もも)の付け根 | |
13 | JOINT_RIGHT_KNEE | 右膝 | |
14 | JOINT_RIGHT_FOOT | 右足首 |
ID | 骨(limb)の名前 | 意味(どこか) | |
---|---|---|---|
0 | LIMB_LEFT_UPPER_TORSO | 左上ワキ(TORSO-SHOULDER) | |
1 | LIMB_LEFT_SHOILDER | 左鎖骨(NECK-SHOULDER) | |
2 | LIMB_LEFT_UPPER_ARM | 左上腕(SHOULDER-ELBOW) | |
3 | LIMB_LEFT_LOWER_ARM | 左前腕(ELBOW-HAND) | |
4 | LIMB_LEFT_LOWER_TORSO | 左下ワキ(TORSO-HIP) | |
5 | LIMB_LEFT_UPPER_LEG | 左上腿(HIP-KNEE) | |
6 | LIMB_LEFT_LOWER_LEG | 左下腿(KNEE-FOOT) | |
7 | LIMB_RIGHT_UPPER_TORSO | 右上ワキ(TORSO-SHOULDER) | |
8 | LIMB_RIGHT_SHOULDER | 右鎖骨(NECK-SHOULDER) | |
9 | LIMB_RIGHT_UPPER_ARM | 右上腕(SHOULDER-ELBOW) | |
10 | LIMB_RIGHT_LOWER_ARM | 右前腕(ELBOW-HAND) | |
11 | LIMB_RIGHT_LOWER_TORSO | 右下ワキ(TORSO-HIP) | |
12 | LIMB_RIGHT_UPPER_LEG | 右上腿(HIP-KNEE) | |
13 | LIMB_RIGHT_LOWER_LEG | 右下腿(KNEE-FOOT) | |
14 | LIMB_NECK | 首(NECK-HEAD) | |
15 | LIMB_PELVIS | マタ(HIP-HIP) |
全身のスケルトンではなく,手の位置だけをセンシングしたいのであれば,もっと手軽(というかロバスト)に実現できるようです.つぎのプログラムを参考にしてください.手の位置に赤いボールが表示されるはずです.
#pragma once #include "ofMain.h" #include "ofxOpenNI.h" class ofApp : public ofBaseApp { private: ofxOpenNI kinect; public: (... 変更なし ...) };
#include "ofApp.h" void ofApp::setup() { // window ofSetBackgroundColor(0, 0, 0); ofSetWindowShape(640, 480); ofSetFrameRate(30); // setup ofxOpenNI kinect.setup(); kinect.setRegister(true); kinect.setMirror(true); kinect.addImageGenerator(); // required for RGB image kinect.addDepthGenerator(); // required for depth image kinect.addHandsGenerator(); // required for hand tracking kinect.addAllHandFocusGestures(); kinect.setMaxNumHands(1); // max num of skeleton to track // start kinect kinect.start(); } void ofApp::update() { kinect.update(); } void ofApp::draw() { ofSetColor(255, 127, 127); if (kinect.getNumTrackedHands() > 0) { ofxOpenNIHand hand = kinect.getTrackedHand(0); ofPoint p = hand.position(); ofDrawCircle(p.x, p.y, 20); } } (... 以下変更なし ...)