1ビットの孤独

コンピュータに関する技術系ブログ。斎藤 康毅のブログ。

「天才けんけんぱ」:Unityとセンサーの連携(2/4)

本記事は次に示す4つのページから構成されています。
「天才けんけんぱ」:Unityとセンサーの連携(1/4)
「天才けんけんぱ」:Unityとセンサーの連携(2/4)
「天才けんけんぱ」:Unityとセンサーの連携(3/4)
「天才けんけんぱ」:Unityとセンサーの連携(4/4)

1.2 Unityとセンサーの連携

現実世界のモノの動きをコンピュータに認識させるためにはセンサーが必要です。センサーが人の動きなどを計測し、そのデータをUnity側のプログラムに渡すことで、インタラクティブに反応するシステムを構築することができます。

ここではソフトウェアの構成として、図3に示すように、Unityで開発されたプログラムとセンサー用のプログラムの二つが実行されている環境を想定します(センサー用プログラムはC++などの言語で開発することが多いでしょう)。また、その二つのプロセス間で通信を行うためにOSC(OpenSound Control)[1]と呼ばれる通信プロトコルを使用します(OSCについては後ほど説明します)。

f:id:koki0702:20141120144348p:plain

図3.センサー用プログラムはOSCという通信プロトコルを介して、Unity側に情報を送信する

もちろん、センサーを取り付けただけでは何も解決されません。センサーから取得できるのは生のデータです。生データに手を加えて、必要な情報を抽出する作業は、各自で開発しなければなりません。本章ではセンサー側の実装については省略しますが、インタラクティブなシステムにおいては必須な作業になります。

1.2.1 センサーの選択

センサーには様々なモノがあります。Webカメラ、距離センサー、温度センサー、圧力センサー、また、最近だとKinectやXtionなどのような深度センサーも一般的になってきています。どのセンサーを使うかにあたって、いくつか基準があると思います。要求を達成できることはもちろんのこと、性能やコスト、業務用かどうかなど、いくつかの指標を天秤にかざして最適と思われるセンサーを選択する必要があります。

ここでの目標は、「人の足の位置をリアルタイムに取得する」ということです。そのために、本プロジェクトでは測域センサーを採用しました。次節では測域センサーについて説明を行います。

1.2.2 測域センサー

測域センサーは「レーザ・レンジ・スキャナー( Laser Range Scanner)」とも呼ばれ、自立型ロボットのセンサーとしてよく用いられます。測域センサーを用いることで、周囲の物理的な形状を計測することができます。今回使用したセンサーは一軸走査型の測域センサーです。図4に示すように、平面上に走査線を発し(図の赤いライン)、その光の飛行時間から、遮蔽物までの距離を計測することができます。

f:id:koki0702:20141120144350p:plain

図4.測域センサーのイメージ図:光の反射時間から距離を計測する

今回の例で言えば、床面に測域センサーを設置することで、センサーから人の足までの距離を計測することができます。走査線は角度が180度超えて放射され、検出エリアも広くカバーすることができます。業務用のため、コストはかかりますが、今回の用途には適していると言えるでしょう。

測域センサーを使用することで、極座標系で周囲の距離を取得できます(「45度の方向に130cm進んだ場所に遮蔽物がある」といった情報が取得できます)。この取得データに対して閾値処理を行い、極座標系データを直行座標に変換することで、xy座標系での遮蔽物の位置を求めることができます(極座標系を直行座標系に変換するには、単純な計算で行えます。このような機能は、測域センサーを提供しているベンダーがアプリケーションとして提供しているかもしれません)。しかし、このセンサーが計算したxy座標系の値をそのまま使うことはできません。センサーの座標系とプロジェクターが出力するUnity上での座標系との対応関係を考慮して変換する必要があります。

1.2.3 キャリブレーション

センサーの座標系をUnity上での座標系に対応させる必要があります。このような対応関係を求めることを「キャリブレーションを行う」と言います。通常「キャリブレーション」とは、機器固有のパラメータを求めることを言いますが、今回のように複数の座標系の対応関係を求めるときにも「キャリブレーション」という言葉を用います。

ここではキャリブレーションについて簡単な説明を行います。やりたいことは、センサーの座標系とUnityのピクセルでの座標系との対応関係を求めることです。たとえば、センサーから見てユーザの足がxy座標で(103cm, 53cm)の位置は、Unityのピクセル座標では(354px, 524px)に対応する、といったような対応関係を求めることが目標になります。

今回の問題では、センサーが計測する平面上の点とプロジェクターが投影する映像の平面上の点との対応関係を求めることになります。この平面上の点の変換は「ホモグラフィ変換」と言います。このホモグラフィ変換行列を求めれば、センサー座標系をUnity座標系に変換することができます。

ホモグラフィ行列を求めるためには、センサー座標とUnity座標の対応する点が4点以上必要です。この対応点としての4点は、たとえば、プロジェクターの映像が出力する四隅を使うのが簡単でしょう。今、ディスプレイの出力サイズが(横1920px、縦1200px)であったとします。図5に示すように、プロジェクターが出力する映像の四隅にモノを置いて、センサーの座標値を記録すれば、この4点の対応関係からホモグラフィ行列を求めることができます(ホモグラフィ行列を求める計算は、OpenCVなどのライブラリを使うのが簡単です)。

f:id:koki0702:20141120144351p:plain

図5.プロジェクターで投影される映像の四隅において、センサーの座標を計測し、Unity座標とセンサー座標の対応関係を求める。その4箇所の対応関係から、ホモグラフィ変換行列Hを求める。

