Kinect v2とopenFrameworksをイベントで使う

社内のファミリーデーでKinect v2を使った出し物をやった構成。アプリ的には、

・トラッキングした人だけ取って正面に投影
・天井にはトラッキングした人の位置の上に色んなタイプの紋様をクルクル回したり・大きさを変えながら投影
・手をグーにすると、電撃(複数人でグーにしていると、グー同士を電撃がつないでみんなでビリビリ)
・手をパーにすると、色が変化するパーティクルを物理係数をかけてパラーっと床まで落とす
・手をチョキにすると、ビームみたいに飛ばす
・落ちたパーティクルは足で蹴っ飛ばしたり踏みつけて消していく

こんな感じ
1524864_10152276961978244_7011368719679669408_n

使ったモノはこんな感じ

– PC
VAIO Duo 11, Core i5-3317U, Mem 4Gbyte/Windows 8 64bit
Macbook air Mid 2011, Core i5 1.6GHz, Mem 4Gbyte/OS X 10.9.4

– プロジェクタ
BenQ MS616ST
EPSON EP-1751

– Network
Planex MZK-RP150N

– 3Dセンサ
Kinect v2

– Software
openFrameworks 0.8.3(Win VC++ 2012/Mac Xcode), Kinect v2 SDK
素材作成用(Inkscape, Gimp), フリー音源

機材を雑然と全部つなげたのがこんな感じ
10322601_10152276962048244_5018764362456022686_n

pic1

そろそろ最新のMacbook Airが欲しい。けど、今の手持ちのMid 2011のでも十分性能も出ているし、Core i7・16G memを積んだMacbook ProはOculus用で使っちゃったのもあって、手持ちの物を使うことに。Planex MZK-RP150Nは通称「ちびファイ」。旅先でも重宝する割と好きなアイテムだったりする。

 

ファミリーデーで最新で面白い事をヨロピク、ということで真っ先に思いついたのがv2を使うことに。まだ出始めのv2だったら一般的(?)には最新かなと。あと、子供向けというのもあって、あんまり激しいギーク寄りにするとドン引きするし、そっち方向は止めようと。ただ、最初に色々と考えたのは、

