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

Kinect V2 の顔認識をDLL化してUnityから利用する


2016.08.25: 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. NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 の Visual Studio 2017 のプロジェクト NtKinectDll.zipを用いて作成します。
  2. プロジェクトのプロパティからインクルードファイルやライブラリに関する設定を行います。
    1. ソリューションエクスプローラでプロジェクト名の上で右ドラッグして「プロパティ」を選択します。



    2. 構成:「すべての構成」, プラットフォーム 「アクティブ(x64)」の状態で設定を行います。こうすることによってDebugおよびReleaseの設定を同時に行うことができます。もちろん、別々に設定しても構いませんが。
    3. リンクするライブラリを追加します。
    4. 「構成プロパティ」 -> 「リンカー」 -> 全般 -> 入力

        Kinect20.Face.lib



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

    NtKinectDll.h
    #ifdef NTKINECTDLL_EXPORTS
    #define NTKINECTDLL_API __declspec(dllexport)
    #else
    #define NTKINECTDLL_API __declspec(dllimport)
    #endif 
    
    extern "C" {
      NTKINECTDLL_API void* getKinect(void);
      NTKINECTDLL_API int faceDirection(void* ptr, float* dir);
    }
    
  5. 関数は"プロジェクト名.cpp"に記述します。この例だと NtKinectDll.cpp になります。USE_FACEマクロのdefineと、faceDirection()関数の定義を追加します。
  6. 顔認識を使うので、NtKinect.h を include する前にUSE_FACEマクロをdefineします。 このマクロをdefineした場合は Kinect20.Face.lib ライブラリをリンクする必要があるので注意して下さい。

    int faceDirection(void *, float *data) 関数の定義を追加します。 この関数ではまず、 カメラで取得した画像を1/16に縮小してその上に関節を赤で描画して、 さらに認識できた顔領域に矩形を描画してからcv::imshow()で表示しています。 ウィンドウの内容を正しく表示させるためにはcv::waitKey(1)を呼び出す必要があります。 顔の向きは pitch, yaw, roll の3通りのfloat値で、それが最大で6人分なので、 data 領域はこの関数を呼び出す側で 「floatのバイト数 * 3 * 6 」バイト以上確保されている必要があります。 返り値は、向きを認識できた顔の個数です。

    NtKinectDll.cpp
    #include "stdafx.h"
    #include "NtKinectDll.h"
    
    #define USE_FACE
    #include "NtKinect.h"
    
    using namespace std;
    
    NTKINECTDLL_API void* getKinect(void) {
      NtKinect* kinect = new NtKinect;
      return static_cast<void*>(kinect);
    }
    
    NTKINECTDLL_API int faceDirection(void* ptr,float *dir) {
      NtKinect *kinect = static_cast<NtKinect*>(ptr);
      (*kinect).setRGB();
      (*kinect).setSkeleton();
      (*kinect).setFace();
      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);
        }
      }
      for (auto r: (*kinect).faceRect) {
        cv::Rect r2(r.x/scale,r.y/scale,r.width/scale,r.height/scale);
        cv::rectangle(img, r2, cv::Scalar(255, 255, 0), 2);
      }
      cv::imshow("face",img);
      cv::waitKey(1);
      int idx=0;
      for (auto d: (*kinect).faceDirection) {
        dir[idx++] = d[0];
        dir[idx++] = d[1];
        dir[idx++] = d[2];
      }
      return (*kinect).faceDirection.size();
    }
    
  7. 「アプリケーションの構成」を "Release"にしてビルドします。フォルダ x64/Release に NtKinectDll.lib および NtKinectDll.dll が生成されます。
  8. [注意](2017/10/07 追記) Visual Studio 2017 Update 2 でのビルド時に「dllimport ...」というエラーが起きる場合は こちらを参考にして NtKinectDll.cpp 内で NTKINECTDLL_EXPORTS をdefineする ことで対処して下さい。

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


生成したDLLファイルの動作確認

