深度センサーカメラ+OSC


今更ながらKinectを借りる機会があったのでUnityに繋いでみました。(しかもv1)
Kinectの開発に関わっていたPrimeScene社がオープンソースでOpenNIを作っていたけど、PrimeScene社はAppleに買収されその後配布サイトが閉鎖。いまならIntelのReal ScenseD415/D435を使って、ミドルウェアにNUItrack(Winのみ)がよさそうな気がします。
とりあえず今回はKinectをOpenNIをつかってUnity(Mac)で繋いでみました。

WindowsであればKinect v2 Examples with MS-SDK and Nuitrack SDKが使えそう。(って、あれ?最近更新されてるし、Nuitrackも対応してる!)

今回はMacでやりたかったので、Processing <= OSC => Unityという感じでやって見ました。

まずProcessingの方から。
Processingは3.3.7。ProcessingではOpenNIとNiteというボーン検出するライブラリのラッパーとしてSimpleOpenNIというのがあるのでそれをインポート。以前はライブラリを追加から検索してボタン一つで入ったようだけど、いま探して見たら見つからなかったのでこちらのgithubからDL。
OSCはライブラリから検索するとoscP5というのがあるのでそいつをインポート。

続いてUnity。UnityにはいくつかOSCのライブラリがあるけUnityOSCを使用。UnityはProcessingから送られてくるOSCにて座標を受け取って、ビューを操作するだけ。今回は頭の位置だけとって、Unity側でEthanのキャラの頭を動かすことに。

まずはProcessing側。SimpleOpeNIから深度センサーをONにしてボーン検出したら頭の位置をOSCにて送信。

import SimpleOpenNI.*;
import oscP5.*;
import netP5.*;

final color drawColor = color(255, 0, 0);
final int sendPort = 55555;
final int receivePort = 50000;
final String sendIP = "127.0.0.1";
final String addressPattern = "/head";
final int width = 640;
final int height = 480;
final int frameRate = 30;

SimpleOpenNI  context;
int detectedUserID = -1;
NetAddress remoteLocation;
OscP5 osc;
float neckLength;

void settings() {
  size(this.width, this.height);
}

void setup() {
  frameRate(this.frameRate);

  this.context = new SimpleOpenNI(this);
  
  if (this.context.isInit() == false) {
     println("Can't init SimpleOpenNI, maybe the camera is not connected!"); 
     exit();
     return;  
  }
  
  // enable depthMap generation 
  if (!context.enableDepth())
    println("depth is not supported.");
   
  // enable skeleton generation for all joints
  if (!context.enableUser())
    println("skelton is not supported.");
  
  this.osc = new OscP5(this, this.receivePort);
  this.remoteLocation = new NetAddress(this.sendIP, this.sendPort);
 
  background(0, 0, 0);
  strokeWeight(5);
}

void draw() {
  this.context.update();
  
  image(this.context.depthImage(), 0, 0);
  //image(context.userImage(), 0, 0); // paint user shape
  
  if (this.detectedUserID != -1 && this.context.isTrackingSkeleton(this.detectedUserID)) {
    stroke(this.drawColor);
    this.context.drawLimb(this.detectedUserID, SimpleOpenNI.SKEL_HEAD, SimpleOpenNI.SKEL_NECK);
    this.SendHeadPos();
  }
}

void SendHeadPos() {
  OscMessage message = new OscMessage(this.addressPattern);
  PVector neckPos = new PVector();
  PVector headPos = new PVector();

    this.context.getJointPositionSkeleton(this.detectedUserID, SimpleOpenNI.SKEL_NECK, neckPos);
    this.context.getJointPositionSkeleton(this.detectedUserID, SimpleOpenNI.SKEL_HEAD, headPos);

  if (this.neckLength == 0f) {
    // check original neck length
    this.neckLength = headPos.y - neckPos.y;
  }
  else {
    message.add(headPos.x - neckPos.x);
    message.add(headPos.y - neckPos.y - this.neckLength);
    message.add(headPos.z - neckPos.z);
    
    this.osc.send(message, this.remoteLocation);
  }
}

// SimpleOpenNI events
void onNewUser(SimpleOpenNI c, int userID) {
  println("Detected User : " + userID);
  if (this.detectedUserID == -1) {
    PVector neckPos = new PVector();
    PVector headPos = new PVector();
    this.context.startTrackingSkeleton(userID);
    this.detectedUserID = userID;
  }
}

void onLostUser(SimpleOpenNI c, int userID) {
  println("Lost User : " + userID);
  if (this.detectedUserID == userID) {
    this.detectedUserID = -1;
    this.neckLength = 0f;
  }
}

ちなみにProcessingからアプリにエクスポートするときはSimpleOpenNI側でエラーが出るようで、こちらのようにルート直下にSimpleOpenNIのライブラリを置く必要がある。

続いてUnity側。UnityOSCをつかってデータを待つ。頭の位置を受け取ってEthanの頭を動かすことに。頭の角度は適当に首の位置と合わせて回転させる。あとKinect側の単位はmmなので、Unityのメートルに合わせる。

using UnityEngine;
using UnityOSC;

public class HeadPosReceiver : MonoBehaviour {
	/// <summary>
	/// 頭の位置。
	/// </summary>
	[SerializeField]
	Transform headTrans;

	/// <summary>
	/// 首の位置。
	/// </summary>
	[SerializeField]
	Transform neckTrans;

	/// <summary>
	/// 頭のデフォルトの位置/角度。
	/// </summary>
	Vector3 originalHeadPos;
	Vector3 originalHeadAngles;

	/// <summary>
	/// 待ち受けるポート番号。
	/// </summary>
	int receivePort = 55555;

	/// <summary>
	/// 受け取るアドレス。
	/// </summary>
	string receiveAddressPattern = "/head";

	/// <summary>
	/// サーバー。
	/// </summary>
	OSCServer server;

	/// <summary>
	/// Start.
	/// </summary>
	void Start() {
		this.originalHeadPos = this.headTrans.position;
		this.originalHeadAngles = this.headTrans.eulerAngles;

		// 初期化
		OSCHandler.Instance.Init();
		this.server = OSCHandler.Instance.CreateServer("server name", this.receivePort);
	}

	/// <summary>
	/// Update.
	/// </summary>
	void Update () {
		Vector3 gap = this.headTrans.position - this.neckTrans.position;
		this.headTrans.eulerAngles = new Vector3(Mathf.Atan2(gap.y, gap.x) * Mathf.Rad2Deg - 90f,
		                                         0f,
		                                         Mathf.Atan2(gap.y, gap.z) * Mathf.Rad2Deg - 90f)
			+ this.originalHeadAngles;

		// OSCのパケットを処理
		for (var i = 0; i < OSCHandler.Instance.packets.Count; i++) {
			// パケット内容を処理
			this.ReceivedOSC(OSCHandler.Instance.packets[i]);

			// 処理済みデータを削除
			OSCHandler.Instance.packets.Remove(OSCHandler.Instance.packets[i]);
			i--;
		}
	}

	/// <summary>
	/// パケット内容を処理。
	/// </summary>
	void ReceivedOSC(OSCPacket packet) {
		if (packet == null) { Debug.Log("Empty packet"); return; }
		if (packet.Address != this.receiveAddressPattern) return;

		if (packet.Data.Count == 3) {
			Vector3 headMovement = new Vector3((float)packet.Data[0], (float)packet.Data[1], (float)packet.Data[2]);

			this.headTrans.position = headMovement * 0.001f + this.originalHeadPos; // ミリメートル -> メートル
		}
	}
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です