Blog

2011.09.13

AnaglyphとKinectを使った3DUI

Yu Nejigane

Engineer

祢次金です。

最近、映画やテレビ、ゲームなど、色々な場面で3Dコンテンツに触れる機会が増えてきました。が、現状は3Dコンテンツは受動的に見ているだけ、あるいは旧来からある入力装置を介して操作する場合が多いかと思います。折角3Dで見えているなら、そのまま手で直接操作するかのようなUIを作れないかと思い、Kinectを使って実験してみました。(つまり、今回もKinectネタです。)

操作にはKinectを使うとして、3D映像はどうやって作るかですが、映画にあるような偏光方式やシャッター方式向けの映像を個人で作るのは難しそうです。そこで今回はアナグリフ方式を採用することにしました。画像を左眼、右眼用に赤青2成分に分離して描画し、赤青メガネで見る方式です。この方式であれば特殊な機材は必要ありません。赤青メガネも下の写真のようなものが、Amazonなどで簡単に手に入ります。

今回は実験として、立方体のワイヤフレームを赤と青で描画しつつ、Kinectから受け取る入力に応じてその立方体をぐるぐる回す、というシンプルなものを実装しました。ワイヤフレームをアナグリフ用に描画すると下記のようになります。これを赤青メガネで見つつ、Kinect経由で操作します。

前回と同様、描画側はAdobe AIRをベースとし、立方体の描画にはPapervision3Dを利用しました。KinectとAIR間の通信も、前回と同じくOSCプロトコルです。残念ながらブログ記事ではアナグリフの3D具合や操作感を詳しくお伝えできないのですが、以下のような雰囲気でKinectを通じて3Dモデルを操作することができました。

スクリーンを使うと幾分迫力は出るのですが、単なる立方体だけだとやはり寂しいので、機会があればもっと複雑なモデルに対し高度な操作ができるようにしてみたいところです。

最後にソースを掲載しておきます。
まずは、AIR側のActionScriptのソース、AnaglyphView.as。

package {
	import org.papervision3d.materials.WireframeMaterial;
	import org.papervision3d.objects.primitives.Cube;
	import org.papervision3d.view.BasicView;
	import org.papervision3d.objects.DisplayObject3D;
	import org.papervision3d.materials.utils.MaterialsList;
	import org.papervision3d.core.math.Matrix3D;

	public class AnaglyphView extends BasicView {
		private var container:DisplayObject3D;

		public function AnaglyphView(gap:Number) {
			container = new DisplayObject3D();
			scene.addChild(container);

			var material:WireframeMaterial = new WireframeMaterial(0xffffff, 1, 3);
			material.oneSide = false;
			var list:MaterialsList = new MaterialsList({all:material});
			var cube:Cube = new Cube(list, 300, 300, 300, 1, 1, 1);
			container.addChild(cube);

			this.camera.x = gap;
			this.camera.z = -600;
			this.startRendering();
		}

		public function rotate(dx:Number, dy:Number):void {
			var my:Matrix3D = Matrix3D.rotationMatrix(0, 1, 0, dx);
			var mx:Matrix3D = Matrix3D.rotationMatrix(1, 0, 0, dy);
			var m:Matrix3D = Matrix3D.multiply(mx, my);
			container.transform = Matrix3D.multiply(m,container.transform);
		}
	}
}

このAnaglyphViewを左眼/右眼用に用意し、受信するOSCメッセージに応じて重ねて描画するのが以下のMain.asです。

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.BlendMode;
	import flash.geom.ColorTransform;
	import flash.geom.Rectangle;
	import flash.geom.Matrix;
	import org.tuio.connectors.UDPConnector;
	import org.tuio.osc.IOSCListener;
	import org.tuio.osc.OSCManager;
	import org.tuio.osc.OSCMessage;

	public class Main extends Sprite implements IOSCListener {
		private var oscManager:OSCManager;
		private var bitmapData:BitmapData;
		private var redView:AnaglyphView;
		private var blueView:AnaglyphView;
		private var gap:Number;

		public function Main() {
			gap = 10;
			redView = new AnaglyphView(-gap);
			blueView = new AnaglyphView(gap);
			addEventListener(Event.ADDED_TO_STAGE, onAddedToStage);
		}

		private function onAddedToStage(e:Event):void {
			oscManager = new OSCManager(new UDPConnector());
			oscManager.addMsgListener(this);
			addChild(redView);
			addChild(blueView);
			bitmapData = new BitmapData(stage.width,stage.height,false,0x000000);
			addChild(new Bitmap(bitmapData));
			drawViews();
		}

		private function drawViews():void {
			bitmapData.fillRect(new Rectangle(0, 0, stage.width, stage.height), 0x000000);
			bitmapData.draw(redView, new Matrix(1, 0, 0, 1, gap/4), new ColorTransform(1, 0, 0));
			bitmapData.draw(blueView, new Matrix(1, 0, 0, 1, -gap/4), new ColorTransform(0, 1, 1), BlendMode.ADD);
		}

		public function acceptOSCMessage(oscmsg:OSCMessage):void {
			if (oscmsg.address == "/touch") {
				var dx:Number = oscmsg.arguments[0];
				var dy:Number = oscmsg.arguments[1];
				redView.rotate(dx/100, -dy/100);
				blueView.rotate(dx/100, -dy/100);
				drawViews();
			}
		}
	}
}