生成したDLLファイルが正しく動作することを確認する簡単なプロジェクトを作成しましょう。

  1. NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 の Visual Studio 2017 のプロジェクト CheckDLL.zipを用いて作成します。
  2. プロジェクトのファイルの NtKinectDll.h を、新しいものに差し替えます。
  3. プロジェクトの stdafx.cpp や CheckDLL.cpp が置かれているフォルダに古いNtKinectDll.hが あるはずですが、その上に新しい NtKinectDll.h をコピーします。

  4. プロジェクトのフォルダにDLLのファルを置きます。
  5. プロジェクトの stdafx.cpp や CheckDLL.cpp が置かれているフォルダに 古いNtKinectDll.dll と NtKinectDll.lib があるはずですが、その上に新しい NtKinectDll.dll と NtKinectDll.lib をコピーします。

  6. プロジェクトのフォルダに 顔認識関係のファイルをコピーします。
  7. プロジェクトの stdafx.cpp や CheckDLL.cpp が置かれているフォルダに、 $(KINECTSDK20_DIR)Redist\Face\x64\ の下のファイルを全てコピーします。 標準の設定だと $(KINECTSDK20_DIR)は C:\Program Files\Microsoft SDKs\Kinect\v2.0_1409\ になっているはずです(「v2.0_バージョン番号」の部分はお使いのSDK のバージョンにより異なります)。

    $(KINECTSDK20_DIR)Redist\Face\x64\NuiDatabase\
                                     \Kinect20.Face.lib
                                     \Microsoft.Kinect.Face.lib
                                     \Microsoft.Kinect.Face.xml
    



  8. CheckDLL.cppの内容を以下のように変更します。
  9. int faceDirection(void* ptr,float* data) の返り値は、向きが認識できた顔の個数です。 一人の顔の向きは pitch, yaw, row の3個のfloatで表されます。 最大で6人を同時認識することがあるので 6*3 個のfloatのための領域を配列で確保しています。

    CheckDLL.cpp
    #include "stdafx.h"
    #include <iostream>
    #include <sstream>
    
    #include "NtKinectDll.h"
    
    using namespace std;
    
    int main() {
      void* kinect = getKinect();
      float dir[6*3];
      while (1) {
        int ret = faceDirection(kinect, dir);
        if (ret) {
          for (int i=0; i<ret; i++) {
    	cout << i << " pitch: " << dir[i*3+0] << "  "
    	     << "yaw: " << dir[i*3+1] << "  "
    	     << "roll: " << dir[i*3+2] << endl;
          }
        } else {
          cout << "unknown" << endl;
        }
      }
      return 0;
    }
    
  10. プログラムを実行するとコンソール画面に、認識された顔の方向が 人数分表示されていきます。 顔認識が可能なのは骨格が認識できた場合に限ります。 DLLファイルの rightHandState() 関数の中では、 RGBカメラで取得した画像の上に関節位置を赤で表し、顔を水色の四角形で表した た画像をウィンドウ表示するので現在骨格および顔が認識されているかどうかがわかります。



  11. サンプルのプロジェクトはこちら CheckDLL2.zip

