小嶋秀樹 | 授業情報 | 研究室
日本語 | English

注意:このページの内容は古くなっています.Mac ユーザは,新しいMac 向けのページがありますので,そちらを参照してください.

openFrameworks 応用編(3)

ここでは,より高度なアドオンの扱いかたを,ofxOpenCV と ofxOpenNI を題材として学んでいきます.ofxOpenCV は openFrameworks に標準添付された画像処理用のアドオンです.ofxOpenNI は(前回に取り上げた ofxTrueTypeFontUC と同じように)外部から入手(ダウンロード)した Kinect 用のアドオンです.これらアドオンの利用は(とくに Windows 上の VC++ からは)手順が複雑ですが,それぞれのアドオンに添付された例(example)をコピーし,改造することで,大抵のアプリケーションを作成することができます.

ofxOpenCV の扱いかた

ofxOpenCV は openFrameworks から OpenCV を利用できるようにするアドオンです.OpenCV は広く普及している画像処理ライブラリです.顔検出をはじめ,輪郭抽出やノイズ除去など,さまざまな画像処理に関する関数からなります.

【サンプルプログラムを動かす】

VC++ を起動し,OF_PATH > examples > addons > opencvExample > opencvExample.vcxproj を開いてください.下図のように,すでに addons・ ofxOpenCV などがプロジェクトに組み込まれています.またインクルードファイル(そしてライブラリファイル)の参照先もすでに登録されています.

デバッグ開始(緑矢印のボタン)を押せば,コンパイルに多少時間がかかるかもしれませんが,やがて,以下のようなアプリケーションが動作するはずです.

このプログラムは,手の動きを影絵のようにカメラで撮影した動画ファイルを読み込み,それを再生(左上)しながら,グレースケール画像に変換(右上)し,あらかじめ記録しておいた背景画像(左中)との差分を2値化(右中)し,さらに右下に,その輪郭を緑色で,またその全体を包み込む長方形を赤色で表示しています.

発展課題: ofApp.h の 7 行目の行頭にある // を外し,この行(#define _USE_LIVE_VIDEO)を有効にして,再度プログラムを実行してください.カメラが必要です.この行によって _USE_LIVE_VIDEO が「定義」されます.すると,ofApp.h や ofApp.cpp の中で,

#ifdef _USE_LIVE_VIDEO
    この部分がプログラムとして有効になる
#else
    この部分は無視される
#endif

のように,VC++ から見たプログラムを変えることができます.カメラの有る無しで,プログラムを自動的に変更しているわけです.(#で始まる行は,Cプログラムの一部ではなく,Cコンパイラに送り込むソースプログラムを改変するための,プリプロセッサ指令と呼ばれるものです.)

カメラを有効にしたプログラムを実行し,背景だけを撮影している状態でスペースを押してください.これで背景(左中)が登録されます.手や顔を撮影し,+ / − のキーで2値化のための閾値を調整してください.

【顔検出に挑戦】

OpenCV を代表する機能のひとつに,Haar 検出器を使った顔検出があげられます.まずは,サンプルプログラムを動かしてみましょう.今回は,dojoApps にコピーして動かします.OF_PATH > examples > addons > opencvHaarFinderExample を dojoApp にコピーし,VC++ から開いて,実行してみてください.

ほぼ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:
    void setup();
    (... 変更なし ...)
    void gotMessage(ofMessage msg);
};
#include "ofApp.h"
void ofApp::setup () {
    ofSetWindowShape(320, 240);
    camera.setDeviceID(0);
    camera.initGrabber(320, 240);
    image.allocate(320, 240, OF_IMAGE_COLOR);
    finder.setup("haarcascade_frontalface_default.xml");
}
void ofApp::update () {
    camera.update();
    //  camera -> image
    unsigned char *cdata = camera.getPixels();
    unsigned char *idata = image.getPixels();
    for (int k = 0; k < 320*240*3; k++)
        idata[k] = cdata[k];
    image.update();
    //  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;
        ofRect(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)を単位とする整数値で返す関数です.

class ofApp : public ofBaseApp {
private:
    ofVideoGrabber camera;
    ofImage image;
    ofxCvHaarFinder finder;
    int msec;
public:
    (... 変更なし ...)
};
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;
        ofRect(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;
        ofRect(cur.x, cur.y + cur.height * 0.3, 
               cur.width, cur.height * 0.2);
    }
}
【ofxOpenCV を活用する】

ofxOpenCV を使ったアプリケーションの開発には,まず,上で取り上げた2つのサンプルのいずれか(プロジェクトフォルダ)を dojoApps などの下にコピーし,ofApp.h, ofApp.cpp を書き換えればよいでしょう.

ofxCvHaarFinder.h のように ofxCv で始まるファイルは,openFrameworks から OpenCV を簡単に使えるようにするための関数群です.これらファイルは,OF_PATH > addons > ofxOpenCV > src の中にあり,そのヘッダファイル(*.h)を見ることで,おおよその機能を知ることができます.また,OpenCV ライブラリの関数を直接使うことも,もちろん可能です.適宜,必要な機能に関するヘッダファイルを読み込んで,利用してください.

日本語で書かれた OpenCV に関するウェブサイトとして,OpenCV.jp があります.マニュアルやサンプルプログラムなど豊富ですので,参考にしてください.