– 当初案とボツ案
・部屋の正面・天井・左右面の全部に投影しようとしたけど、広さもあって止めた。
・アミッドスクリーンを複数面に配置して、それぞれちょっとづつ違う像を投影して空間的に面白い像をトラッキングしながら映そうと思った。プロジェクタの条件も出て来るから手持ちの範囲にすることに。
・床に投影したかったけど、天井にプロジェクタを付けられるよーに穴あけからするのはさすがに言えなかった(w
・よく最初のKinect Hack時にあった透明人間とかにしようかなと思ったけど、子供向けにウケがどうかしら、と思ってやめた。
・投影している天井に手を上げるとオブジェクトが出てきて、正面の投影に何かアイテムを落としたり、その逆も楽しいかな。けど、事前に何人かに見せたらあまり天井見ないのよね…正面の方がインパクトあるし、結局天井にフワフラ浮かばせる事にした。
・夜の渋谷の窓から見える風景を写真に撮って、それを投影しながらジェスチャーで星でも降らせても良いかも。何か簡単そうだから止めた。
・Arduino/ジャイロ/Xbee/LEDなアイテムを持ってもらって、それを振ったり動かして遊ぶモノにしようかな…PS/Wiiとかでそういうの遊んでるだろうし、作ってみてやっぱり止めた。
・トラッキング&投影した人に、手を使って落書きとか絵を書くのもありかなー。と思って自分でやってみたら、あんまり面白く無くって止めた…書いた状態でプリントアウトして渡す感じにすればアリだったかな。
・手の所に常時、幾何的な大小様々なキラキラを表示させておこうかしら…と表示してみたら割と綺麗で良い感じだった。

何と言うか、Arduinoとかの端末も用意してその場で作りながら考えるパターンで。部屋の机・椅子の運び出しとかの準備、採光が強いのもあって暗幕の手配とかしてくれて感謝です。プログラムとかはgithubの方にアップしてあるもので。センサーを部屋に配置して、空間全体をセンシングな状態にしてみたいとかはあるけど機会があったら。

ウケ自体は割と良い感じでした。ずっと走り回ったり、手から色々と出して遊んでいたり、午後からはずっと誰かしら遊んでいたですね。自分もずっと説明で体をたっぷり動かして良い運動になった感じ。

openFrameworksでKinect v2

ちょっとサンプルと実際に使う感じで整理したクラスをアップした。プラグインって程でもなく、サクット使える感じで。Visual Studioの設定とかはこっちを参照

kinectv2sample
ofxKinectv2Image

流れ的にはKinect v2用SDKの細かい処理はバックグラウンドスレッドで実行して、フロントではバックグラウンドスレッドで取得したデータ処理を行う感じ。色んな方のソースをつまみ食い参考にさせてもらって、マルチスレッド用のロックをちょこちょこ入れている。メモに近いコードだけど、まぁ普通に使ってこんな感じの電気ビリビリ&パーティクルパラパラとかもサクっと出来る感じ。

(続)8pinoを使ってみる

8pinoを使ってみるに続いて、8pinoを使ってArduinoっぽいことをしてみる。まず最初に…ブレッドボードに指せるように8pinoにピンヘッダを付ける。これはわりと決断が必要になる。いや、作業自体はサクッと出来るんだけど、AgICにくっ付けたみたいに、このめちゃ小さい&薄い8pinoにピンヘッダを付けて良いものかどうか…しかも1stの8個のうちの1個をゲットしたのに、ピンヘッダを付けて大きくする事はこの小ささに反する行為なのでは無いか…という決断力が必要になる。そう、自分の判断が正しいのかどうか?という心の葛藤が…

ブレッドボードに差したいという欲求は、直撃でハンダ付けするよりお手軽に試せるという所もあって…まぁ、あとでピンヘッダはハンダを取って外しちゃえば良いっか!!と思って、ピンヘッダを付けちゃうことに><

まずは、リセットボタンとユニバーサル基板になっている部分を切離して、

IMG_3770

まずはブレッドボードにピンヘッダを差しといて、この上に8pinoを置いてハンダ付けをする。
IMG_3773

差しといたピンヘッダに8pinoを置いて…ちょちょっとハンダを付けて…
pic2

かんりょー!!8pinoが小さすぎて、iPhoneのカメラだとピンぼけしまくる素敵っぷりwww
pic

続いて動作確認。USBを差してバッチリ動く。
IMG_3782

んで、これから実際のセンサーを使ってみる。ここでは、目の前にあったGP2Y0A21YK(赤外線距離センサー)とLEDを使って、距離に応じてLEDの光をPWMで強弱を付ける感じの物を作ることに。
Arduino IDEでサクッと書けるから、ちょーーーー簡単!!!!以下な感じでバッチリ動く。
IMG_3783

スケッチはめちゃシンプル。[len*2]ってしているのは、GP2Y0A21YKが距離が60-80cmくらいまでしか取れないから、2倍してPWM(0-255)に合わせて光の強さを変えている。

void setup() {
  pinMode(0, OUTPUT);
  pinMode(1, INPUT);
}

void loop() {
  int val = analogRead(1);
  int len = 18618/val;
  
  if (len * 2 <= 255)
    analogWrite(0, len * 2);
  delay(10);
}

8pinoを使ってみる

品モノラボに行ってきた。何気に自分、行くのが2回目という(いつも行こうと思ってはいるですが…)。いやー、刺激的っすねー。Web系でもあまり聞かないようなブッ飛び系のアイディアの話を聞いたりして、率直にモノ系以外の人でも参加してみたらメッチャ面白いと思う。

そして…8/8とくれば、8pinoにドキドキしていたら、田中さんよりジャンケン大会で限定8個の争奪戦と!!オイラ、ジャンケン大会とかめっちゃ弱いというか自信のひとかけらも無かったけど、勝っちゃいました。この場で勝負運を発動できたようで、ホントまぢで嬉しいぃぃぃ。

その場でまずは開封の儀を。まずは包装はこんな感じでカッコイイ。
IMG_3721

中身をあけると…梱包物一覧は。
IMG_3722

ちっちぇぇぇぇー!!早速、その場でマイクロUSBの端子をお借りしてチカチカーっと!!
IMG_3734

この小ささに反比例して、むっちゃテンション上がりまくり。深爪な自分の指先ほどの大きさ。
Arduino IDEでの使い方は8pinoにアップされている通りで、自分はMacOS/Windows7のどちらでもArduino IDEからソースを焼け込めた。Windows7の方は追加でドライバを入れれば問題無し。MacはTrinketのIDEを入れて即使える。以下、Macでのやり方は…

Arduino IDEをサイトからダウンロードしたら書いてある通りに、まずはマイコンボードで「Trinket 8MHz」を選択。
setup1

次に「USBtinyISP」を選択
setup2

これで完了。ちなみに、Arduino IDEから書き込みの時、”Could not find USBtiny device (0x1781/0xc9f)”なんて書き込み失敗が出ると思うけど、8pinoとUSBの抜き差しがリセットという事は…そう、8pinoのUSBを抜き差しした10秒間が書き込みのとき。抜き差しした時にIDEから書き込みをポチっとすれば、すんなり書き込み完了する。
setup3

そして、8pinoをマイクロUSBにつけてLチカのコードをサクッと書き込んで終了!!LEDのピンを挟む感じで付けると特にハンダも使わないで動確できる。こんな感じでLEDのピンでキュッと挟んで。
pic

チカチカー!!
IMG_3748

そしてAgICとコラボってみる
IMG_3741

薄っすーぃ!!ペラッペラで紙と一体化してるよw
サクッと折り紙にして仕込んじゃえるね。

AgICのは、最初の8個という事で、いつもの勢いでしょっぱなからハンダ付けするのは忍び無さ過ぎる…と思ってAgICを使って紙に書くことにした。ATTINY85と一緒な感じでPWMもバッチリー。ちょぴーっとだけ、ハンダ付けしちゃおっかなー。ピンヘッダを付けてブレットボードに刺せるのは合わせてみたらバッチリ出来るのを確認したし、あとはハンダ付けしちゃおーかしらーってオイラの心ひとつ。折角AgIC使って載せられたから、紙の上にセンサー載せちゃおーかしら。指先に8pinoを載せて、何かセンサー反応させて指先ピカピカー!!とか。

Arduino IDEでプログラムをサクッと書いて、この小ささに収めて動かせるというのは、かなり可能性を感じる。Makerやモノ寄りな人だけじゃなく、アートや音系、クリエイターの方でもArduinoを使った作品はよく見るし、この極小サイズは考えること&想像力(妄想力)が膨らみまくると思う。

追記) ピンヘッダの取り付けと、GP2Y0A21YK(赤外線センサ)を一緒に使ってみた内容はこちら。(続)8pinoを使ってみる

