2012-01-30

エンジニアサポート新年会 CROSS 2012でEmotivについてLTをしました

日本ではあまり使っている人を見ない筋電/脳波ヘッドセットのEmotiv EPOC neuroheadsetを使ってみた。デモはNodejsでデータ飛ばしてCanvasに描画した。視線の位置のxyが取れるのでそれを使った方が面白かったかなぁ。



JavaのコードはSDKのサンプルをベースにJSON作っただけ。状態を取る部分のコードは次の通り。
JSONObject currentState = new JSONObject();
Edk.INSTANCE.EE_EmoEngineEventGetEmoState(eEvent, eState);
float timestamp = EmoState.INSTANCE.ES_GetTimeFromStart(eState);

JSONArray actions = new JSONArray();
if (EmoState.INSTANCE.ES_ExpressivIsBlink(eState) == 1) {
    actions.put("Blink");
}
if (EmoState.INSTANCE.ES_ExpressivIsLeftWink(eState) == 1) {
    actions.put("Left Wink");
}
if (EmoState.INSTANCE.ES_ExpressivIsRightWink(eState) == 1) {
    actions.put("Right Wink");
}
if (EmoState.INSTANCE.ES_ExpressivIsLookingLeft(eState) == 1) {
    actions.put("Looking Left");
}
if (EmoState.INSTANCE.ES_ExpressivIsLookingRight(eState) == 1) {
    actions.put("Looking Right");
}
if (EmoState.INSTANCE.ES_ExpressivIsLookingDown(eState) == 1) {
    actions.put("Looking Down");
}
currentState.put("actions", actions);

currentState.put("Excitement ShortTerm", EmoState.INSTANCE.ES_AffectivGetExcitementShortTermScore(eState));
currentState.put("Excitement LongTerm", EmoState.INSTANCE.ES_AffectivGetExcitementLongTermScore(eState));
currentState.put("Engagement Boredom", EmoState.INSTANCE.ES_AffectivGetEngagementBoredomScore(eState));
currentState.put("Frustration", EmoState.INSTANCE.ES_AffectivGetFrustrationScore(eState));
currentState.put("Meditation", EmoState.INSTANCE.ES_AffectivGetMeditationScore(eState));

send("state", currentState.toString());





このエントリーをはてなブックマークに追加

2012-01-09

キネクハトッカーズマニュアルのサポートページを更新しました。

サポートページ作成時点からかなり状況が変ったので、いろいろと修正しました。OpenNIの進化や、WebSocketでバイナリが使える様になった事など。半年も経てば随分と変る物ですね。


f:id:hagino_3000:20120108233246p:image:w640



このエントリーをはてなブックマークに追加

2012-01-02

OpenNIをopenFrameworksから使う Advanced編

OpenNI Advent Calendarの20日目の記事です。

24日目の記事でもopenFrameworksについて書かれているので、内容がかぶらない様にしています。

何故24日目の記事の方が先に公開されているかは気にしないでください。oFについて基本的な事は上記 @mmlemon_twitter さんの記事に書かれているので割愛します。あと、oFはMacユーザーが多いのでMacOSX + Xcode4の環境を前提に書きます。

ofxOpenNIについて

oFのaddonの一つ。OpenNIをoFっぽいインタフェースで扱える様にした物。誰のブランチを使っていいのかわからないけど、gameoverhack氏のブランチが一番forkされているので自分が使う時はそれを使っている。
DepthGenerator, ImageGenerator, UserGenetator, HandGenerator, IRGenerator, GestureGeneratorの機能はカバーしていて、 exampleを見れば大体の使い方は掴めるはず。ofxOpenNIのジェネレータから得られるデータは、既に世界座標から表示座標系に変換されているため、そのまま表示に使う事ができる。

チルトモーターの首ふりも実装されているが、内部でlibusbを使っているためかMacOSでしか動作しない。Windowsでも首ふりしたかったらOpenNIのXnUSBを使う実装に書きかえれば良い。

リポジトリにある物はKinectで使う事が想定されているらしく、Xtion Proではそのまま使えない。SensorKinectの代りにSensorモジュールを自分でビルドして上書きすればいけそう。あとOpenNIのバージョンも古いので注意。今から使う場合は自分がメンテナになる勢いで使っていかないと厳しいだろう。

