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

Kinect V2 をマルチスレッド環境で動作させる


2016.08.24: created by
Japanese English
NtKinect.h version 1.6 以降に対応しているトピックスです。
目次へ

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


Windows 上でのマルチスレッド・プログラミング

Windows上でマルチスレッドで動作するプログラムを記述するにはまず process.h を読み込みます。

#include <process.h>

スレッドで走る関数 unsigned func (LPVOID) は次の形式で定義します。

unsigned __stdcall func (LPVOID pParam) {

  // このスレッドがやるべき仕事

  _endthreadex(0);
  return 0;
}

マルチスレッドで動作するプログラムにおいては、 クリティカル・セクション(競合が発生する可能性がある場所)では 排他制御をする必要があります。これには Mutex を利用します。 すなわち、どのスレッドからも参照できる変数として Mutex を生成しておき、 この利用権を取れたスレッドだけがクリティカル・セクションに入って よいことにします。

Handle mutex;

int main(int argc, char** argv) {
  mutex = CreateMutex(NULL, FALSE, NULL); // Mutexを生成する
  if (GetLastError() == ERROR_ALREADY_EXISTS) throw エラー;

  ...(略)...

}

unsigned __stdcall func (LPVOID pParam) {

  ...(略)...

  DWORD ret = WaitForSingleObject(mutex, INFINITE); // Mutexの利用権を取得する
  if (ret == WAIT_FAILED) throw エラー;

  ...(略)... //クリティカル・セクション

  ReleaseMutex(mutex); // Mutexの利用権を解放する

  ...(略)...

  _endthreadex(0);
  return 0;
}

メインのスレッドにおいて、N 個のスレッドを起動する場合は次のように記述します。 スレッドの起動を _beginthreadex()関数で、 全てのスレッドの終了待ちを WaitForMultipleObjects()関数で行います。

  HANDLE hThread[N ] = { 0 };

  hThread[0] = (HANDLE) _beginthreadex(NULL,0,関数名1,NULL,0,NULL); // 別スレッドで関数1を実行する
  if (hThread[0] == 0) throw エラー;

  ...(略)... // 2〜(N-2)のスレッドを生成する

  hThread[N -1] = (HANDLE) _beginthreadex(NULL,0,関数名N,NULL,0,NULL); // 別スレッドで関数Nを実行する
  if (hThread[N -1] == 0) throw エラー;

  ...(略)... // メインスレッドでやるべき仕事

  DWORD ret = WaitForMultipleObjects(N ,hThread,TRUE,INFINITE); // 全てのスレッドの終了を待つ
  if (ret == WAIT_FAILED) throw エラー;
  return 0;
}

USE_THREAD 定数を define してから NtKinect.h を include する と NtKinect のマルチスレッド関係のメソッドが有効になります。

NtKinect のマルチスレッドに関するメソッド

返り値の型 メソッド名 説明
void acquire() version1.6以降。
Mutex の権利を獲得する。獲得できるまで呼び出したスレッドの実行はブロックされる。
void release() version1.6以降。
Mutex の権利を解放する。

複数の関数呼び出しの間でatomic性を確保したい場合は、 自分でacquire()とrelease()を呼び出して排他制御した方がよいとは思いますが、 利用を簡便にするために、以下の関数を用意しました。 基本的に次の動作を行います。

acquire()を呼び出す。
Kinectの関数を呼び出す。
結果を引数で指定した変数にコピーする。
release()を呼び出す

返り値の型 メソッド名 説明
void _setRGB(cv::Mat& image ) version1.6以降。スレッドセーフ。
setRGB()を呼び出し、rgbImageをimage にコピーする。以下の操作と同じ。
    acquire();
    setRGB();
    rgbImageをimage にコピーする。
    release();
void _setDepth(cv::Mat image, bool raw = true) version1.6以降。スレッドセーフ。
setDepth(raw)を呼出しdepthImageをimage にコピーする。以下の操作と同じ。
    acquire();
    setDepth(raw );
    depthImageをimage にコピーする。
    release();
void _setInfrared(cv::Mat& image ) version1.6以降。スレッドセーフ。
setInfrared()を呼び出し、infraredImageをimage にコピーする。以下の操作と同じ。
    acquire();
    setInfrared();
    infraredImageをimage にコピーする。
    release();
void _setBodyIndex(cv::Mat& image ) version1.6以降。スレッドセーフ。
setBodyIndex()を呼び出し、bodyIndexImageをimage にコピーする。以下の操作と同じ。
    acquire();
    setBodyIndex();
    bodyIndexImageをimage にコピーする。
    release();
