NtKinectを利用して OpenCV や Kinect V2 の基本的機能を使う DLL ファイルを作成し、 他の自作のプログラムや Unity から利用する方法は 「NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 で説明しました。
さて、Kinect V2 で顔認識、音声認識、ジェスチャー認識を行うプログラムは、 それぞれの機能に対応した 既存のDLL ファイルを実行時に必要とします。 このように「Kinect V2 の機能のうち既存のDLLファイルを必要とする機能を 自作のプログラムで利用した場合に、その自作プログラムを DLL 化して Unity をはじめとする 他のプログラムから利用する方法」について解説します。
本稿では、ジェスチャ認識を行う DLL ファイルを作成する方法について解説します。
では実際に既存の DLL ファイルを必要とする Kinect V2 の例として、 ジェスチャ認識を行う DLL ライブラリを作成してみましょう。
「NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 の Visual Studio 2017 のプロジェクト NtKinectDll.zip をダウンロードして展開して下さい。
次のようなファイル構造になっているはずです。 表示した以外のフォルダやファイルもあるかもしれませんが、今は無視します。
NtKinectDll4/NtKinectDll.sln x64/Release/ NtKinectDll/dllmain.cpp NtKiect.h NtKiectDll.h NtKiectDll.cpp stdafx.cpp stdafx.h targetver.h |
「構成プロパティ」 -> 「リンカー」 -> 全般 -> 入力
Kinect20.VisualGestureBuilder.lib
赤色の部分が今回追加する関数のプロトタイプ宣言です。
NtKinectDll.h |
|
ジェスチャ認識を使うので、NtKinect.h を include する前にUSE_GESTUREマクロをdefineします。 このマクロをdefineした場合は Kinect20.VisualGestureBuilder.lib ライブラリをリンクする必要があるので注意して下さい。
NtKinectDLL.cpp を記述する基本的な方針は以下の通りです。
文字列データを Unity と DLL の間で頻繁にやり取りすると実行効率が悪くなります。 そこで、最初に Unity 側で定義した「ジェスチャのID番号(正の整数)」を DLL 側に伝えておくことにします。 そして、DLLがジェスチャを認識した時はID番号でジェスチャの種類を通知します。
この方法だと、DLL 側で「ジェスチャ名からID番号への変換」が必要になりますが、 これにはC++14 の std::unordered_map<string,int> を用いることで効率良く 「string から int へのマップ」を実現できます。 ID番号が定義されていないジェスチャのID番号は 0 となります。
ヘッダファイルでの変数宣言 |
---|
std::unordered_map<std::string, int> gidMap; |
setGestureId 関数で string -> int の対応を登録するコード |
gidMap[s] = id; // <-- sはC++のstring。idは正の整数。 |
getDiscreteGestureId関数でstring -> int 変換を行うコード |
gid[i] = gidMap[gname] // <-- gnameに対応するintが返る。マップに登録されていない場合は0が返る。 |
DLL の関数では文字列は wchar_t* として受け取ります。 UTF8 に変換する必要がある場合は DLL 側で変換します。
UTF16 の wchar_t* 型 の name を UTF8の string 型の nameBuffer に変換するコード |
---|
int len = WideCharToMultiByte(CP_UTIF8, NULL, name, -1, NULL, 0, NULL, NULL) + 1; // <-- UTF16からUTF8に変換したときの文字の長さを調べる char* nameBuffer = new char[len]; // <-- UTF8に変換したデータ領域を確保する memset(nameBuffer, '\0', len); // <-- その領域にまず NULL 文字を詰めておいて WideCharToMultiByte(CP_UTIF8, NULL, name, -1, nameBuffer, 0, NULL, NULL) + 1; // <-- UTF16からUTF8に変換し nameBuffer 領域に書き込む |
認識したデータは最終的に Unity 側で利用したいので、データ領域はUnity側で用意します。 DLL 側では Unity 側に返すべきデータを配列領域の要素として書き込んでいくことになります。
認識されたジェスチャを返す関数では、認識したジェスチャの個数が返り値であると便利です。 しかし、認識されるdiscreteジェスチャの数とcontinousジェスチャの数は一般に異なるため、今回はそれぞれを別の関数で返すことにします。 ただし、ジェスチャの認識そのものは一つの関数呼び出しで同時に行います。
OpenCV が表示しているウィンドウは別スレッドで管理されているので、 Unityのアプリケーションが終了しても生き残り、NtKinectDLL.dll ファイルを使い続けることがあります。 これでは NtKinectDll.dll を取り替えようとしたときに Unity の再起動が必要となり面倒です。 そこで、OpenCV の全てのウィンドウを消去する関数を用意しておいて、Unity のアプリケーションの実行が終了するタイミングで呼び出すことにします。
骨格認識をした際の骨格の trackingId と、ジェスチャ認識の際の trackingId を比較することで, 認識されたジェスチャがどの骨格のものかがわかります。 が、今回は理解しやすさを優先してジェスチャの trackingId の情報は利用しないものとします。 そもそも骨格情報を Unity 側に送る例でないと、骨格の trackingId を利用する意味があまりありませんので。
NtKinectDll.cpp |
|
上の NtKinectDll.cpp のリスト表示において、文字の色分けは基本的に
緑色の文字: Visual StudioにおけるDLL作成に関係する部分です。
青色の文字: ジェスチャ認識のDLL化関数を定義する部分
赤色の文字: NtKinectやOpenCVを利用する部分
マゼンタ色の文字: C#とC++の間のデータ変換(マーシャリング)に関係する部分
[注意](2017/10/07 追記) Visual Studio 2017 Update 2 でのビルド時に「dllimport ...」というエラーが起きる場合は こちらを参考にして NtKinectDll.cpp 内で NTKINECTDLL_EXPORTS をdefineする ことで対処して下さい。
上記のzipファイルには必ずしも最新の NtKinect.h が含まれていない場合があるので、 こちらから最新版をダウンロードして 差し替えてお使い下さい。
NtKinectDll.dll を Unityで利用します。
[注意]$(KINECTSDK20_DIR)は手元の環境では "C:\Program Files\Microsoft SDKs\Kinect\v2.0_1409\" に設定されています。 各自の環境にしたがって読み替えて下さい。
今回は 「NtKinect: Kinect V2 でジェスチャを認識する」 で利用した $(KINECTSDK20_DIR)Tools\KinectStudio\databases\SampleDatabase.gbd を使うことにしましょう。 このファイルをUnityのプロジェクトフォルダ直下にコピーします。
CheckNtKinectDll4/SampleDatabase.gbd
上部のメニューから「Assets」-> 「Create」 -> 「C# Script」 -> ファイル名は NtKinectGesture
これで Assets/Scripts/NtKinectGesture.cs が生成されます。
NtKinectGesture.cs を記述する基本的な方針は以下の通りです。
C# の string をDLLの関数に引数として渡す場合は、 Marshall.StringToHGlobalUni()関数を用いてヒープメモリ上の unmanaged なUTF16 文字列に変換します。 この操作により、C++に渡した文字列データが C# のガベージコレクタによって場所が移動する危険性は無くなります。 unmanaged なデータは必要がなくなったら Marshal. FreeHGlobal()関数を呼び出してメモリを解放しなければいけません。
認識したデータは最終的に Unity 側で利用したいので、この例ではデータ領域はUnity側で配列として用意します。 C#の配列をDLL の関数に引数として渡すときは、データ領域がガベージコレクタによって動かないように GCHandle を用いてピン止めします。ピン止めした領域は、その必要がなくなった時点で解放する必要があることに注意しましょう。
GCHandle gch = GCHandle.Allocate(ヒープ上の配列, GCHandleType.Pinned) // 配列をガベージコレクタで移動しないようにする ... gch.AddrOfPinnedObject() // 移動しない領域の先頭のアドレスを返す ... gch.Free(); // 移動しない(unmanaged)領域を解放する。 |
setGesture()関数を呼び出した後で、getDiscreteGesture() と getContinuousGesture() を呼び出します。 必要ない方の取り出し関数は呼び出さなくてもかまいません。
この例では、認識されるdiscreteジェスチャは 3 種類, continuousジェスチャは 1 種類であり、 Kinectで同時認識されるのが6人です。 したがって、配列要素の数 gestureCount = max(3,1) * 6 = 18 に設定しています。
NtKinectGesture.cs |
|
上の NtKinectGesture.cs のリスト表示において、文字の色分けは基本的に
緑色の文字: UnityにおいてDLL関数を利用するための宣言部分です。
青色の文字: DLL化関数の呼び出しに関係する部分
マゼンタ色の文字: C#とC++の間のデータ変換(マーシャリング)に関係する部分
今回の例では Game 画面は変化しません。Consoleパネルに Debug.Log の出力として認識したジェスチャが表示されます。
SampleDatabase.gbd ファイルで定義されているジェスチャは次の4種類
ジェスチャの名前 | 種類 | 設定したID番号 |
---|---|---|
Steering_Left | discrete | 1 |
Steering_Right | discrete | 2 |
SteeringProgress | continuous | 3 |
SteeringStraight | discrete | 4 |
0 discrete 1 0.09684379 <-- Steering_Left が confidence 0.09684379 で認識されている 1 discrete 4 0.06129133 <-- SteeringStraight が confidence 0.06129133 で認識されている 0 continuous 3 0.4763844 <-- SteeringProgress が progress 0.4763844 で認識されている |
[注意] この例の場合ではなぜか、 continuous ジェスチャの SteeringProgress は一旦認識すると、 骨格が認識されている限りずっと認識されたままになるようです。 この状況がまともなのかどうか、および、原因は不明です。
[注意] 骨格や顔の認識状態を表示するためにDLL内でOpenCVのウィンドウを生成しています。 後から生成されたこのウィンドウにフォーカスがあるときは (= Unityのウィンドウにフォーカスがない場合は) UnityのGame画面は変化しないので注意して下さい。Console画面は変化します。 この例では Game 画面はフォーカスされていても変化しませんが。
要はジェスチャ定義ファイル中に含まれる可読文字を取り出せばよく、バイナリを扱えるテキストエディタで開いて調べることもできますが、 ここでは strings コマンドを使う例を説明します。
Unix環境(Linux, MacOS X, cygwin) では strings という、バイナリ中の可読文字列を取り出すコマンドがあります。 たとえばLinux上で SampleDatabase.gbd に対して strings を動作させると次のような出力になります。
strings コマンドの出力例 (抜粋) |
|
discrete ジェスチャは AdaBoost アルゴリズムで判定されて、 continuous ジェスチャは Random Forest アルゴリズムで判定されて いることを思い出せば、上記の gbd ファイルには、discrete ジェスチャとして
Steering_Left Steering_Right SteeringStraightの3種類が、continuous ジェスチャとして
SteeringProgressの1種類が含まれていることがわかります。
[注意] cygwin 環境ではインストールの状態によって strings コマンドがインストールされていない場合があります。 追加でインストールする方法は こちら 。