このMainのインスタンスを適当な大きさのstageにaddすれば動くかと思います。各眼用のカメラ位置などのパラメータはいい加減に決めているので、環境によって調整が必要かと思います。

続いて、Kinect側ですが、ofxKinectのサンプルプロジェクトをコピーしたら、ofxOscをアドオンとして加え、testApp.hとtestApp.cppを修正します。

#ifndef _TEST_APP
#define _TEST_APP

#include "ofMain.h"

#include "ofxOpenCv.h"
#include "ofxKinect.h"
#include "ofxOsc.h"

class testApp : public ofBaseApp
{
	public:
		void setup();
		void update();
		void draw();
		void exit();
		void keyPressed  (int key);

		ofxKinect kinect;
		ofxCvGrayscaleImage grayImage;
		ofxCvContourFinder 	contourFinder;
		ofxOscSender sender;
		int 				nearThreshold;
		int					farThreshold;
		int					angle;
		ofxPoint2f prevPoint;
};

#endif
#include "testApp.h"


//--------------------------------------------------------------
void testApp::setup()
{
	ofSetFrameRate(10);

	nearThreshold = 10;
	farThreshold  = 20;
	angle = 24;
	prevPoint.set(-1, -1);

	kinect.init();
	kinect.setVerbose(true);
	kinect.open();
	kinect.setCameraTiltAngle(angle);
	kinect.enableDepthNearValueWhite(true);

	grayImage.allocate(kinect.width, kinect.height);

	sender.setup("localhost", 3333);
}

//--------------------------------------------------------------
void testApp::update()
{
	ofBackground(100, 100, 100);
	kinect.update();

	grayImage.setFromPixels(kinect.getDepthPixels(), kinect.width, kinect.height);

	unsigned char * pix = grayImage.getPixels();
	int numPixels = grayImage.getWidth() * grayImage.getHeight();
	for(int i = 0; i < numPixels; ++i)
		pix[i] = (pix[i] > nearThreshold && pix[i] < farThreshold) ? 255 : 0;

	//update the cv image
	grayImage.flagImageChanged();

	int maxArea = kinect.width * kinect.height;
    contourFinder.findContours(grayImage, maxArea / 256, maxArea / 8, 1, false);

	if (contourFinder.blobs.size() > 0) {
		ofxCvBlob blob = contourFinder.blobs[0];
		if (prevPoint.x >= 0 && prevPoint.y >= 0 &&
			ofDist(prevPoint.x, prevPoint.y, blob.centroid.x, blob.centroid.y) >= 1) {
			ofxOscMessage m;
			m.setAddress("/touch");
			m.addFloatArg(blob.centroid.x - prevPoint.x);
			m.addFloatArg(blob.centroid.y - prevPoint.y);
			sender.sendMessage(m);
		}
		prevPoint.set(blob.centroid);
	} else {
		prevPoint.set(-1, -1);
	}
}

//--------------------------------------------------------------
void testApp::draw()
{
	ofSetColor(255, 255, 255);
	kinect.drawDepth(10, 10, 400, 300);
	grayImage.draw(420, 10, 400, 300);
	contourFinder.draw(420, 10, 400, 300);

	ofSetColor(255, 255, 255);
	char reportStr[1024];
	sprintf(reportStr, "set near threshold %i (press: + -)nset far threshold %i (press: < >) num blobs found %i, fps: %f",nearThreshold, farThreshold, contourFinder.nBlobs, ofGetFrameRate());
	ofDrawBitmapString(reportStr, 10, 340);
}

//--------------------------------------------------------------
void testApp::exit(){
	kinect.close();
}

//--------------------------------------------------------------
void testApp::keyPressed (int key)
{
	switch (key)
	{
		case '>':
		case '.':
			farThreshold ++;
			if (farThreshold > 255) farThreshold = 255;
			break;
		case '<':
		case ',':
			farThreshold --;
			if (farThreshold < 0) farThreshold = 0;
			break;

		case '+':
		case ';':
			nearThreshold ++;
			if (nearThreshold > 255) nearThreshold = 255;
			break;
		case '-':
			nearThreshold --;
			if (nearThreshold < 0) nearThreshold = 0;
			break;

		case OF_KEY_UP:
			angle++;
			if(angle>30) angle=30;
			kinect.setCameraTiltAngle(angle);
			break;
		case OF_KEY_DOWN:
			angle--;
			if(angle<-30) angle=-30;
			kinect.setCameraTiltAngle(angle);
			break;
	}
}

それでは!

  • Twitter
  • Facebook