アドオンを使わない方法

さて、ofxOpenNIを紹介したが、現在自分の作業マシンにはKinectでは無くXtion Pro liveが接続されているのでofxOpenNIは動かない。なのでoFのプロジェクトにOpenNIを普通に追加して使う時に気にする事を書きます。

config.xmlがアプリケーションの内部に配置される様にする

Build Phasesで「Add Build Phase」してOpenNIの設定ファイルをビルドした.appファイルの中にコピーする。しておかないとHoge.app単体を配布して動作させた時にconfig.xmlが見つからない、とエラーになってしまう。

f:id:hagino_3000:20120102012615p:image:w360

OpenNIの初期化コードは次の通りになる。
xn::EnumerationErrors ctxErrors;
static string configFilePath = ofFilePath::getCurrentWorkingDirectory() + "../Resources/config.xml";
XnStatus rc = openNIContext.InitFromXmlFile(configFilePath.c_str(), &ctxErrors);
if (rc != XN_STATUS_OK) {
  XnChar strError[1024];
  ctxErrors.ToString(strError, 1024);
  throw std::runtime_error(strError);
}

updateメソッドでfpsを落さないようにする

updateメソッドで注意しないといけないのは、WaitAndUpdata系のメソッドを毎回使うと、ofSetFrameRateでフレームレートを指定していてもブロックがかかりfps30程度に落ちてしまう。fpsに影響を与えたく無い場合はジェネレーターのWaitAndUpdateDataを呼ぶ前にIsNewDataAvailableで新しいデータが来ているかチェックする。ofxOpenNIではupdateメソッド内でWaitAndUpdateDataを呼んでいるため、フレームレートが30fpsに落ちてしまう。

コードは次の通り
// 各種ジェネレータとOpenNIコンテキストはヘッダファイルでメンバとして定義してある。
void testApp::update(){
  if (depthGen.IsNewDataAvailable()) {
    // 深度データの更新
    depthGen.WaitAndUpdateData();
  
    xn::DepthMetaData depthMetaData;
    depthGen.GetMetaData(depthMetaData);
  
    XnMapOutputMode mapMode;
    depthGen.GetMapOutputMode(mapMode);
  
    int width = mapMode.nXRes;
    int height = mapMode.nYRes;
    int numPixels = width * height;
  
    float* depthHist = createHist(&depthMetaData, numPixels);
  
    const XnDepthPixel* depthPixels = depthMetaData.Data();
    for (XnUInt i = 0; i < numPixels; i++, ++depthPixels) {
      grayPixels[i] = depthHist[*depthPixels];
    }
  
    grayImage.setFromPixels(grayPixels, width, height); 
  }
  if (imageGen.IsNewDataAvailable()) {
    // ビデオカメラ画像の更新
    imageGen.WaitAndUpdateData();
  
    xn::ImageMetaData imageMetaData;
    imageGen.GetMetaData(imageMetaData);
  
    colorImage.setFromPixels((unsigned char*)imageMetaData.RGB24Data(), colorImage.width, colorImage.height);
  
  }
  if (audioGen.IsNewDataAvailable()) {
    // オーディオの更新
    audioGen.WaitAndUpdateData();
    ofLog(OF_LOG_NOTICE, "Audio data size:" + ofToString(audioGen.GetDataSize()));
  
    // doSomething
  }
}
void testApp::draw(){
    ofScale(0.8, 0.8);
    // 深度画像の描画
    grayImage.draw(0, 0);
    // ビデオカメラ画像の描画
    colorImage.draw(640, 0);
}
どうしてもupdateメソッド内がごちゃごちゃしてくるので、OpenNIの処理を分離するためのクラスを一つ作るのが良いだろう。Audioジェネレーターはこの記事のために初めて使ってみたが、安定して動作しなかったので特に何も処理は実装しなかった。*1

コード一式はgistにアップしてあります。

openFrameworks + Kinectな作品

Vimeoで検索するといろいろ見つかる。

f:id:hagino_3000:20120102011553p:image:w360