void
_setSkeleton(vector<vector<Joint> >& skel ,
             vector<int>& skelId ,
             vector<UINT64>& skelTrackingId )
version1.6以降。スレッドセーフ。
setSkeleton()を呼び出し、結果を引数にコピーする。以下の操作と同じ。
    acquire();
    setSkeleton();
    skeletonをskel にコピーする。
    skeletonIdをskelId にコピーする。
    skeletonTrakingIdをskelTrackingId にコピーする。
    release();
pair<int,int> _handState(int id =0, bool isLeft =true) version1.6以降。スレッドセーフ。
handState()を呼び出し、結果を返す。以下の操作と同じ。
    acquire();
    auto ret = handState(id , isLeft );
    release();
    return ret;
void
_setFace(vector<vector<PointF> >& point ,
         vector<cv::Rect>& rect ,
         vector<cv::Vec3f>& direction ,
         vector<vector<DetectionResult> >& property ,
         vector<UINT64>& trackingId ,
         bool isColorSpace =true)
version1.6以降。スレッドセーフ。USE_FACEをdefineした場合。
setFace(isColorSpace )を呼び出し、結果を引数にコピーする。次の操作と同等。
    acquire();
    setFace(isColorSpace );
    facePointを point にコピーする。;
    faceRectを rect にコピーする。;
    faceDirectionを direction にコピーする。;
    facePropertyを property にコピーする。;
    faceTrackingIdを trakingId にコピーする。;
    release();
void
_setHDFace(vector<vector<CameraSpacePoint> >& vertices ,
           vector<UINT64>& trackingId ,
           vector<pair<int,int> >& status )
version1.6以降。スレッドセーフ。USE_FACEをdefineした場合。
setHDFace()を呼び出し、結果を引数にコピーする。次の操作と同等。
    acquire();
    setHDFace();
    hdfaceVerticesを vertices にコピーする。;
    hdfaceTrackingIdを trackingId にコピーする。;
    hdfaceStatusを status にコピーする。;
    release();
void
_setGesture(
    vector<pair<CComPtr<IGesture>,float> >& discrete ,
    vector<pair<CComPtr<IGesture>,float> >& continuous ,
    vector<UINT64>& dTrackingId ,
    vector<INT64>& cTrackingId 
)
version1.6以降。スレッドセーフ。USE_GESTUREをdefineした場合。
setGesture()を呼び出し、結果を引数にコピーする。次の操作と同等。
    acquire();
    setGesture();
    discreteGestureを discrete にコピーする。;
    continuousGestureを continuous にコピーする。;
    discreteTrackingIdを dTrackingId にコピーする。;
    continuousTrackingIdを cTrackingId にコピーする。;
    release();
string _gesture2string(const CComPtr<IGesture>& gesture ) version1.6以降。スレッドセーフ。USE_GESTUREをdefineした場合。
gesture2string(gesture)を呼び出す。
void
_setAudio(float& angle ,
          float& confidence ,
          UINT64& trackingId ,
          bool flag  = false)
version1.6以降。スレッドセーフ。USE_AUDIOをdefineした場合。
setAudio(flag)を呼び出し、結果を引数にコピーする。次の操作と同等。
    acquire();
    setAudio(flag);
    beamAngleを angle にコピーする。;
    beamAngleConfidenceを confidence にコピーする。;
    beamTrackingIdを trackingId にコピーする。;
    release();
void
_setSpeech(pair<wstring,wstring>& p )
version1.6以降。スレッドセーフ。USE_SPEECHをdefineした場合。
setSpeech()を呼び出し、結果を引数にコピーする。次の操作と同等。
    acquire();
    setAudio(flag);
    speechTagのコピーとspeechItemのコピーのpairを p に代入する。;
    release();
HRESULT _MapCameraPointToColorSpace(
    CameraSpacePoint sp ,
    ColorSpacePoint *cp )
version1.6以降。スレッドセーフ。
coordinateMapper->MapCameraPointToColorSpace(sp,cp)を呼び出す。
HRESULT _MapCameraPointToDepthSpace(
  CameraSpacePoint sp ,
  DelpthSpacePoint *dp )
version1.6以降。スレッドセーフ。
coordinateMapper->MapCameraPointToDepthSpace(sp,dp)を呼び出す。
HRESULT _MapDepthPointToColorSpace(
  DepthSpacePoint dp ,
  UINT16 depth ,
  ColorSpacePoint *cp )
version1.6以降。スレッドセーフ。
coordinateMapper->MapDepthPointToColorSpace(dp,depth,cp)を呼び出す。
HRESULT _MapDepthPointToCameraSpace(
  DepthSpacePoint dp ,
  UINT16 depth ,
  CameraSpacePoint *sp )