openFrameworks 0.8.3 and Kinect v2

環境は以下
– Windows 8.0
– VS 2012
– openFrameworks 0.8.3(x86)

まずMSのサイトからKinect SDKをダウンロードしてきてインストール。

1. Includeに$(KINECTSDK20_DIR)¥inc;を追加
setup11

2. Libに$(KINECTSDK20_DIR)¥lib¥x86;を追加
setup22

3. DependenciesにKinect20.libを追加
setup32

4. コードを書く
Kinect for Windows v2 Developer Preview入門 ― C++プログラマー向け連載をほぼコピってof向けに整形しただけですが…

ofApp.h

#pragma once

#include "ofMain.h"
#include "ofxOpenCv.h"
#include <kinect.h>
#include <Windows.h>

using namespace cv;

template
inline void SafeRelease( Interface *& pInterfaceToRelease )
{
	if( pInterfaceToRelease != NULL ){
		pInterfaceToRelease->Release();
		pInterfaceToRelease = NULL;
	}
}

class ofApp : public ofBaseApp{
	public:
		void setup();
		void update();
		void draw();

		bool initializeKinectv2();

		IKinectSensor* pSensor;
		IDepthFrameSource* pDepthSource;
		IColorFrameSource* pColorSource;
		IBodyFrameSource* pBodySource;