Unity内でDLLを利用する

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

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



  5. Unity のプロジェクトのAssets/Plugins/x86_64/の下に NtKinectDll.dll をコピーします。 また $(KINECTSDK20_DIR)Redist\Face\x64\の下のファイルも全てコピーします。



  6. Cubeをシーンに6個配置してそれぞれの名前をCube0からCube5とし、それぞれ位置を変更します。 カメラの位置はデフォルトのままで構いません。
  7. 上部のメニューから「Game Object」-> 「3D Object」 -> 「Cube」

    6個のCubeの位置を変更します。カメラはCubeを前から見るような位置にします(デフォルトのままでOKです)。

    name Position Rotation Scale
    x y z x y z x y z
    Cube0 -5 0 0 0 0 0 1 1 1
    Cube1 -3 0 0 0 0 0 1 1 1
    Cube2 -1 0 0 0 0 0 1 1 1
    Cube3 1 0 0 0 0 0 1 1 1
    Cube4 3 0 0 0 0 0 1 1 1
    Cube5 5 0 0 0 0 0 1 1 1
    Main Camera 0 1 -10 0 0 0 1 1 1
  8. Empty Objectをシーンに配置して、名前を NtKinect に変更します。



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




    C++ のポインタは C# では System.IntPtr として扱います。

    NtKinectBehaviour.cs
    using UnityEngine;
    using System.Collections;
    using System.Runtime.InteropServices;
    
    public class NtKinectBehaviour : MonoBehaviour {
        [DllImport ("NtKinectDll")] private static extern System.IntPtr getKinect();
        [DllImport ("NtKinectDll")] private static extern int rightHandState(System.IntPtr kinect);
        [DllImport ("NtKinectDll")] private static extern int faceDirection(System.IntPtr kinect, System.IntPtr data);
    
        private System.IntPtr kinect;
        int bodyCount = 6;
        GameObject[] obj;
        int counter;
        
        void Start () {
            kinect = getKinect();
    	obj = new GameObject[bodyCount];
    	obj[0] = GameObject.Find("Cube0");
    	obj[1] = GameObject.Find("Cube1");
    	obj[2] = GameObject.Find("Cube2");
    	obj[3] = GameObject.Find("Cube3");
    	obj[4] = GameObject.Find("Cube4");
    	obj[5] = GameObject.Find("Cube5");
        }
        public static Quaternion ToQ (float pitch, float yaw, float roll) {
    	yaw *= Mathf.Deg2Rad;
    	pitch *= Mathf.Deg2Rad;
    	roll *= Mathf.Deg2Rad;
    	float rollOver2 = roll * 0.5f;
    	float sinRollOver2 = (float)System.Math.Sin ((double)rollOver2);
    	float cosRollOver2 = (float)System.Math.Cos ((double)rollOver2);
    	float pitchOver2 = pitch * 0.5f;
    	float sinPitchOver2 = (float)System.Math.Sin ((double)pitchOver2);
    	float cosPitchOver2 = (float)System.Math.Cos ((double)pitchOver2);
    	float yawOver2 = yaw * 0.5f;
    	float sinYawOver2 = (float)System.Math.Sin ((double)yawOver2);
    	float cosYawOver2 = (float)System.Math.Cos ((double)yawOver2);
    	Quaternion result;
    	result.w = cosYawOver2 * cosPitchOver2 * cosRollOver2 + sinYawOver2 * sinPitchOver2 * sinRollOver2;
    	result.x = cosYawOver2 * sinPitchOver2 * cosRollOver2 + sinYawOver2 * cosPitchOver2 * sinRollOver2;
    	result.y = sinYawOver2 * cosPitchOver2 * cosRollOver2 - cosYawOver2 * sinPitchOver2 * sinRollOver2;
    	result.z = cosYawOver2 * cosPitchOver2 * sinRollOver2 - sinYawOver2 * sinPitchOver2 * cosRollOver2;
    	return result;
        }
    
        void Update () {
    	float[] data = new float[bodyCount * 3];
    	GCHandle gch = GCHandle.Alloc(data,GCHandleType.Pinned);
    	int n = faceDirection(kinect,gch.AddrOfPinnedObject());
    	gch.Free();
    	counter = (n != 0) ? 10 : System.Math.Max(0,counter-1);
    	for (int i=0; i<bodyCount; i++) {
    	    if (i<n) {
    		obj[i].transform.rotation = ToQ(data[i*3+0],data[i*3+1],data[i*3+2]);
    		obj[i].GetComponent<Renderer>().material.color = new Color(1.0f,0.0f,0.0f,1.0f);
    	    } else if (counter == 0) {
    		obj[i].transform.rotation = Quaternion.identity;
    		obj[i].GetComponent<Renderer>().material.color = new Color(1.0f,1.0f,1.0f,1.0f);
    	    }
    	}
        }
    }
    
  11. NtKinectBehaviour.cs を NtKinect のComponentとして付加します。



  12. 実行すると、認識した顔の個数だけ立方体が赤くなって顔の向きに合わせて回転します。
  13. 顔認識をしていても瞬間的に認識できなくなると立方体が激しく動いて見辛いので、 10回連続で認識に失敗するときだけ追跡失敗とする簡便なエラー処理を 行っています(変数counter)。 本来は faceTrackingId を受け取って顔毎に判断すべきですが、 説明のコードが複雑になるのを避けるために簡単な方法に留めています。

    [注意] 骨格や顔の認識状態を表示するためにDLL内でOpenCVのウィンドウを生成しています。 後から生成されたこのウィンドウにフォーカスがあるときは (= Unityのウィンドウにフォーカスがない場合は) Unityの画面は変化しないので注意して下さい。 Unityのウィンドウの上部をクリックしてUnityのウィンドウにフォーカスが ある状態で動作を試して下さい。




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


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