version1.6以降。スレッドセーフ。
coordinateMapper->MapDepthPointToCameraSpace(dp,depth,sp)を呼び出す。

プログラム作成の手順

  1. NtKinect: Kinect V2 で骨格を認識する」 の Visual Studio 2015 のプロジェクト KinectV2_skeleton.zipを用いて作成します。
  2. このプログラムを理解するには、以下のトピックスの知識が必要です。
  3. main.cppの内容を以下のように変更します。
  4. main.cpp
    #include <iostream>
    #include <sstream>
    
    #define USE_THREAD
    #include "NtKinect.h"
    
    using namespace std;
    
    NtKinect kinect;
    
    unsigned __stdcall doJob1(LPVOID pParam) {
      cv::Mat image;
      vector<vector<Joint> > skel;
      vector<int> skelId;
      vector<UINT64> skelTrackingId;
      while (1) {
        kinect._setRGB(image);
        kinect._setSkeleton(skel,skelId,skelTrackingId);
        for (auto person: skel) {
          for (auto joint: person) {
    	if (joint.TrackingState == TrackingState_NotTracked) continue;
    	ColorSpacePoint cp;
    	kinect._MapCameraPointToColorSpace(joint.Position,&cp);
    	cv::rectangle(image,cv::Rect((int)cp.X-5,(int)cp.Y-5,10,10),cv::Scalar(0,0,255),2);
          }
        }
        cv::imshow("1", image);
        auto key = cv::waitKey(1);
        if (key == 'q') break;
      }
      cv::destroyWindow("1");
      _endthreadex(0);
      return 0;
    }
    
    void drawHand(cv::Mat& image, Joint& joint, pair<int,int>& state) {
      cv::Scalar colors[] = {
        cv::Scalar(255,0,0),  // HandState_Unknown
        cv::Scalar(0,255,0),  // HandState_NotTracked
        cv::Scalar(255,255,0), // HandState_Open
        cv::Scalar(255,0,255), // HandState_Closed
        cv::Scalar(0,255,255),  // HandState_Lass
      };
      ColorSpacePoint cp;
      kinect._MapCameraPointToColorSpace(joint.Position,&cp);
      cv::rectangle(image, cv::Rect((int)cp.X-8, (int)cp.Y-8, 16, 16), colors[state.first], 4);
    }
    
    unsigned __stdcall doJob2(LPVOID pParam) {
      cv::Mat image;
      vector<vector<Joint> > skel;
      vector<int> skelId;
      vector<UINT64> skelTrackingId;
      while (1) {
        kinect._setRGB(image);
        kinect._setSkeleton(skel,skelId,skelTrackingId);
        for (int i=0; i<skel.size(); i++) {
          Joint left = skel[i][JointType_HandLeft];
          Joint right = skel[i][JointType_HandRight];
          if (left.TrackingState != TrackingState_NotTracked) {
    	auto state = kinect._handState(i,true);
    	drawHand(image,left,state);
          }
          if (right.TrackingState != TrackingState_NotTracked) {
    	auto state = kinect._handState(i,false);
    	drawHand(image,right,state);
          }
        }
        cv::imshow("2", image);
        auto key = cv::waitKey(1);
        if (key == 'q') break;
      }
      cv::destroyWindow("2");
      _endthreadex(0);
      return 0;
    }
    
    unsigned __stdcall doJob3(LPVOID pParam) {
      cv::Mat image;
      while (1) {
        kinect._setBodyIndex(image,false);
        cv::imshow("3", image);
        auto key = cv::waitKey(1);
        if (key == 'q') break;
      }
      cv::destroyWindow("3");
      _endthreadex(0);
      return 0;
    }
    
    void doJob() {
      HANDLE hThread[3] = { 0 };
      hThread[0] = (HANDLE) _beginthreadex(NULL,0,doJob1,NULL,0,NULL);
      if (hThread[0] == 0) throw runtime_error("cannot create thread 0");
      hThread[1] = (HANDLE) _beginthreadex(NULL,0,doJob2,NULL,0,NULL);
      if (hThread[1] == 0) throw runtime_error("cannot create thread 1");
      hThread[2] = (HANDLE) _beginthreadex(NULL,0,doJob3,NULL,0,NULL);
      if (hThread[2] == 0) throw runtime_error("cannot create thread 2");
      DWORD ret = WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
      if (ret == WAIT_FAILED) throw runtime_error("wait failed");
    }
    
    int main(int argc, char** argv) {
      try {
        doJob();
      } catch (exception &ex) {
        cout << ex.what() << endl;
        string s;
        cin >> s;
      }
      return 0;
    }
    
    
  5. プログラムを実行すると4個のウィンドウが表示されます。
  6. コンソール画面以外のウィンドウを'q'キーで閉じると、コンソール画面も閉じてプログラムが終了します。

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


