NtKinect: Kinect V2 C++ Programming with OpenCV on Windows10

Kinect V2 のジェスチャ認識をDLL化してUnityから利用する


2016.12.13: created by
2017.10.07: revised by
Japanese English
目次へ

前提として理解しておくべき知識


ジェスチャ認識プログラムのDLL化

NtKinectを利用して OpenCV や Kinect V2 の基本的機能を使う DLL ファイルを作成し、 他の自作のプログラムや Unity から利用する方法は 「NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 で説明しました。

さて、Kinect V2 で顔認識、音声認識、ジェスチャー認識を行うプログラムは、 それぞれの機能に対応した 既存のDLL ファイルを実行時に必要とします。 このように「Kinect V2 の機能のうち既存のDLLファイルを必要とする機能を 自作のプログラムで利用した場合に、その自作プログラムを DLL 化して Unity をはじめとする 他のプログラムから利用する方法」について解説します。

本稿では、ジェスチャ認識を行う DLL ファイルを作成する方法について解説します。


プログラム作成の手順

では実際に既存の DLL ファイルを必要とする Kinect V2 の例として、 ジェスチャ認識を行う DLL ライブラリを作成してみましょう。

  1. DLLを作成するプロジェクトファイルをダウンロードして展開します。
  2. NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 の Visual Studio 2017 のプロジェクト NtKinectDll.zip をダウンロードして展開して下さい。

  3. 以下、プロジェクトファイルのフォルダ名は NtKinectDll4 に変更したものとして説明します。
  4. 次のようなファイル構造になっているはずです。 表示した以外のフォルダやファイルもあるかもしれませんが、今は無視します。

    NtKinectDll4/NtKinectDll.sln
                 x64/Release/
                 NtKinectDll/dllmain.cpp
                             NtKiect.h
                             NtKiectDll.h
                             NtKiectDll.cpp
                             stdafx.cpp
                             stdafx.h
                             targetver.h
    
  5. NtKinect.h を最新版に置き換えて下さい。
  6. プロジェクトのプロパティからインクルードファイルやライブラリに関する設定を行います。
    1. ソリューションエクスプローラでプロジェクト名の上で右ドラッグして「プロパティ」を選択します。



    2. 構成:「すべての構成」, プラットフォーム 「アクティブ(x64)」の状態で設定を行います。こうすることによってDebugおよびReleaseの設定を同時に行うことができます。もちろん、別々に設定しても構いませんが。



    3. リンクするライブラリを追加します。
    4. 「構成プロパティ」 -> 「リンカー」 -> 全般 -> 入力

        Kinect20.VisualGestureBuilder.lib



  7. 宣言をヘッダファイルに記述します。ヘッダファイルの名前は"プロジェクト名.h"で、この場合は "NtKinectDll.h" になります。
  8. 赤色の部分が今回追加する関数のプロトタイプ宣言です。

    NtKinectDll.h
    #ifdef NTKINECTDLL_EXPORTS
    #define NTKINECTDLL_API __declspec(dllexport)
    #else
    #define NTKINECTDLL_API __declspec(dllimport)
    #endif
    
    namespace NtKinectGesture {
      extern "C" {
        NTKINECTDLL_API void* getKinect(void);
        NTKINECTDLL_API void setGestureFile(void* ptr, wchar_t* filename);
        NTKINECTDLL_API int setGestureId(void* ptr, wchar_t* name, int id); // id: non-zero
        NTKINECTDLL_API void setGesture(void* ptr);
        NTKINECTDLL_API int getDiscreteGesture(void* ptr, int *gid, float *confidence);
        NTKINECTDLL_API int getContinuousGesture(void* ptr, int *gid, float *progress);
        NTKINECTDLL_API void stopKinect(void* ptr);
      }
      std::unordered_map<std::string, int> gidMap;
    }
    
    
  9. 関数は"プロジェクト名.cpp"に記述します。この例だと NtKinectDll.cpp になります。USE_GESTUREマクロをdefineし、追加する関数の定義を記述します。
  10. ジェスチャ認識を使うので、NtKinect.h を include する前にUSE_GESTUREマクロをdefineします。 このマクロをdefineした場合は Kinect20.VisualGestureBuilder.lib ライブラリをリンクする必要があるので注意して下さい。

    NtKinectDLL.cpp を記述する基本的な方針は以下の通りです。

    NtKinectDll.cpp
    #include "stdafx.h"
    #include <unordered_map>
    #include "NtKinectDll.h"
    
    #define USE_GESTURE
    #include "NtKinect.h"
    
    using namespace std;
    
    namespace NtKinectGesture {
      NTKINECTDLL_API void* getKinect(void) {
        NtKinect* kinect = new NtKinect;
        return static_cast<void*>(kinect);
      }
    
      NTKINECTDLL_API void setGestureFile(void* ptr, wchar_t* filename) {
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        wstring fname(filename);
        (*kinect).setGestureFile(fname);
      }
    
      NTKINECTDLL_API int setGestureId(void* ptr, wchar_t* name, int id) {
        int len = WideCharToMultiByte(CP_UTF8,NULL,name,-1,NULL,0,NULL,NULL) + 1;
        char* nameBuffer = new char[len];
        memset(nameBuffer,'\0',len);
        WideCharToMultiByte(CP_UTF8,NULL,name,-1,nameBuffer,len,NULL,NULL);
        string s(nameBuffer);
        gidMap[s] = id;
    
        return id;
      }
    
      NTKINECTDLL_API void setGesture(void* ptr) {
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        (*kinect).setRGB();
        (*kinect).setSkeleton();
        int scale = 4;
        cv::Mat img((*kinect).rgbImage);
        cv::resize(img,img,cv::Size(img.cols/scale,img.rows/scale),0,0);
        for (auto person: (*kinect).skeleton) {
          for (auto joint: person) {
    	if (joint.TrackingState == TrackingState_NotTracked) continue;
    	ColorSpacePoint cp;
    	(*kinect).coordinateMapper->MapCameraPointToColorSpace(joint.Position,&cp);
    	cv::rectangle(img, cv::Rect((int)cp.X/scale-2, (int)cp.Y/scale-2,4,4), cv::Scalar(0,0,255),2);
          }
        }
        cv::imshow("rgb",img);
        cv::waitKey(1);
        (*kinect).setGesture();
      }
    
      NTKINECTDLL_API int getDiscreteGesture(void* ptr, int *gid, float *confidence) {
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        for (int i=0; i<(*kinect).discreteGesture.size(); i++) {
          auto g = (*kinect).discreteGesture[i];
          string gname = (*kinect).gesture2string(g.first);
          gid[i] = gidMap[gname];
          confidence[i] = g.second;
        }
        return (*kinect).discreteGesture.size();
      }
    
      NTKINECTDLL_API int getContinuousGesture(void* ptr, int *gid, float *progress){
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        for (int i=0; i<(*kinect).continuousGesture.size(); i++) {
          auto g = (*kinect).continuousGesture[i];
          string gname = (*kinect).gesture2string(g.first);
          gid[i] = gidMap[gname];
          progress[i] = g.second;
        }
        return (*kinect).continuousGesture.size();
      }
    
      NTKINECTDLL_API void stopKinect(void* ptr) {
        cv::destroyAllWindows();
      }
    }

    上の NtKinectDll.cpp のリスト表示において、文字の色分けは基本的に

    緑色の文字: Visual StudioにおけるDLL作成に関係する部分
    青色の文字: ジェスチャ認識のDLL化関数を定義する部分
    赤色の文字: NtKinectやOpenCVを利用する部分
    マゼンタ色の文字: C#とC++の間のデータ変換(マーシャリング)に関係する部分
    です。

  11. 「アプリケーションの構成」を "Release"にしてコンパイルします。フォルダ x64/Release に NtKinectDll.lib および NtKinectDll.dll が生成されます。
  12. [注意](2017/10/07 追記) Visual Studio 2017 Update 2 でのビルド時に「dllimport ...」というエラーが起きる場合は こちらを参考にして NtKinectDll.cpp 内で NTKINECTDLL_EXPORTS をdefineする ことで対処して下さい。

  13. サンプルのプロジェクトはこちら NtKinectDll4.zip
  14. 上記のzipファイルには必ずしも最新の NtKinect.h が含まれていない場合があるので、 こちらから最新版をダウンロードして 差し替えてお使い下さい。