		IColorFrameReader* pColorReader;
		IBodyFrameReader* pBodyReader;
		IDepthFrameReader* pDepthReader;

		IFrameDescription* pDepthDescription;
		IFrameDescription* pColorDescription;

		ICoordinateMapper* pCoordinateMapper;


		// buffer
		ofxCvGrayscaleImage grayscaleImage;
		ofxCvColorImage colorscaleImage;

		// 
		int depthWidth, depthHeight;
		unsigned int depthBufferSize;

		int colorWidth, colorHeight;
		unsigned int colorBufferSize;

};

ofApp.cpp

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
	ofSetVerticalSync(true);
	ofBackground(255, 255, 255);
	// ofSetFullscreen(true);
	ofEnableAlphaBlending();
	ofEnableSmoothing();
	ofSetBackgroundAuto(true);

	if (!this->initializeKinectv2())
		exit();
}

bool ofApp::initializeKinectv2() {
	HRESULT hResult = S_OK;

	// Open Kinect
	hResult = GetDefaultKinectSensor( &this->pSensor );
	if( FAILED( hResult ) ){
		std::cerr << "Error : GetDefaultKinectSensor" << std::endl;
 		return false;
 	}
 	hResult = this->pSensor->Open( );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IKinectSensor::Open()" << std::endl;
 		return false;
 	}

 	// Open Source
 	hResult = this->pSensor->get_ColorFrameSource( &this->pColorSource );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IKinectSensor::get_ColorFrameSource()" << std::endl;
 		return false;
 	}
 	hResult = this->pSensor->get_BodyFrameSource( &this->pBodySource );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IKinectSensor::get_BodyFrameSource()" << std::endl;
 		return false;
 	}
 	hResult = pSensor->get_DepthFrameSource( &this->pDepthSource );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IKinectSensor::get_DepthFrameSource()" << std::endl;
 		return false;
 	}
 	// Open Reader
 	hResult = this->pColorSource->OpenReader( &this->pColorReader );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IColorFrameSource::OpenReader()" << std::endl;
 		return false;
 	}
 	hResult = this->pBodySource->OpenReader( &this->pBodyReader );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IBodyFrameSource::OpenReader()" << std::endl;
 		return false;
 	}
 	hResult = this->pDepthSource->OpenReader( &this->pDepthReader );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IDepthFrameSource::OpenReader()" << std::endl;
 		return false;
 	}
 	// get descriptions
 	hResult = pDepthSource->get_FrameDescription( &this->pDepthDescription );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IDepthFrameSource::get_FrameDescription()" << std::endl;
 		return false;
 	}
 	hResult = pColorSource->get_FrameDescription( &this->pColorDescription );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IColorFrameSource::get_FrameDescription()" << std::endl;
 		return false;
 	}
 	// get coordinate mapper
 	hResult = this->pSensor->get_CoordinateMapper( &this->pCoordinateMapper );
	if( FAILED( hResult ) ){
		std::cerr << "Error : IKinectSensor::get_CoordinateMapper()" << std::endl;
 		return false;
 	}
 	this->pDepthDescription->get_Width( &depthWidth ); // 512
	this->pDepthDescription->get_Height( &depthHeight ); // 424
	this->depthBufferSize = depthWidth * depthHeight * sizeof( unsigned short );

	this->pColorDescription->get_Width( &colorWidth );
	this->pColorDescription->get_Height( &colorHeight );
	this->colorBufferSize = colorWidth * colorHeight * 4 * sizeof( unsigned char );

	this->grayscaleImage.allocate(depthHeight, depthWidth);
	this->colorscaleImage.allocate(colorHeight, colorWidth);

}