プログラム作成の手順 (その2)

  1. NtKinect: Kinect V2 で音声を認識する」 の Visual Studio 2015 のプロジェクト KinectV2_Speech.zipを用いて作成します。
  2. 音声認識に必要なファイル(WaveFile.h, KinectAudioStream.h, KinectAudioStream.cpp, Grammaer_jaJP.grxml)がプロジェクトに追加されていて、ライブラリ(sapi.lib)も参照するように追加されているはずです。

  3. このプログラムを理解するには、以下のトピックスの知識が必要です。
  4. main.cppの内容を以下のように変更します。
  5. スレッドセーフではない関数を呼び出すところでは kinect.acquire();kinect.release(); で囲んで、競合状態が発生するのを避けています。

    main.cpp
    #include <iostream>
    #include <sstream>
    
    #define USE_SPEECH
    #define USE_THREAD
    #include "NtKinect.h"
    
    using namespace std;
    
    NtKinect kinect;
    
    unsigned __stdcall doJob1(LPVOID pParam) {
      cv::Mat image;
      vector<vector<Joint> > skel;
      vector<int> skelId;
      vector<UINT64> skelTrackingId;
      while (1) {
        kinect._setRGB(image);
        kinect._setSkeleton(skel,skelId,skelTrackingId);
        for (auto person: skel) {
          for (auto joint: person) {
    	if (joint.TrackingState == TrackingState_NotTracked) continue;
    	ColorSpacePoint cp;
    	kinect._MapCameraPointToColorSpace(joint.Position,&cp);
    	cv::rectangle(image,cv::Rect((int)cp.X-5,(int)cp.Y-5,10,10),cv::Scalar(0,0,255),2);
          }
        }
        cv::imshow("1", image);
        auto key = cv::waitKey(1);
        if (key == 'q') break;
      }
      cv::destroyWindow("1");
      _endthreadex(0);
      return 0;
    }
    
    unsigned __stdcall doJob2(LPVOID pParam) {
      cv::Mat image;
      while (1) {
        kinect._setBodyIndex(image,false);
        cv::imshow("2", image);
        auto key = cv::waitKey(1);
        if (key == 'q') break;
      }
      cv::destroyWindow("2");
      _endthreadex(0);
      return 0;
    }
    
    unsigned __stdcall doJob3(LPVOID pParam) {
      ERROR_CHECK(CoInitializeEx(NULL, COINIT_MULTITHREADED));
      kinect.acquire();
      kinect.startSpeech();
      kinect.release();
      std::wcout.imbue(std::locale(""));
      
      pair<wstring,wstring> p;
      while (1) {
        bool ret = kinect._setSpeech(p);
        if (ret) {
          wcout << p.first << L" " << p.second << endl;
        }
        if (p.first == L"EXIT") break;
        //Sleep(1L);
      }
      kinect.acquire();
      kinect.stopSpeech();
      kinect.release();
      _endthreadex(0);
      return 0;
    }
    
    void doJob() {
      HANDLE hThread[3] = { 0 };
      hThread[0] = (HANDLE) _beginthreadex(NULL,0,doJob1,NULL,0,NULL);
      if (hThread[0] == 0) throw runtime_error("cannot create thread 0");
      hThread[1] = (HANDLE) _beginthreadex(NULL,0,doJob2,NULL,0,NULL);
      if (hThread[1] == 0) throw runtime_error("cannot create thread 1");
      hThread[2] = (HANDLE) _beginthreadex(NULL,0,doJob3,NULL,0,NULL);
      if (hThread[2] == 0) throw runtime_error("cannot create thread 2");
      DWORD ret = WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
      if (ret == WAIT_FAILED) throw runtime_error("wait failed");
    }
    
    int main(int argc, char** argv) {
      try {
        doJob();
      } catch (exception &ex) {
        cout << ex.what() << endl;
        string s;
        cin >> s;
      }
      return 0;
    }
    
    
  6. プログラムを実行すると3個のウィンドウが表示されます。
  7. 全てのスレッドが終了すると、コンソール画面も閉じてプログラムが終了します。

    ただし、「音声認識と骨格認識」および「音声認識とbodyIndex画像取得」の同時動作は相性が悪く、 前者では骨格認識のスレッドが非常に遅くなり、また後者では音声認識の精度が非常に悪くなります。 どちらかのスレッドにSleep(INT32)関数を入れて動作のタイミングを調整すれば 少しはマシになるのかもしれませんが未確認です。

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



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