ホモグラフィ変換行列は3×3の行列になります。この行列が求まったら、センサーの取得した位置データに対してホモグラフィ行列を掛けることで、Unity上での座標系に変換することができます。図6には、具体的な計算フローを示しています。ここでは、センサーの座標系は(x, y)の2次元データなので、3番目の次元に1を追加した(x,y,1)となる行列を用い、ホモグラフィ行列を掛けます。また、最終的な座標は、行列の1,2次元目の値を3次元目の値で割り算した値になります。

f:id:koki0702:20141120144353p:plain

図6.センサー座標系を先ほど求めたホモグラフィ行列を掛け算して、Unity座標系へ変換

ここで4つの対応関係からホモグラフィ行列を求めるためには、たとえば、OpenCVというコンピュータビジョンライブラリのfindHomographyという関数を用いることで求めることができます[2]。ホモグラフィ変換の詳しい解説については『詳解 OpenCV [3]』を参考にして下さい。

以上でUnity座標系におけるユーザ位置を求めることができました。センサー側のプログラムは、変換後の座標値をUnityにOSCで送信します。それでは続いて、OSCによる送受信について解説します。

1.2.4 OSC

二つのプロセス間で情報をやりとるする必要がある場合、特定の通信プロトコルにしたがって情報を送受信する必要があります。インタラクティブなシステムにおいて、リアルタイム性が重視される場合、OSCを用いることが多くあります。OSCはネットワークの通信プロトコル上(UDPTCPなど)で動作し、異なるプロセス間で通信することができます。一台のPC内でのプロセス間通信はもちろんのこと、インターネットを介して通信することも可能です。

それでは、Unity上でOSCを用いて通信するプログラムについて説明していきます。現在、UnityのOSC用ライブラリはいくつか用意されています。ここでは、UnityOSC[4]と呼ばれるライブラリを用います。UnityOSCはgithubからダウンロードできます。ファイルを展開すると、「UnityOSC/src」というフォルダが現れるので、そのフォルダを今回使用するUnityのAssets以下に配置します。

f:id:koki0702:20141120144354p:plain

図5.UnityOSCをAssetsフォルダに配置する

続いて、OSCHandler.csファイルの初期化部分に以下を追記します。

public void Init()
{
    //OSCクライアントの初期化
    //CreateClient("KenKenClient”, IPAddress.Parse("127.0.0.1"), 5555);
        
    //OSCサーバの初期化
    CreateServer("KenKenServer”, 6666)
}

OSCを送信する場合はCreateClientという関数を、受信する場合はCreateServerを用います。CreateClientでは引数に「クライアントの名前、送信先のIPアドレス、送信先のポート番号」を取ります。CreateServerでは「サーバの名前、ポート番号」を引数にとります。ここでは、センサーからOSCで情報を受信するため、OSCサーバだけ生成することにします。ちなみに、上の例では6666番のポートで通信を受信するように設定しています。

続いてMyOSCManager.csという名前のファイルを作成し、OSCを受信するコードを書いていきます。初めにStart()関数内に、OSCHandlerを初期化するためのコードを記述します。そして、OSCを受信するコードはUpdate関数内に記述します。OSCHandler.Instance.UpdateLogs()を用いた後、_lastOscTimeStampで記録したOSCを受信したタイムスタンプより新しいデータがある場合、それをコンソールに出力します。

public class MyOscManager : MonoBehaviour {
    private long _lastOscTimeStamp = -1;
    
    void Start () {
        OSCHandler.Instance.Init();
    }
    
    void Update () {
        OSCHandler.Instance.UpdateLogs();
        
        foreach( KeyValuePair<string, ServerLog> item in OSCHandler.Instance.Servers ) {
            for( int i=0; i < item.Value.packets.Count; i++ ) {              if( _lastOscTimeStamp < item.Value.packets[i].TimeStamp ) {
                    _lastOscTimeStamp = item.Value.packets[i].TimeStamp;

                    string address = item.Value.packets[i].Address;
                    int userX = (int)item.Value.packets[i].Data[0];
                    int userY = (int)item.Value.packets[i].Data[1];

                    Debug.Log( address + ":(" + userX + ", " + userY + ")" );
                }                   }                   }       
    }
}

OSCデータの中身は、OSCアドレスとそれに続いてint型やstring型のデータが続きます。ここで想定するデータは、ユーザの足の位置を示すx,y座標がint型で送信されることを想定します。そのため、上記コードではデータ配列の一番目の要素をユーザのx座標値、二番目をy座標値として解釈しています。最後にMyOSCManager.csを空のGameObjectに追加すれば完了です。

それではOSC通信のテストを行うために、テスト用にOSCを送信するプログラムを作成します。ここでは、PythonのsimpleOSCライブラリ[5]を利用して、簡単なテストプログラムを作成します。simpleOSCを使用することで、OSCを送信することができます。

import simpleOSC

simpleOSC.initOSCClient(ip='127.0.0.1', port=6666)
simpleOSC.sendOSCMsg(“/user”, [354, 524])

上に示すPythonのコードでは、初めにIPアドレスとポート番号を指定します。今回はローカルホストを示す127.0.0.1と、先ほどUnity側のプログラムで設定したポート番号6666を指定します。その後で、「/user」というOSCアドレスにデータが[354、524]というint型の数字を送ります。上のテスト用コードを実行すれば、Unity側で受信できているか確認します。通信が成功すれば、図6のように受信した結果が表示されます。

f:id:koki0702:20141120144356p:plain

図6.OSCデータを受信した結果を出力する

以上でOSCの通信部分が完成しました。この受信データを使用して、Unity側で処理を書いていくことができます。たとえば、OSCで取得した位置にエフェクトを発生させたりすることができるでしょう。本アトラクションでは、ユーザ位置とボードの当たり判定を行い、それに応じたエフェクトを発生させています。また、川に波紋を発生させる処理をシェーダで記述したり、その近くにいる生き物が逃げていくようなアクションを起こすような処理も追加しています。