ofxOpenNI による Kinect の利用
図

Kinect はピクセルごとの深度(対象物までの距離)を計測できる特殊なカメラです.深度画像のほかに,通常のカラー画像を取得するカメラもあり,その両方を同時に取得することができます.実売1万円強の安価なデバイスですが,インタラクティブなシステムをつくる上でさまざまな活用が可能です.ここでは,ofxOpenNI というアドオンを使って,Kinect から深度画像・カラー画像を取得する方法や,深度画像からスケルトン情報(人物の各関節の位置情報)を取得する方法などを解説します.

【準備編】

ofxOpenNI を利用するには準備が必要です.Windows 環境の人は,以下の作業を(安定したインターネット接続環境のもとで)行なってください.すべて 32 ビット版をダウンロード・インストールしてください.

  1. openNI のインストール
  2. nite のインストール
  3. SensorKinect のインストール(Bin の中から)
  4. ofxOpenNI ををダウンロード・解凍し.フォルダ名を ofxOpenNI として OF_PATH > addons の中に入れる.

Mac OS から ofxOpenNI を利用するには,以下の作業を(やはり安定したインターネット接続環境のもとで)行なってください.

  1. gameoverhack/ofxOpenNI をダウンロードし解凍する.
  2. 解凍したらフォルダ名を ofxOpenNI として OF_PATH > addons の中に入れる.
【サンプルを動かす】

ofxOpenNi に添付されたサンプルプログラムを実行してみましょう.まず,OF_PATH > addons >ofxOpenNI > examples > opeNI-SimpleExamples フォルダを,OF_PATH > apps > dojoApps の中にコピーします.また,src の中にある src-UserAndCloud-Simple を src に名前変更しておきます.

つぎに,OF_PATH > addons > ofxOpenNI > win あるいは mac > lib フォルダを,OF_PATH > apps > dojoApps > opeNI-SimpleExamples > bin > data > openni フォルダの中にコピーします.(このコピー作業は,今後も,新しく ofxOpenNI プロジェクトをつくるときに必要になります.)

Kinect を USB 接続し,OF_PATH > apps > dojoApps > opeNI-SimpleExamples の中のプロジェクトファイルを VC++ で開き,Debug 構成を指定して,ビルド・実行してみてください.下図のようにアプリケーションが動作するはずです.

【ofxOpenNI を活用する:その1】

このサンプルプログラムから ofApp.h と ofApp.cpp の内容を書き換えれば,オリジナルの ofxOpenNI プログラムを作成することができます.opeNI-SimpleExamples のコピーをつくり,その名前を kinectExample1 とします.その ofApp.h と ofApp.cpp をつぎのように書き換えてみてください.

kinectExample1
#pragma once
#include "ofMain.h"
#include "ofxOpenNI.h"
class ofApp : public ofBaseApp {
private:    
    ofxOpenNI kinect;
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 dragEvent (ofDragInfo dragInfo);
    void gotMessage (ofMessage msg);
};
#include "ofApp.h"
void ofApp::setup () {
    //  window
    ofBackground(0, 0, 0);
    ofSetWindowShape(960, 480);
    ofSetFrameRate(30);
    //  setup ofxOpenNI
    kinect.setup();
    kinect.setRegister(true);
    kinect.setMirror(true);
    kinect.addImageGenerator();
    kinect.addDepthGenerator();
    kinect.addUserGenerator();
    kinect.setMaxNumUsers(1);
    //  start kinect
    kinect.start();
}
void ofApp::update () {
    kinect.update();
}
void ofApp::draw () {
    //  normal color
    ofSetColor(255, 255, 255);
    //  draw color/depth images
    kinect.drawDepth(0, 0, 640, 480);
    kinect.drawImage(640, 0, 320, 240);
    //  draw user
    kinect.drawSkeletons(640, 240, 320, 240);
}
void ofApp::keyPressed (int key) {}
void ofApp::keyReleased (int key) {}
void ofApp::mouseMoved (int x, int y) {}
void ofApp::mouseDragged (int x, int y, int button){}
void ofApp::mousePressed (int x, int y, int button) {}
void ofApp::mouseReleased (int x, int y, int button){}
void ofApp::windowResized (int w, int h){}
void ofApp::dragEvent (ofDragInfo dragInfo) {}
void ofApp::gotMessage (ofMessage msg) {}

実行するとわかるように,RGBカラー画像,深度画像,スケルトン画像が表示されています.

【ofxOpenNI を活用する:その2】

Kinect から得られたスケルトン情報を,ユーザ独自の方法で描画する機能を実装します.つぎのプログラムを参考にしてください.

void ofApp::draw(){
    //  normal color
    ofSetColor(255, 255, 255);
    //  draw depth/RGB/skeletons images
    kinect.drawDepth(0, 0, 640, 480);
    kinect.drawImage(640, 0, 320, 240);
    kinect.drawSkeletons(640, 240, 320, 240);
    //  draw user
    if (kinect.getNumTrackedUsers() == 1) {
        ofxOpenNIUser user = kinect.getTrackedUser(0);
        //  draw limbs
        ofSetLineWidth(3);
        ofSetColor(255, 100, 100);
        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, 255, 100);
        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;
                ofCircle(x, y, 5);
            }
        }
    }
}