Unity内でDLLを利用する

NtKinectDll.dll を Unityで利用します。

  1. Unity で DLL を利用する方法の詳細については 公式のマニュアル を参照して下さい。
  2. Unity(C#) のデータは managed (ガベージコレクタによって管理されており場所を移動することがある) な状態であり、DLL(C++) のデータは unmanaged (場所が移動することはない) な状態です。 この間でデータを受け渡すには、状態を変換する必要があり、 それにはC# の System.Runtime.InteropServices.Marshal クラス のメソッドを利用します。
  3. Unity の新しいプロジェクトを開始します。



  4. Unity のプロジェクトのAssets/Plugins/x86_64/の下に, 「NtKinectDll.dll」 および 「$(KINECTSDK20_DIR)Redist\VGB\x64\の下の全てのファイル」をコピーします。
  5. [注意]$(KINECTSDK20_DIR)は手元の環境では "C:\Program Files\Microsoft SDKs\Kinect\v2.0_1409\" に設定されています。 各自の環境にしたがって読み替えて下さい。




  6. ジェスチャ定義ファイルである .gbdファイルや .gbaファイルはUnityプロジェクトのフォルダ直下に配置します。
  7. 今回は 「NtKinect: Kinect V2 でジェスチャを認識する」 で利用した $(KINECTSDK20_DIR)Tools\KinectStudio\databases\SampleDatabase.gbd を使うことにしましょう。 このファイルをUnityのプロジェクトフォルダ直下にコピーします。

        CheckNtKinectDll4/SampleDatabase.gbd
    



  8. Empty Objectをシーンに配置して、名前を NtKinectGesture に変更します。



  9. ProjectのAssets/Scripts/の下に C# のスクリプトを生成します。
  10. 上部のメニューから「Assets」-> 「Create」 -> 「C# Script」 -> ファイル名は NtKinectGesture

    これで Assets/Scripts/NtKinectGesture.cs が生成されます。




    NtKinectGesture.cs を記述する基本的な方針は以下の通りです。

    NtKinectGesture.cs
    using UnityEngine;
    using System.Collections;
    using System.Runtime.InteropServices;
    using System;
    
    public class NtKinectGesture : MonoBehaviour {
      [DllImport ("NtKinectDll")] private static extern System.IntPtr getKinect();
      [DllImport ("NtKinectDll")] private static extern int rightHandState(System.IntPtr kinect);
      [DllImport ("NtKinectDll")] private static extern void setGestureFile(System.IntPtr ptr, System.IntPtr filename);
      [DllImport ("NtKinectDll")] private static extern int setGestureId(System.IntPtr ptr, System.IntPtr name, int id);
      [DllImport ("NtKinectDll")] private static extern void setGesture(System.IntPtr kinect);
      [DllImport ("NtKinectDll")] private static extern int getDiscreteGesture(System.IntPtr kinect, System.IntPtr gid, System.IntPtr cnf);
      [DllImport ("NtKinectDll")] private static extern int getContinuousGesture(System.IntPtr kinect, System.IntPtr gid, System.IntPtr cnf);
      [DllImport ("NtKinectDll")] private static extern void stopKinect(System.IntPtr kinect);
    
      private System.IntPtr kinect;
      int gestureCount = 18;
     
      void Start () {
        kinect = getKinect();
        System.IntPtr gbd = Marshal.StringToHGlobalUni("SampleDatabase.gbd"); // gbd file
        System.IntPtr g1 = Marshal.StringToHGlobalUni("Steer_Left"); // discrete
        System.IntPtr g2 = Marshal.StringToHGlobalUni("Steer_Right"); // discrete
        System.IntPtr g3 = Marshal.StringToHGlobalUni("SteerProgress"); // continuous
        System.IntPtr g4 = Marshal.StringToHGlobalUni("SteerStraight"); // discrete
        setGestureFile(kinect,gbd);
        setGestureId(kinect,g1,1);
        setGestureId(kinect,g2,2);
        setGestureId(kinect,g3,3);
        setGestureId(kinect,g4,4);
        Marshal.FreeHGlobal(gbd);
        Marshal.FreeHGlobal(g1);
        Marshal.FreeHGlobal(g2);
        Marshal.FreeHGlobal(g3);
        Marshal.FreeHGlobal(g4);
      }
      void Update () {
        setGesture(kinect);
        int[] gid = new int[gestureCount];
        float[] cnf = new float[gestureCount];
        GCHandle gch = GCHandle.Alloc(gid,GCHandleType.Pinned);
        GCHandle gch2 = GCHandle.Alloc(cnf,GCHandleType.Pinned);
        // discrete gesture
        int n = getDiscreteGesture(kinect, gch.AddrOfPinnedObject(), gch2.AddrOfPinnedObject());
        for (int i=0; i<n; i++) {
          Debug.Log(i + " discrete " + gid[i] + " " + cnf[i]); // "cnf" value is "confidence"
        }
        // continous gesture
        n = getContinuousGesture(kinect, gch.AddrOfPinnedObject(), gch2.AddrOfPinnedObject());
        for (int i=0; i<n; i++) {
          Debug.Log(i + " continous " + gid[i] + " " + cnf[i]); // "cnf" value is "progress"
        }
        gch.Free();
        gch2.Free();
      }
      void OnApplicationQuit() {
        stopKinect(kinect);
      }
    }
    

    上の NtKinectGesture.cs のリスト表示において、文字の色分けは基本的に

    緑色の文字: UnityにおいてDLL関数を利用するための宣言部分
    青色の文字: DLL化関数の呼び出しに関係する部分
    マゼンタ色の文字: C#とC++の間のデータ変換(マーシャリング)に関係する部分
    です。

  11. Projectパネル中のNtKinectGesture.cs をHierarchyパネル中の NtKinectGesture にドラッグして、Componentとして付加します。



  12. 実行すると、認識したジェスチャをコンソールに表示します。
  13. 今回の例では Game 画面は変化しません。Consoleパネルに Debug.Log の出力として認識したジェスチャが表示されます。




    SampleDatabase.gbd ファイルで定義されているジェスチャは次の4種類

    ジェスチャの名前種類設定したID番号
    Steering_Leftdiscrete1
    Steering_Rightdiscrete2
    SteeringProgresscontinuous3
    SteeringStraightdiscrete4
    ですので、上記のコンソールの図に示されている先頭3行は
    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 画面はフォーカスされていても変化しませんが。

  14. Unity のサンプルプロジェクトはこちら CheckNtKinectDll4.zip

ジェスチャ定義ファイルに含まれるジェスチャ名を調べる方法

要はジェスチャ定義ファイル中に含まれる可読文字を取り出せばよく、バイナリを扱えるテキストエディタで開いて調べることもできますが、 ここでは strings コマンドを使う例を説明します。

Unix環境(Linux, MacOS X, cygwin) では strings という、バイナリ中の可読文字列を取り出すコマンドがあります。 たとえばLinux上で SampleDatabase.gbd に対して strings を動作させると次のような出力になります。

strings コマンドの出力例 (抜粋)
$ strings SampleDatabase.gbd
VGBD
Steer_Left
Steer_Left
AdaBoostTrigger
Steer_Right
Steer_Right
AdaBoostTrigger
SteerProgress
SteerProgress
RFRProgress
SteerStraight
SteerStraight
AdaBoostTrigger
Accuracy Level
33s?"
Number Weak Classifiers at Runtime
Filter Results
...(略)...

discrete ジェスチャは AdaBoost アルゴリズムで判定されて、 continuous ジェスチャは Random Forest アルゴリズムで判定されて いることを思い出せば、上記の gbd ファイルには、discrete ジェスチャとして

  Steering_Left
  Steering_Right
  SteeringStraight
の3種類が、continuous ジェスチャとして
  SteeringProgress
の1種類が含まれていることがわかります。

[注意] cygwin 環境ではインストールの状態によって strings コマンドがインストールされていない場合があります。 追加でインストールする方法は こちら



http://nw.tsuda.ac.jp/