自分のおきにいりはWebcam Piano 2.0 Teaserという作品 (Kinectは使っていないけど)

f:id:hagino_3000:20120102011942p:image:w360

まとめ

android対応も進んでおり、openFrameworksも発展も楽しみなプラットフォームです。OpenNIと一緒に使えるようになっておくと良いかもしれません。openFrameworks + Kinectについての解説はキネクハッカーズマニュアルにもページを裂いて書いたので、興味のある方はチェックしてみてください。



書名
キネクトハッカーズマニュアル
著者
@hagino3000 + 小野 憲史
発売日
2011-08-26
価格
2,604 JPY (256ページ)
*1:Windowsでは動作実績があるみたいだが、自分の環境ではすぐにデータが取得できなくなり、Xtion pro liveを抜いて刺し直すというのが必要だった。



このエントリーをはてなブックマークに追加

2011-12-19

OpenNI/NITEのボーントラッキングが劇的に進化していた件

第3回Kinectハッカソン無事に開催できました。レポートっぽい物はShibuya.NIのサイトにアップするとして、OpenNI + Xtion Pro liveで今日試した事など。

ポーズ不要

NITEを1.5.0 unstableにすると、ユーザー検出からポーズ検出をすっ飛ばしてボーントラッキングが開始できる様になった、以前まで必要だった学習データも不要。適当に動かしてみたら、たまたまその場にいた4人をいきなりトラッキングして普通に動いていた。重すぎて描画が遅れるという事も無かった。動画撮ればよかった。

Kinectポーズという単語は最早死語、「OpenNIはポーズを取らないと骨格取れないんですよ」とドヤ顔をする事はもうできませんね。

上半身のみボーントラッキングが動作した

トラッキング開始時にSkeletonCapabilityに渡すXnSkeletonProfileについて。大抵はXN_SKEL_PROFILE_ALLを渡して頭から足までのジョイントを得ると思うが、自分は 机に座る→作業→退席 の動作を正面からトラッキングしたかったので上半身だけで十分。下半身は見えないだろうし。
そこで、上半身のみのジョイントをトラッキングするXN_SKEL_PROFILE_UPPERを使うとこうなった。

f:id:hagino_3000:20111219020843p:image:w360

頭と手だけもいける

頭の位置を取りたいだけならXN_SKEL_PROFILE_HEAD_HANDSで十分だった。ユーザー検出と同時に頭部の中心座標が取れるので、用途によっては顔認識よりも便利。横顔でも後ろ頭でもいける。始終静止状態の人は検出できないけど。つまり、全身を入れてポーズを取るのが不要になったおかげで、上半身のみや、下半身のみしか見えない状況においてジョイントのトラッキングが可能になった。

正面を向いてるかどうか

OpenCVの物体検出で正面顔の学習データを使ってみた。頭の位置は既にわかっているので、うまくやれば計算量は減らせるはず。cvHaarDetectObjectsに検索領域とか指定できればと思ったけど、そんなオプション無かった。

顔の向き

頭部の深度の平均値を使ってなんとなく傾きを得る方法を@molant3さんに教えてもらった。

Xtion Pro liveは家電量販店でも買えます

中の人曰くそういう事だった。輸入した時は190ドルだったけど、日本価格25,000円なり。 Kinectよりも高いけど、重くてでかいKinectを毎日持ち歩きたくはないので自分はXtion Pro liveを使っています。

Mac App StoreにKinectアプリが増えていた。

3D Scannerというアプリ。Kinectが接続されていない、とかいうエラーが出て動作しない!! 170JPYが無駄に……!!

f:id:hagino_3000:20111219023812p:image:w360



このエントリーをはてなブックマークに追加

2011-12-09

WebSocketでバイナリデータを送受信してみる



この記事はHTML5 Advent Calendarの8日目です。

いつの間にか手元の環境(Chrome17 dev + Node 0.6.3)においてWebSocketでバイナリデータが扱える様になっていたので何か作ってみようかと。

まず、NodeでWebSocketを使おうとしたらそれ用のライブラリを使う事になるのだが現時点でバイナリデータが扱えるのはWebSocket-Nodeのみだった。

