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

Kinect V2 を用いた音声認識をDLL化してUnityから利用する


2016.09.12: created by
2016.09.30: revised by
2016.10.18: revised by
2017.10.07: revised by
Japanese English
目次へ

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


音声認識プログラムのDLL化

Kinect V2 で音声認識を行う方法自体は 「NtKinect: Kinect V2 で音声を認識する」で、 音声認識をマルチスレッドで行う方法は 「NtKinect: Kinect V2 をマルチスレッド環境で動作させる」 で説明しました。

また、DLL ファイルを作成してKinect V2 を他のプログラム(Unity など)から利用する方法は、 「NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 で説明しました。

今回は、Kinect V2 で音声認識を行うDLLファイルを作成してUnityから利用する方法を説明します。


プログラム作成の手順

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

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

    NtKinectDll3/NtKinectDll.sln
                 x64/Release/
                 NtKinectDll/dllmain.cpp
                             NtKiect.h
                             NtKiectDll.h
                             NtKiectDll.cpp
                             stdafx.cpp
                             stdafx.h
                             targetver.h
    
  5. NtKinect.h を最新版にし、さらに音声認識に必要なファイルを配置します。
  6. まず、NtKinect.h を最新版に置き換えて下さい。

    また、音声認識に必要なファイル KinectAudioStream.cpp , KinectAudioStream.h , WaveFile.h を追加します。次のようになるはずです。 (注意: 今回は stdafx.h が存在するので KinectAudioStream.cpp が stdafx.hを読むように変更されています)。

    NtKinectDll3/NtKinectDll.sln
                 x64/Release/
                 NtKinectDll/dllmain.cpp
                             NtKiect.h
                             NtKiectDll.h
                             NtKiectDll.cpp
                             stdafx.cpp
                             stdafx.h
                             targetver.h
                             KinectAudioStream.cpp
                             KinectAudioStream.h
                             WaveFile.h
    



  7. NtKinectDll.slnをクリックして、プロジェクトを Visual Studio 2017 で開きます。
  8. さきほどコピーしたファイルをプロジェクトに追加します。
  9. これでプロジェクトに3つのファイルが追加されました。NtKinect.hは元からプロジェクトに追加されています。


  10. Visual Studio のウィンドウの上部メニューは "x64" になっているはずです。 今回は速度が必要なのでビルドオプションは "Release" を選択します。



  11. プロジェクトのプロパティからインクルードファイルやライブラリに関する設定を行います。 ビルドが Release か Debug かでオプションが変わるので、きちんと設定されていることを確認して下さい。
    1. ソリューションエクスプローラでプロジェクト名の上で右ドラッグして「プロパティ」を選択します。



    2. 構成:「すべての構成」, プラットフォーム 「アクティブ(x64)」の状態で設定を行います。こうすることによってDebugおよびReleaseの設定が同時にできます。もちろん、別々に設定しても構いませんが。
    3. インクルードファイルの場所を追加します。同じ名前のヘッダファイルが別の場所にあっても必ず正しいファイルが読み込まれるように設定します。
    4. 「構成プロパティ」 -> 「VC++ディレクトリ」 -> インクルードディレクトリ ->

        先頭に $(ProgramW6432)\Microsoft SDKs\Speech\V11.0\Include を追加する



    5. ライブラリの場所を追加します。
    6. 「構成プロパティ」 -> 「VC++ディレクトリ」 -> ライブラリディレクトリ ->

        先頭に $(ProgramW6432)\Microsoft SDKs\Speech\V11.0\Lib を追加する



    7. リンクするライブラリを追加します。(sapi.lib はここで追加しなくても動作するようですが、念のため追加しておきましょう。)
    8. 「構成プロパティ」 -> 「リンカー」 -> 全般 -> 入力

          sapi.libを追加する

      以下の2つのライブラリは既に設定されているはずですが、確認しておきます。

        Kinect20.lib
        opencv_world310.lib



  12. 宣言をヘッダファイルに記述します。ヘッダファイルの名前は"プロジェクト名.h"で、この場合は "NtKinectDll.h" になります。
  13. 緑文字の部分がプロジェクトを作成した時から定義されているDLLのimport/exportに関する部分です。 NtKinectDll.hはこのプロジェクト内ではexport用の宣言となり、他のプロジェクトに読み込まれたときは import用の宣言となります。

    青文字の部分が自分で定義した関数に関する部分です。 C++で関数名が mangling (= 関数名がC++コンパイラによって返り値の型と引数の型を含めた名前に変更されること) されるのを避けるために extern "C" {} の中で関数のプロトタイプを宣言します。 これによりこのDLLを他の言語から利用することが可能になります。

    名前の衝突を避けるために NtKinectSpeech という名前空間を定義して、 その中で関数や変数宣言を行っています。

    音声認識は別スレッドで常時行いその結果を speechQueue リストに貯めていきます。 キューにアクセスする場合はスレッドの排他制御が必要になるので mutex を用いています。

    NtKinectDll.h
    #ifdef NTKINECTDLL_EXPORTS
    #define NTKINECTDLL_API __declspec(dllexport)
    #else
    #define NTKINECTDLL_API __declspec(dllimport)
    #endif
    
    #include <mutex>
    #include <list>
    #include <thread>
    
    namespace NtKinectSpeech {
      extern "C" {
        NTKINECTDLL_API void* getKinect(void);
        NTKINECTDLL_API void initSpeech(void* kinect);
        NTKINECTDLL_API void setSpeechLang(void* kinect,wchar_t*,wchar_t*);
        NTKINECTDLL_API int speechQueueSize(void* kinect);
        NTKINECTDLL_API int getSpeech(void* kinect,wchar_t*& tagPtr,wchar_t*& itemPtr);
        NTKINECTDLL_API void destroySpeech(void* kinect);
      }
      std::mutex mutex;
      std::thread* speechThread;
      std::list<std::pair<std::wstring,std::wstring>> speechQueue;
      bool speechActive;
    #define SPEECH_MAX_LENGTH	1024
      wchar_t tagBuffer[SPEECH_MAX_LENGTH];
      wchar_t itemBuffer[SPEECH_MAX_LENGTH];
    }
    
  14. 関数は"プロジェクト名.cpp"に記述します。この例だと NtKinectDll.cpp になります。
  15. 関数宣言の先頭に "NTKINECTDLL_API" を記述する必要があります。 これは NtKinectDll.h の中で定義されているマクロで、DLLからのexport/importを容易にします。

    DLLの中ではオブジェクトはヒープに確保する必要があります。 そのため void* getKinect()関数では NtKinectを new してそのポインタを(void *)にキャストして返しています。

    DLLの関数を実行するときは、ヒープ上のNtKinectオブジェクトのポインタを渡してもらい、 (void *)型のポインタから (NtKinect *)型のポインタに変更してNtKinect の機能を利用します。 下の例では関数中では kinect 変数は (NtKinect *)型のポインタになるので、 たとえば acquire というメンバ関数の呼出しは (*kinect).acquire() と記述します。

    Unity の C# の側ではデータは managed でありメモリ中を勝手に移動することがあるので、 C++とのデータのやりとりには注意が必要です。

    C++ で NtKinect の setSpeechLang()関数に渡す文字列は、第1引数は string 型、第2引数は wstring 型であることに注意が必要です。 Unity(C#) から渡される string データは WideCharacter (UTF16 で表現された文字列データ)なので、 C++ の wstring (UTF16で表現された文字列データ)としてそのまま使えますが、 C++ の string (UTF8で表現された文字列データ)に変換するためには WideCharToMultiByte() 関数を使う必要があります。 下の void setSpeechLang(void*, wchar_t*, wchar_t*) 関数の定義では、 最初の WideCharToMultiByte()関数の呼出しで変換後のデータの長さを調べて、 2回目の WideCharToMultiByte()関数の呼出しで UTF16 から UTF8 に変換して langBuffer に入れています。

    C++のwstringデータを C# に渡す場合は、wchar_t データの領域をヒープ上に確保して、 その領域の先頭アドレスを渡す必要があります。 getSpeech関数では、認識した単語の tag と item という2つの wstring を返す必要があるので、 C#側からアドレス参照を渡してもらって、その参照に文字列データのメモリの位置を書き込んでいます。

    NtKinectDll.cpp
    #include "stdafx.h"
    #include "NtKinectDll.h"
    
    #define USE_THREAD
    #define USE_SPEECH
    #include "NtKinect.h"
    
    using namespace std;
    
    namespace NtKinectSpeech {
      NTKINECTDLL_API void* getKinect(void) {
        NtKinect* kinect = new NtKinect;
        return static_cast<void*>(kinect);
      }
    
      NTKINECTDLL_API void setSpeechLang(void* ptr, wchar_t* wlang, wchar_t* grxmlBuffer) {
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        if (wlang && grxmlBuffer) {
          int len = WideCharToMultiByte(CP_UTF8,NULL,wlang,-1,NULL,0,NULL,NULL) + 1;
          char* langBuffer = new char[len];
          memset(langBuffer,'\0',len);
          WideCharToMultiByte(CP_UTF8,NULL,wlang,-1,langBuffer,len,NULL,NULL);
          string lang(langBuffer);
          wstring grxml(grxmlBuffer);
          (*kinect).acquire();
          (*kinect).setSpeechLang(lang,grxml);
          (*kinect).release();
        }
      }
    
      void speechThreadFunc(NtKinect* kinect) {
        ERROR_CHECK(CoInitializeEx(NULL,COINIT_MULTITHREADED));
        (*kinect).acquire();
        (*kinect).startSpeech();
        (*kinect).release();
        while (speechActive) {
          pair<wstring,wstring> p;
          bool flag = (*kinect)._setSpeech(p);
          if (flag) {
            mutex.lock();
            speechQueue.push_back(p);
            mutex.unlock();
          }
          std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
      }
      
      NTKINECTDLL_API void initSpeech(void* ptr) {
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        speechActive = true;
        speechThread = new std::thread(NtKinectSpeech::speechThreadFunc, kinect);
        return;
      }
      
      NTKINECTDLL_API int speechQueueSize(void* ptr) {
        int n=0;
        mutex.lock();
        n = (int) speechQueue.size();
        mutex.unlock();
        return n;
      }
      
      NTKINECTDLL_API int getSpeech(void* ptr, wchar_t*& tagPtr, wchar_t*& itemPtr) {
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        wmemset(tagBuffer,'\0',SPEECH_MAX_LENGTH);
        wmemset(itemBuffer,'\0',SPEECH_MAX_LENGTH);
        pair<wstring,wstring> p;
        mutex.lock();
        bool empty = speechQueue.empty();
        if (! empty) {
          p = speechQueue.front();
          speechQueue.pop_front();
        }
        mutex.unlock();
        if (!empty) {
          wsprintf(tagBuffer,L"%ls",p.first);
          wsprintf(itemBuffer,L"%ls",p.second);
        }
        tagPtr = tagBuffer;
        itemPtr = itemBuffer;
        return !empty;
      }
      
      NTKINECTDLL_API void destroySpeech(void* ptr) {
        speechActive = false;
        speechThread->join();
        NtKinect *kinect = static_cast<NtKinect*>(ptr);
        (*kinect).acquire();
        (*kinect).stopSpeech();
        (*kinect).release();
        delete speechThread;
        CoUninitialize();
      }
    }
    
  16. "Release" "x64" の状態で「ビルド」から「NtKinectDllのリビルド」選択してDLLファイルを生成します。 フォルダ x64/Release に NtKinectDll.lib および NtKinectDll.dll が生成されます。






  17. [注意](2017/10/07 追記) Visual Studio 2017 Update 2 でのビルド時に「dllimport ...」というエラーが起きる場合は こちらを参考にして NtKinectDll.cpp 内で NTKINECTDLL_EXPORTS をdefineする ことで対処して下さい。

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


Unity内でDLLを利用する

Kinect V2 を使って音声認識をするUnityのプログラムを作成します。

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 を置きます。



  5. 音声認識には sapi.lib (通常は C:\Program Files\Microsoft SDKs\Speech\v11.0\Lib の下にあります) を利用しますが、これはWindows10環境にインストールされていればよく、 Unityの個別のプロジェクトの下にコピーを置く必要はないようです。

  6. Cubeをシーンに配置します。
  7. 上部のメニューから「Game Object」-> 「3D Object」 -> 「Cube」




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







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

    C# 側での string は managed (メモリ管理されていてガベージコレクタがデータの場所を移動することがある) なので、C++側に渡す前に Marshal. StringToHGlobalUni()関数を用いて ヒープメモリ上の unmanaged なUTF16 文字列に変換します。 この操作により、C++に渡した文字列データが C# のガベージコレクタによって場所が移動する危険性は無くなります。 unmanaged なデータは必要がなくなったら Marshal. FreeHGlobal()関数を呼び出してメモリを解放しなければいけません。

    C++側で認識した文字列を2個 C# 側に返すために、 ref宣言がついた引数でポインタへの参照を渡しています。 C++側で確保しているメモリの参照が返されるので C# 側では直ちに Marshal. PtrToStringUni()関数で managed な string に変換しています。

    Unityの string (= System.String) は UTF16 形式の Unicode です。

    CubeBehaviour.cs
    using UnityEngine;
    using System.Collections;
    using System;
    using System.Runtime.InteropServices;
    
    public class CubeBehaviour : MonoBehaviour {
        [DllImport ("NtKinectDll")] private static extern IntPtr getKinect();
        [DllImport ("NtKinectDll")] private static extern void initSpeech(IntPtr kinect);
        [DllImport ("NtKinectDll")] private static extern void setSpeechLang(IntPtr kinect,IntPtr lang,IntPtr grxml);
        [DllImport ("NtKinectDll")] private static extern int getSpeech(IntPtr kinect,ref IntPtr tagPtr,ref IntPtr itemPtr);
        [DllImport ("NtKinectDll")] private static extern void destroySpeech(IntPtr kinect);
        private IntPtr kinect;
        
        void Start () {
    	kinect = getKinect();
    	IntPtr lang = Marshal.StringToHGlobalUni("ja-JP"); // "en-US"
    	IntPtr grxml = Marshal.StringToHGlobalUni("Grammar_jaJP.grxml"); // "Grammar_enUS.grxml"
    	setSpeechLang(kinect,lang,grxml);
    	initSpeech(kinect);
    	Marshal.FreeHGlobal(lang);
    	Marshal.FreeHGlobal(grxml);
        }
        
        void Update () {
    	IntPtr tagPtr = (IntPtr)0;
    	IntPtr itemPtr = (IntPtr)0;
    	int flag = getSpeech(kinect,ref tagPtr,ref itemPtr);
    	string speechTag = Marshal.PtrToStringUni(tagPtr);
    	string speechItem = Marshal.PtrToStringUni(itemPtr);
    	if (flag > 0) {
    	    Debug.Log("tag = "+speechTag);
    	    Debug.Log("item = "+speechItem);
    	}
    	if (flag>0 && speechTag.CompareTo("RED")==0) {
    	    gameObject.GetComponent<Renderer>().material.color = new Color(1.0f, 0.0f, 0.0f, 1.0f);
    	} else if (flag>0 && speechTag.CompareTo("GREEN")==0) {
    	    gameObject.GetComponent<Renderer>().material.color = new Color(0.0f, 1.0f, 0.0f, 1.0f);
    	} else if (flag>0 && speechTag.CompareTo("BLUE")==0) {
    	    gameObject.GetComponent<Renderer>().material.color = new Color(0.0f, 0.0f, 1.0f, 1.0f);
    	} else if (flag>0 && speechTag.CompareTo("EXIT")==0) {
    	    Application.Quit();
    	    UnityEditor.EditorApplication.isPlaying = false;
    	}
        }
        void OnApplicationQuit() {
    	destroySpeech(kinect);
        }
    }
    
  10. CubeBehaviour.cs を CubeのComponentとして付加する。



  11. 音声認識用の単語ファイルをUnityのプロジェクトの直下に配置します。
  12. 音声認識用の言語ファイルは1つでいいのですが、後で言語を切り替えて実験できるように 日本語用ファイル "Grammar_jaJP.grxml" と英語用ファイル "Grammar_enUS.grxml" をUnityのプロジェクトの直下に置きます。




    Grammar_jaJP.grxml
    <?xml version="1.0" encoding="utf-8" ?>
    <grammar version="1.0" xml:lang="ja-JP" root="rootRule" tag-format="semantics/1.0-literals" xmlns="http://www.w3.org/2001/06/grammar">
      <rule id="rootRule">
        <one-of>
          <item>
            <tag>RED</tag>
            <one-of>
              <item> 赤 </item>
              <item> 赤色 </item>
            </one-of>
          </item>
          <item>
            <tag>GREEN</tag>
            <one-of>
              <item> 緑 </item>
              <item> 緑色 </item>
            </one-of>
          </item>
          <item>
            <tag>BLUE</tag>
            <one-of>
              <item> 青 </item>
              <item> 青色 </item>
            </one-of>
          </item>
          <item>
            <tag>EXIT</tag>
            <one-of>
              <item> 終わり </item>
              <item> 終了 </item>
            </one-of>
          </item>
        </one-of>
      </rule>
    </grammar>
    
    Grammar_enUS.grxml
    <?xml version="1.0" encoding="utf-8" ?>
    <grammar version="1.0" xml:lang="en-US" root="rootRule" tag-format="semantics/1.0-literals" xmlns="http://www.w3.org/2001/06/grammar">
      <rule id="rootRule">
        <one-of>
          <item>
            <tag>RED</tag>
            <one-of>
              <item> Red </item>
            </one-of>
          </item>
          <item>
            <tag>GREEN</tag>
            <one-of>
              <item> Green </item>
            </one-of>
          </item>
          <item>
            <tag>BLUE</tag>
            <one-of>
              <item> Blue </item>
            </one-of>
          </item>
          <item>
            <tag>EXIT</tag>
            <one-of>
              <item> Exit </item>
              <item> Quit </item>
              <item> Stop </item>
            </one-of>
          </item>
        </one-of>
      </rule>
    </grammar>
    
  13. 実行すると、音声を認識して Cube の色が赤、青、緑に変化します。 「あか」「あかいろ」「あお」「あおいろ」「みどり」「みどりいろ」という単語を認識してCubeの色が変化します。 「しゅうりょう」または「おわり」という単語を認識するとプログラムの実行が終了します。



  14. [注意] Unityの Game ウィンドウにフォーカスがない場合は、Game 画面は変化しないので注意して下さい。 そのような場合は一度Unityのウィンドウの上部をクリックしてUnityのウィンドウにフォーカスが 設定された状態で音声認識を試して下さい。

  15. 日本語を認識させていましたが、英語を認識するように変更して実行してみましょう。
  16. CubeBehaviour.cs の "ja-JP" を "en-US" に、 "Grammar_jaJP.grxml" を "Grammar_enUS.grxml" に変更すると、 "red", "blue", "green", "exit" という英単語を認識するようになります。

  17. 単語ファイルの記述を増やして、もっとたくさんの言葉を認識できるようにしてみましょう。
  18. Unity のサンプルプロジェクトはこちら CheckNtKinectDll3.zip


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