//--------------------------------------------------------------
void ofApp::update(){

	// get depth frame
	Mat bufferMat( depthHeight, depthWidth, CV_16SC1 );
	Mat depthMat(depthHeight, depthWidth, CV_8UC1 );

	cv::Mat colorBufferMat( colorHeight, colorWidth, CV_8UC4 );
	cv::Mat colorMat( colorHeight, colorWidth, CV_8UC4 );

	// Frame
	IDepthFrame* pDepthFrame = nullptr;
	HRESULT hResult = pDepthReader->AcquireLatestFrame( &pDepthFrame );

	if(SUCCEEDED( hResult )){
		hResult = pDepthFrame->AccessUnderlyingBuffer( &depthBufferSize, reinterpret_cast<UINT16**>( &bufferMat.data ) );
		if( SUCCEEDED( hResult ) ){
			bufferMat.convertTo(depthMat, CV_8U, -255.0f / 4500.0f, 255.0f);
			grayscaleImage.setFromPixels(depthMat.data, depthWidth, depthHeight);
		}
	}
	SafeRelease( pDepthFrame );


	// get color frame
	cv::Vec3b color[6];
	color[0] = cv::Vec3b( 255,   0,   0 );
	color[1] = cv::Vec3b(   0, 255,   0 );
	color[2] = cv::Vec3b(   0,   0, 255 );
	color[3] = cv::Vec3b( 255, 255,   0 );
	color[4] = cv::Vec3b( 255,   0, 255 );
	color[5] = cv::Vec3b(   0, 255, 255 );

	IColorFrame* pColorFrame = nullptr;
	hResult = pColorReader->AcquireLatestFrame( &pColorFrame );
	if( SUCCEEDED( hResult ) ){
		hResult = pColorFrame->CopyConvertedFrameDataToArray( colorBufferSize, reinterpret_cast<BYTE*>( colorBufferMat.data ), ColorImageFormat_Bgra );
		/*
		if( SUCCEEDED( hResult ) ){
			cvtColor(colorBufferMat, colorMat, CV_BGR2RGB);
			colorscaleImage.setFromPixels(colorMat.data, colorWidth, colorHeight);
		}
		*/
	}
	SafeRelease( pColorFrame );

	// get body frame
	IBodyFrame* pBodyFrame = nullptr;
	hResult = pBodyReader->AcquireLatestFrame( &pBodyFrame );
	if( SUCCEEDED( hResult ) ){
		IBody* pBody[BODY_COUNT] = { 0 };
		hResult = pBodyFrame->GetAndRefreshBodyData( BODY_COUNT, pBody );
		if( SUCCEEDED( hResult ) ){
			for( int count = 0; count < BODY_COUNT; count++ ){
 				BOOLEAN bTracked = false;
 				hResult = pBody[count]->get_IsTracked( &bTracked );
				if( SUCCEEDED( hResult ) && bTracked ){
					Joint joint[JointType::JointType_Count];
					hResult = pBody[ count ]->GetJoints( JointType::JointType_Count, joint );
					if( SUCCEEDED( hResult ) ){
						// Left Hand State
						HandState leftHandState = HandState::HandState_Unknown;
						hResult = pBody[count]->get_HandLeftState( &leftHandState );
						if( SUCCEEDED( hResult ) ){
							ColorSpacePoint colorSpacePoint = { 0 };
							hResult = pCoordinateMapper->MapCameraPointToColorSpace( joint[JointType::JointType_HandLeft].Position, &colorSpacePoint );
							if( SUCCEEDED( hResult ) ){
								int x = static_cast( colorSpacePoint.X );
								int y = static_cast( colorSpacePoint.Y );
								if( ( x >= 0 ) && ( x < colorWidth ) && ( y >= 0 ) && ( y < colorHeight ) ){ 									if( leftHandState == HandState::HandState_Open ){ 										cv::circle( colorBufferMat, cv::Point( x, y ), 75, cv::Scalar( 0, 128, 0 ), 5, CV_AA );
 									} else if( leftHandState == HandState::HandState_Closed ){
 										cv::circle( colorBufferMat, cv::Point( x, y ), 75, cv::Scalar( 0, 0, 128 ), 5, CV_AA );
 									} else if( leftHandState == HandState::HandState_Lasso ) {
 										cv::circle( colorBufferMat, cv::Point( x, y ), 75, cv::Scalar( 128, 128, 0 ), 5, CV_AA );
 									}
 							        }
 							}
 						}
 						// Right Hand State
 						HandState rightHandState = HandState::HandState_Unknown;
 						hResult = pBody[count]->get_HandRightState( &rightHandState );
						if( SUCCEEDED( hResult ) ){
							ColorSpacePoint colorSpacePoint = { 0 };
							hResult = pCoordinateMapper->MapCameraPointToColorSpace( joint[JointType::JointType_HandRight].Position, &colorSpacePoint );
							if( SUCCEEDED( hResult ) ){
								int x = static_cast( colorSpacePoint.X );
								int y = static_cast( colorSpacePoint.Y );
								if( ( x >= 0 ) && ( x < colorWidth ) && ( y >= 0 ) && ( y < colorHeight ) ){
									if( rightHandState == HandState::HandState_Open ){
										cv::circle( colorBufferMat, cv::Point( x, y ), 75, cv::Scalar( 0, 128, 0 ), 5, CV_AA );
									}
									else if( rightHandState == HandState::HandState_Closed ){
										cv::circle( colorBufferMat, cv::Point( x, y ), 75, cv::Scalar( 0, 0, 128 ), 5, CV_AA );
									}
									else if( rightHandState == HandState::HandState_Lasso ){
										cv::circle( colorBufferMat, cv::Point( x, y ), 75, cv::Scalar( 128, 128, 0 ), 5, CV_AA );
									}
								}
							}
						}

						// Joint
						for( int type = 0; type < JointType::JointType_Count; type++ ){
 							ColorSpacePoint colorSpacePoint = { 0 };
 							pCoordinateMapper->MapCameraPointToColorSpace( joint[type].Position, &colorSpacePoint );
							int x = static_cast( colorSpacePoint.X );
							int y = static_cast( colorSpacePoint.Y );
							if( ( x >= 0 ) && ( x < colorWidth ) && ( y >= 0 ) && ( y < colorHeight ) ){
								cv::circle( colorBufferMat, cv::Point( x, y ), 5, static_cast< cv::Scalar >( color[count] ), -1, CV_AA );
							}
						}
					}
				}
			}
			cvtColor(colorBufferMat, colorMat, CV_BGR2RGB);
			colorscaleImage.setFromPixels(colorMat.data, colorWidth, colorHeight);
		}
	}
	SafeRelease( pBodyFrame );

}