当初はサーバーから画像のRAWデータをガンガンクライアントに送りまくるというのを作ろうとしたのだがnode-pngがNodeの0.6系に対応していなかったので断念。CanvasのgetImageDataで得られるデータをそのままサーバーに投げて、サーバー側で加工した物を受け取るサンプルを作った。

f:id:hagino_3000:20111209034908p:image:w500

バイナリデータの送信


クライアントのコード(client.js)

var socket = null;

function bootstrap() {

// 適当な図形を描画
var c = document.getElementById('mycanvas');
var ctx = c.getContext('2d');
// (中略)

// Socketの初期化
socket = new WebSocket('ws://localhost:8082');
socket.binaryType = 'arraybuffer';
socket.onopen = function() {
send(ctx);
}
socket.onmessage = handleReceive;
};

function send(ctx) {
// RAWデータをそのまま送信
var data = ctx.getImageData(0, 0, 200, 200).data;
var byteArray = new Uint8Array(data);
socket.send(byteArray.buffer);
}


CanvasのgetImageDataで得られるデータは8bit4チャンネルのRGBAの数値配列。1ピクセルあたり要素は4つ、それぞれが0-255の値なので、これをそのままUint8Arrayのコンストラクタに渡した。見なれないのは次の行ですね。

socket.binaryType = 'arraybuffer';


サーバーでRAWデータを加工


サーバーのコード(server.js)

#!/usr/bin/env node
var WebSocketServer = require('websocket').server,
http = require('http'),
fs = require('fs');

// (中略)

wsServer = new WebSocketServer({
httpServer: server,
// デフォルトでは65535byte以上受けつけないので
// 値を増やしてみる
maxReceivedFrameSize: 0x1000000,
autoAcceptConnections: false
});

wsServer.on('request', function(request) {

var connection = request.accept(null, request.origin);

connection.on('message', function(message) {
if (message.type === 'utf8') {
// テキストデータを受信した場合
}
else if (message.type === 'binary') {
// バイナリデータを受信した場合
var data = message.binaryData;
var len = data.length;

// 受信したRAW画像をグレースケールにする
var buf = new Buffer(len);
for (var i = 0; i < len; i+=4 ) {
var r = data.readUInt8(i);
var g = data.readUInt8(i+1);
var b = data.readUInt8(i+2);
var y = Math.floor((77*r + 28*g + 151*b)/255);

// Canvasにそのまま投入するために
// 4チャンネル8ビットのRGBAにする
var v = y + (y << 8) + (y << 16) + (0xFF << 24);
buf.writeInt32LE(v, i);
}
// グレースケールにした物をクライアントに送信する
connection.sendBytes(buf);
}
});

connection.on('close', function(reasonCode, description) {
// (略)
});
});


受信したデータを8bitずつ読んで、それぞれRGBAとして処理する。加工したデータはNodeのBufferオブジェクトに値を書き込んで connection.sendBytesメソッドに渡す。writeInt32LEを使ってバッファに書き込んでいるのはビット演算子が使いたかっただけ。writeUInt8を4回やってもいい。

バイナリデータの受信


再びクライアントのコード(client.js)

WebSocketのonmessageイベントハンドラにセットした関数。

function handleReceive(message) {
// 受信したRAWデータをcanvasに
var c = resultCanvas = document.getElementById('result');
var ctx = c.getContext('2d');
var imageData = ctx.createImageData(200, 200);
var pixels = imageData.data;

var buffer = new Uint8Array(message.data);
for (var i=0; i < pixels.length; i++) {
pixels[i] = buffer[i];
}
ctx.putImageData(imageData, 0, 0);
}


サーバーでやったのと同様に8bitずつ値を取得したかったので、Uint8Arrayを使った。

まとめ


結果は上のスクリーンショットの通り。全てのコードはgistにアップしました。




WebSocket with binary data ― Gist

https://gist.github.com/1447986


とりあえずNodeのBufferオブジェクトとTyped Arrayの使い方を覚えればなんとかなりそう。これからは送信する前にbase64する手間が省けて嬉しいですね。





このエントリーをはてなブックマークに追加