Kinect V2 で音声認識を行う方法自体は 「NtKinect: Kinect V2 で音声を認識する」で、 音声認識をマルチスレッドで行う方法は 「NtKinect: Kinect V2 をマルチスレッド環境で動作させる」 で説明しました。
また、DLL ファイルを作成してKinect V2 を他のプログラム(Unity など)から利用する方法は、 「NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 で説明しました。
今回は、Kinect V2 で音声認識を行うDLLファイルを作成してUnityから利用する方法を説明します。
「NtKinect: Kinect V2 を使うプログラムをDLL化してUnityから利用する」 で説明したVisual Studio 2017 の 「DLLファイルを作成するプロジェクト NtKinectDll.zip」 をダウンロードして展開して下さい。
次のようなファイル構造になっているはずです。 表示した以外のフォルダやファイルもあるかもしれませんが、今は無視します。
NtKinectDll3/NtKinectDll.sln
x64/Release/
NtKinectDll/dllmain.cpp
NtKiect.h
NtKiectDll.h
NtKiectDll.cpp
stdafx.cpp
stdafx.h
targetver.h
|
まず、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
|

「ソリューションエクスプローラー」-> 「ソースファイル」上で右クリック -> 「追加」 -> 「既存の項目の追加」 -> KinectAudioStream.cpp を選択する

「ソリューションエクスプローラー」-> 「ヘッダーファイル」上で右クリック -> 「追加」 -> 「既存の項目の追加」 -> KinectAudioStream.h を選択する
「ソリューションエクスプローラー」-> 「ヘッダーファイル」上で右クリック -> 「追加」 -> 「既存の項目の追加」 -> WaveFile.h を選択する



「構成プロパティ」 -> 「VC++ディレクトリ」 -> インクルードディレクトリ ->
先頭に $(ProgramW6432)\Microsoft SDKs\Speech\V11.0\Include を追加する
「構成プロパティ」 -> 「VC++ディレクトリ」 -> ライブラリディレクトリ ->
先頭に $(ProgramW6432)\Microsoft SDKs\Speech\V11.0\Lib を追加する
「構成プロパティ」 -> 「リンカー」 -> 全般 -> 入力
sapi.libを追加する
以下の2つのライブラリは既に設定されているはずですが、確認しておきます。
Kinect20.lib opencv_world310.lib
緑文字の部分がプロジェクトを作成した時から定義されている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];
}
|
関数宣言の先頭に "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(); } } |


[注意](2017/10/07 追記) Visual Studio 2017 Update 2 でのビルド時に「dllimport ...」というエラーが起きる場合は こちらを参考にして NtKinectDll.cpp 内で NTKINECTDLL_EXPORTS をdefineする ことで対処して下さい。
上記のzipファイルには必ずしも最新の NtKinect.h が含まれていない場合があるので、 こちらから最新版をダウンロードして 差し替えてお使い下さい。
Kinect V2 を使って音声認識をするUnityのプログラムを作成します。
NtKinectDll.dll を Unityで利用します。


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

上部のメニューから「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);
}
}
|

音声認識用の言語ファイルは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>
|

[注意] Unityの Game ウィンドウにフォーカスがない場合は、Game 画面は変化しないので注意して下さい。 そのような場合は一度Unityのウィンドウの上部をクリックしてUnityのウィンドウにフォーカスが 設定された状態で音声認識を試して下さい。
CubeBehaviour.cs の "ja-JP" を "en-US" に、 "Grammar_jaJP.grxml" を "Grammar_enUS.grxml" に変更すると、 "red", "blue", "green", "exit" という英単語を認識するようになります。