//--------------------------------------------------------------
void ofApp::draw(){
	colorscaleImage.draw(0, 0);
	grayscaleImage.draw(0, 0);
}

5. 実行
setup44

脳波で迷路@ヤミ市

迷路いっぱい出来ました。ヤミ市で作成した、79人分の迷路はこちらにアップしています。

みんなの脳波を集めて迷路を作るよ

個々人の迷路は32×32マスで作成して、生成には約1分で完了ですね。2014-08-03のタイムスタンプのが当日作成&収集した迷路群です。そして79人分全員の脳波データを集めて1個の大きな迷路を作成しました。こちらは120×120マスで1時間半くらい生成にかかりました。結果はこちらです。

yami-brain-wave

こちらは、誰かが(脳波で)作成した個人の迷路です。

32-maze

同じサイズで見ると、密度が全然違いますね。ロジック的に32マスの方は生成される迷路の種類は900の階乗(900x899x898x…3x2x1)くらいで、120マスの方は13924の階乗個くらいです。どちらも膨大なパターンです。この迷路をセキュリティの鍵、認証、ワンタイムパスワードとかで使いたいくらいです。
ちなみにこの迷路、ちゃんと迷路として成立しています。こんな感じで、入り口と出口を用意すると解けます。

brainimage_1407175864

他の個々人で作成した迷路も「入り口・出口」を用意すると解けます。変な所に用意すると閉路になっている所もあるので。最後は持って行った光沢紙が無くって終了でした。