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

Kinect V2 の骨格認識をDLL化してUnityの人型キャラクタをリアルタイムで動かす


2017.08.06: created by
2017.08.07: revised by
2017.08.10: revised by
2017.08.11: revised by
Japanese English

本トピックスではHumanoidの顔を動かしてはいません。 顔も動かしたい場合は 「 Kinect V2 で骨格と顔を認識してUnityの人型キャラクタをリアルタイムで動かす 」 を参照して下さい。

目次へ

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


UnityのHumanoid のポーズを Kinect V2で認識した骨格に追随させる




  1. NtKinect: Kinect V2 を用いた骨格認識をDLL化してUnityから利用する」 で作成した Unity のプロジェクト CheckNtKinectDll5.zip を利用することにします。
  2. 上記のzipファイルを展開して、フォルダ名を CheckNtKinectDll6/ と変更します。

  3. Unityを起動して上記フォルダのプロジェクトをOpenします。
  4. 新しいシーンを開始し、すぐにそのシーンに名前をつけて Assets/Scenes/humanoid.scene として保存します。
  5. 上のメニュー -> File -> New Scene  
    
    上のメニュー -> File -> Save Scene as ... -> humanoid.scene
    
  6. Assets/Modelsというフォルダを作成し、中に Humanoid (人型キャラクタ)データをimportします。
  7. ここでは 「 MakeHuman: Unity5 で使用する人型モデルを作成する 」 で作成したデータ makehuman.zip を用います。

    上記ファイルを展開するとexports/の下に AsianBoy.fbx と textures/ があるはずです。 これを Assets/Models/ の下にimportします。




    [注意]上の操作は

    Assets -> Import New Asset... -> AsianBody.fbx
    から行なってもよいのですが、これだとAsianBoy.fbx に必要なtextureが 自動ではimportされず、モデルが真っ白になってしまいます。 この場合は Assets/Models/Materials/に生成された白いMaterialに対応する Textureを手動でimportしなくてはいけません。




  8. ProjectウィンドウでAsianBoyを選択をして、 Inspectorウィンドウで "Rig" で Animation Type を "Humanoid" に変更し、 Avatar Definition を "Create From This Model" のままとして、 "Apply"ボタンをクリックします。



  9. 正しく Humanoid のBoneが適用できたことは Inspectorウィンドウの "Configure" の前にチェックがついたことでわかりますが、 "Configure"ボタンを をクリックして、ボーンが正しく設定されたか確かめておいた方が安全です。



  10. Inspectorウィンドウで "Mapping" をクリックして、ボーンの対応を見ます。
  11. 必須なBoneが割り当てられていないと "Configure" にチェックがつかないのでわかりますが、 本プロジェクトでは Optional な Bone も利用しています。 Neck と UpperChest にも Bone が割り当てられていることを確認して下さい




  12. Inspectorウィンドウで "Muscles & Settings" をクリックして、スライダを動かして各ボーンが正しく動作するかを確認します。確認できたら "Done" をクリックします。



  13. Assets/Models から AsianBoy を Hierarchy にドラッグします。 Hierarchy内でAsianBoyが選択された状態で、InspectorのTransformのから Resetを選択して、Position (x,y,z)=(0,0,0)とします。






  14. ProjectのAssets/Scripts/の下に C# のスクリプト RigBone.cs を生成します。 このC#プログラムの中で定義されている RigBone クラスは MonoBehavior クラスを継承していないことに注意しましょう。
  15. RigBone クラスは HumanoidBoneが与えられると、 Humanoid に付加されている Animator コンポーネントから Bone の transform を取り出して管理するクラスです。

    RigBone.cs
    /*
     * Copyright (c) 2017 Yoshihisa Nitta
     * Released under the MIT license
     * http://opensource.org/licenses/mit-license.php
     */
    
    /* http://nw.tsuda.ac.jp/lec/unity5/ */
    /* version 1.1: 2017/08/05 */
    /* version 1.0: 2017/08/02 */
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class RigBone {
      public GameObject gameObject;
      public HumanBodyBones bone;
      public bool isValid;
      public Transform transform {
        get { return animator.GetBoneTransform(bone); }
      }
      Animator animator;
      Quaternion savedValue;
      public RigBone(GameObject g, HumanBodyBones b) {
        gameObject = g;
        bone = b;
        isValid = false;
        animator = gameObject.GetComponent<Animator>();
        if (animator == null) {
          Debug.Log("no Animator Component");
          return;
        }
        Avatar avatar = animator.avatar;
        if (avatar == null || !avatar.isHuman || !avatar.isValid) {
          Debug.Log("Avatar is not Humanoid or it is not valid");
          return;
        }
        isValid = true;
        savedValue = animator.GetBoneTransform(bone).localRotation;
      }
      public void set(float a, float x, float y, float z) {
        set(Quaternion.AngleAxis(a, new Vector3(x,y,z)));
      }
      public void set(Quaternion q) {
        animator.GetBoneTransform(bone).localRotation = q;
        savedValue = q;
      }
      public void mul(float a, float x, float y, float z) {
        mul(Quaternion.AngleAxis(a, new Vector3(x,y,z)));
      }
      public void mul(Quaternion q) {
        Transform tr = animator.GetBoneTransform(bone);
        tr.localRotation = q * tr.localRotation;
      }
      public void offset(float a, float x, float y, float z) {
        offset(Quaternion.AngleAxis(a, new Vector3(x,y,z)));
      }
      public void offset(Quaternion q) {
        animator.GetBoneTransform(bone).localRotation = q * savedValue;
      }
      public void changeBone(HumanBodyBones b) {
        bone = b;
        savedValue = animator.GetBoneTransform(bone).localRotation;
      }
    }
    
    
  16. ProjectのAssets/Scripts/の下に C# のスクリプト CharacterSkeleton.cs を生成します。 このC#プログラムの中で定義されている CharacterSkeleton クラスは MonoBehavior クラスを継承していないことに注意しましょう。
  17. CharacterSkeleton クラスは、一人分の Humanoid のデータを管理し、 与えられた関節データを用いてポーズを変化させるクラスです。

    set()メソッドでは、Kinect V2 で取得した関節データが渡されます。 最大で6人分の関節データがまとめて渡されてくるので、 現在着目している骨格のインデックスが第3引数 offset で 0 から 5 までの整数で指定されます。

    人間の体の向きをHumanoid の向きに反映するように変更しています。 人間の骨盤の向き(y軸回りの回転)がHumanoid 全体の向きに、 人間の両肩の向きがHumanoid の上半身の向きになります。

    CharacterSkeleton.cs
    /*
     * Copyright (c) 2017 Yoshihisa Nitta
     * Released under the MIT license
     * http://opensource.org/licenses/mit-license.php
     */
    
    /* http://nw.tsuda.ac.jp/lec/kinect2/ */
    /* version 1.3: 2017/08/11 */
    /* version 1.2: 2017/08/10 */
    /* version 1.1: 2017/08/07 */
    /* version 1.0: 2017/08/06 */
    
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    
    class CharacterSkeleton {
      public const int
        // JointType
        JointType_SpineBase= 0,
        JointType_SpineMid= 1,
        JointType_Neck= 2,
        JointType_Head= 3,
        JointType_ShoulderLeft= 4,
        JointType_ElbowLeft= 5,
        JointType_WristLeft= 6,
        JointType_HandLeft= 7,
        JointType_ShoulderRight= 8,
        JointType_ElbowRight= 9,
        JointType_WristRight= 10,
        JointType_HandRight= 11,
        JointType_HipLeft= 12,
        JointType_KneeLeft= 13,
        JointType_AnkleLeft= 14,
        JointType_FootLeft= 15,
        JointType_HipRight= 16,
        JointType_KneeRight= 17,
        JointType_AnkleRight= 18,
        JointType_FootRight= 19,
        JointType_SpineShoulder= 20,
        JointType_HandTipLeft= 21,
        JointType_ThumbLeft= 22,
        JointType_HandTipRight= 23,
        JointType_ThumbRight= 24,
        // TrackingState
        TrackingState_NotTracked= 0,
        TrackingState_Inferred= 1,
        TrackingState_Tracked= 2,
        // Number
        bodyCount = 6,
        jointCount = 25;
    
      private static int[] jointSegment = new int[] {
        JointType_SpineBase, JointType_SpineMid,             // Spine
        JointType_Neck, JointType_Head,                      // Neck
        // left
        JointType_ShoulderLeft, JointType_ElbowLeft,         // LeftUpperArm
        JointType_ElbowLeft, JointType_WristLeft,            // LeftLowerArm
        JointType_WristLeft, JointType_HandLeft,             // LeftHand
        JointType_HipLeft, JointType_KneeLeft,               // LeftUpperLeg
        JointType_KneeLeft, JointType_AnkleLeft,             // LeftLowerLeg6
        JointType_AnkleLeft, JointType_FootLeft,             // LeftFoot
        // right
        JointType_ShoulderRight, JointType_ElbowRight,       // RightUpperArm
        JointType_ElbowRight, JointType_WristRight,          // RightLowerArm
        JointType_WristRight, JointType_HandRight,           // RightHand
        JointType_HipRight, JointType_KneeRight,             // RightUpperLeg
        JointType_KneeRight, JointType_AnkleRight,           // RightLowerLeg
        JointType_AnkleRight, JointType_FootRight,           // RightFoot
      };
      public Vector3[] joint = new Vector3[jointCount];
      public int[] jointState = new int[jointCount];
    
      Dictionary<HumanBodyBones,Vector3> trackingSegment = null;
      Dictionary<HumanBodyBones, int>  trackingState = null;
    
      private static HumanBodyBones[] humanBone = new HumanBodyBones[] {
        HumanBodyBones.Hips,
        HumanBodyBones.Spine,
        HumanBodyBones.UpperChest,
        HumanBodyBones.Neck,
        HumanBodyBones.Head,
        HumanBodyBones.LeftUpperArm,
        HumanBodyBones.LeftLowerArm,
        HumanBodyBones.LeftHand,
        HumanBodyBones.LeftUpperLeg,
        HumanBodyBones.LeftLowerLeg,
        HumanBodyBones.LeftFoot,
        HumanBodyBones.RightUpperArm,
        HumanBodyBones.RightLowerArm,
        HumanBodyBones.RightHand,
        HumanBodyBones.RightUpperLeg,
        HumanBodyBones.RightLowerLeg,
        HumanBodyBones.RightFoot,
      };
    
        private static HumanBodyBones[] targetBone = new HumanBodyBones[] {
        HumanBodyBones.Spine,
        HumanBodyBones.Neck,
        HumanBodyBones.LeftUpperArm,
        HumanBodyBones.LeftLowerArm,
        HumanBodyBones.LeftHand,
        HumanBodyBones.LeftUpperLeg,
        HumanBodyBones.LeftLowerLeg,
        HumanBodyBones.LeftFoot,
        HumanBodyBones.RightUpperArm,
        HumanBodyBones.RightLowerArm,
        HumanBodyBones.RightHand,
        HumanBodyBones.RightUpperLeg,
        HumanBodyBones.RightLowerLeg,
        HumanBodyBones.RightFoot,
      };
    
      public GameObject humanoid;
      private Dictionary<HumanBodyBones, RigBone> rigBone = null;
      private bool isSavedPosition = false;
      private Vector3 savedPosition;
      private Quaternion savedHumanoidRotation;
    
      public CharacterSkeleton(GameObject h) {
        humanoid = h;
        rigBone = new Dictionary<HumanBodyBones, RigBone>();
        foreach (HumanBodyBones bone in humanBone) {
          rigBone[bone] = new RigBone(humanoid,bone);
        }
        savedHumanoidRotation = humanoid.transform.rotation;
        trackingSegment = new Dictionary<HumanBodyBones,Vector3>(targetBone.Length);
        trackingState = new Dictionary<HumanBodyBones, int>(targetBone.Length);
      }
      private void swapJoint(int a, int b) {
        Vector3 tmp = joint[a]; joint[a] = joint[b]; joint[b] = tmp;
        int t = jointState[a]; jointState[a] = jointState[b]; jointState[b] = t;
      }
      public void set(float[] jt, int[] st, int offset, bool mirrored, bool move) {
        if (isSavedPosition == false && jointState[JointType_SpineBase] != TrackingState_NotTracked) {
          isSavedPosition = true;
          int j = offset * jointCount + JointType_SpineBase;
          savedPosition = new Vector3(jt[j*3],jt[j*3+1],jt[j*3+2]);
        }
        for (int i=0; i<jointCount; i++) {
          int j = offset * jointCount + i;
          if (mirrored) {
    	joint[i] = new Vector3(-jt[j*3], jt[j*3+1], -jt[j*3+2]);
          } else {
    	joint[i] = new Vector3(jt[j*3], jt[j*3+1], savedPosition.z*2-jt[j*3+2]);
          }
          jointState[i] = st[j];
        }
        if (mirrored) {
          swapJoint(JointType_ShoulderLeft, JointType_ShoulderRight);
          swapJoint(JointType_ElbowLeft, JointType_ElbowRight);
          swapJoint(JointType_WristLeft, JointType_WristRight);
          swapJoint(JointType_HandLeft, JointType_HandRight);
          swapJoint(JointType_HipLeft, JointType_HipRight);
          swapJoint(JointType_KneeLeft, JointType_KneeRight);
          swapJoint(JointType_AnkleLeft, JointType_AnkleRight);
          swapJoint(JointType_FootLeft, JointType_FootRight);
          swapJoint(JointType_HandTipLeft, JointType_HandTipRight);
          swapJoint(JointType_ThumbLeft, JointType_ThumbRight);
        }
        for (int i=0; i<targetBone.Length; i++) {
          int s = jointSegment[2*i], e = jointSegment[2*i+1];
          trackingSegment[targetBone[i]] = joint[e] - joint[s];
          trackingState[targetBone[i]] = System.Math.Min(jointState[e],jointState[s]);
        }
    
        Vector3 waist = joint[JointType_HipRight] - joint[JointType_HipLeft];
        waist = new Vector3(waist.x, 0, waist.z);
        Quaternion rot = Quaternion.FromToRotation(Vector3.right,waist);
        Quaternion rotInv = Quaternion.Inverse(rot);
     
        Vector3 shoulder = joint[JointType_ShoulderRight] - joint[JointType_ShoulderLeft];
        shoulder = new Vector3(shoulder.x, 0, shoulder.z);
        Quaternion srot = Quaternion.FromToRotation(Vector3.right,shoulder);
        Quaternion srotInv = Quaternion.Inverse(srot);
    
        humanoid.transform.rotation = Quaternion.identity;
        foreach (HumanBodyBones bone in targetBone) {
          rigBone[bone].transform.rotation = rotInv * Quaternion.FromToRotation(Vector3.up,trackingSegment[bone]);
        }
        rigBone[HumanBodyBones.UpperChest].offset(srot);
        Quaternion bodyRot = rot;
        if (mirrored) {
          bodyRot = Quaternion.AngleAxis(180,Vector3.up) * bodyRot;
        }
        humanoid.transform.rotation = bodyRot;
        if (move == true) {
          Vector3 m = joint[JointType_SpineBase];
          if (mirrored) m = new Vector3(-m.x, m.y, -m.z);
          humanoid.transform.position = m;
        }
      }
    }
    
    
  18. ProjectのAssets/Scripts/の下に C# のスクリプト RigControl.cs を生成します。
  19. RigControl.cs
    /*
     * Copyright (c) 2017 Yoshihisa Nitta
     * Released under the MIT license
     * http://opensource.org/licenses/mit-license.php
     */
    
    /* http://nw.tsuda.ac.jp/lec/kinect2/ */
    /* version 1.0: 2017/08/06 */
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.Runtime.InteropServices;
    
    public class RigControl : MonoBehaviour {
      [DllImport ("NtKinectDll")] private static extern System.IntPtr getKinect();
      [DllImport ("NtKinectDll")] private static extern int setSkeleton(System.IntPtr kinect, System.IntPtr data, System.IntPtr state, System.IntPtr id);
      int bodyCount = 6;
      int jointCount = 25;
      private System.IntPtr kinect;
    
      public GameObject humanoid;
      public bool mirror = true;
      public bool move = true;
      CharacterSkeleton skeleton;
     
      void Start () {
        kinect = getKinect();
        skeleton = new CharacterSkeleton(humanoid);
      }
      void Update () {
        float[] data = new float[bodyCount * jointCount * 3];
        int[] state = new int[bodyCount * jointCount];
        int[] id = new int[bodyCount];
        GCHandle gch = GCHandle.Alloc(data,GCHandleType.Pinned);
        GCHandle gch2 = GCHandle.Alloc(state,GCHandleType.Pinned);
        GCHandle gch3 = GCHandle.Alloc(id,GCHandleType.Pinned);
        int n = setSkeleton(kinect,gch.AddrOfPinnedObject(),gch2.AddrOfPinnedObject(),gch3.AddrOfPinnedObject());
        gch.Free();
        gch2.Free();
        gch3.Free();
        if (n > 0) {
          skeleton.set(data,state,0,mirror,move);
        }
      }
    }
    
    
  20. Hierarchy に Empty Object を追加し、名前を RigController と変更しましょう。 Hierarchy内で RigController が選択された状態で、InspectorのTransformのから Resetを選択して、Position (x,y,z)=(0,0,0)とします。



  21. Assets/Scripts/RigControl.cs を Hierarchy のRigController にドラッグします。 HierarchyでRigController を選択すると、Inspectorに "Rig Control (Script)" コンポーネントが 追加されていることがわかります。 この Humanoid に Hierarchy の AsianBoy を設定します。






  22. Hierarchy のMain Cameraの位置を変更しましょう。
  23. Transform Position (x,y,z) = (0, 1, -2)
    
  24. 実行すると、Kinect V2 が認識した人間のポーズに Humanoid が追随します。
  25. RigController に付加されている "Rig Control (Script)" コンポーネントの mirrored がチェックされていると、 人間の鏡像の動きに追随します。 mirrored のチェックをはずすと Humanoid はカメラに背を向けて、人間の動きに そのまま追随するようになります。 (注)実行中のUnityのウィンドウの右に小さく表示されているKinect V2の認識中の画面は すでに鏡像になったものです。

    Humanoid の transform.rotation の値をプログラムで変更するので、 AsianBoy の Transform の Rotation の初期値はなんであっても構いません。

    実行中のキャプチャ画面はこちら CheckNtKinectDll6d.mp4。実行の途中で mirrored のチェックをはずしています。

    また、moveのチェックをはずすと人間の場所の移動には追随しなくなります。







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

    [注意2] CharacterSkeleton.cs の中でオプショナルなBoneである HumanBodyBones.Neck および HumanBodyBones.UpperChest にアクセスしています。 もしもお使いのHumanoidデータでこのBoneが設定できていない場合は、エラーとなります。

    [注意3] このプログラムでは顔の向きが変化しません。 顔認識をして得られた顔の向きを HumanBodyBones.Head の向きに反映させた方がよいと思われますが、 ここでは説明を簡単にするために省略しています。

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

複数の骨格を認識して Humanoid の動きに反映させる

6人までの Humanoid を同時に動かすプロジェクトを作成します。

  1. 上で作成したUnityのプロジェクト CheckNtKinectDll6.zip を展開します。フォルダ名を CheckNtKinectDll6b/ と変更したものとして以下の説明を行います。
  2. シーンを新しく名前をつけて保存します。
  3.   File -> Save Scence as -> humanoid2.unity
    
  4. 5種類の Humanoid を Assets/Models の下に追加で import します。
  5. ここでは makehuman で作成した次のデータ makehuman2.zipを使います。 AfricanBoy, AfricanGirl, AsianGirl, CaucasianBoy, CaucasianGirl および texture/ をそれぞれimport して、Animation Type を Humanoid に変更します。




  6. Assets/Models/から 5つのモデルを Hierarchy にドラッグしてシーンに追加します。
  7. Position や Rotation をそれぞれ次のように設定します。 Position の Z 座標を -10 として、カメラに写らなくしているだけです。

    Model NamePositionRotation
    XYZXYZ
    AsianBoy00-10000
    AsianGirl00-10000
    AfricanBoy00-10000
    AfricanGirl00-10000
    CaucasianBoy00-10000
    CaucasianGirl00-10000



  8. Assets/Scripts/RigControl2.cs を作成します。
  9. RigControl2.cs
    /*
     * Copyright (c) 2017 Yoshihisa Nitta
     * Released under the MIT license
     * http://opensource.org/licenses/mit-license.php
     */
    
    /* http://nw.tsuda.ac.jp/lec/kinect2/ */
    /* version 1.1: 2017/08/10 */
    /* version 1.0: 2017/08/06 */
    
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.Runtime.InteropServices;
    
    public class RigControl2 : MonoBehaviour {
      [DllImport ("NtKinectDll")] private static extern System.IntPtr getKinect();
      [DllImport ("NtKinectDll")] private static extern int setSkeleton(System.IntPtr kinect, System.IntPtr data, System.IntPtr state, System.IntPtr id);
      int bodyCount = 6;
      int jointCount = 25;
      private System.IntPtr kinect;
    
      public GameObject[] humanoid = new GameObject[] {null, null, null, null, null, null};
      public bool[] mirror = new bool[] {true, true, true, true, true, true};
      public bool[] move = new bool[] {true, true, true, true, true, true};
      CharacterSkeleton[] skeleton = new CharacterSkeleton[] {null, null, null, null, null, null};
    
      void Start () {
        kinect = getKinect();
        for (int i=0; i<bodyCount; i++) {
          if (humanoid[i] != null) {
    	skeleton[i] = new CharacterSkeleton(humanoid[i]);
          }
        }
      }
      void Update () {
        float[] data = new float[bodyCount * jointCount * 3];
        int[] state = new int[bodyCount * jointCount];
        int[] id = new int[bodyCount];
        GCHandle gch = GCHandle.Alloc(data,GCHandleType.Pinned);
        GCHandle gch2 = GCHandle.Alloc(state,GCHandleType.Pinned);
        GCHandle gch3 = GCHandle.Alloc(id,GCHandleType.Pinned);
        int n = setSkeleton(kinect,gch.AddrOfPinnedObject(),gch2.AddrOfPinnedObject(),gch3.AddrOfPinnedObject());
        gch.Free();
        gch2.Free();
        gch3.Free();
        for (int i=0; i<bodyCount; i++) {
          if (i < n && skeleton[i] != null) {
    	skeleton[i].set(data,state,i,mirror[i],move[i]);
          } else {
    	humanoid[i].transform.position = new Vector3(0,0,-10);
          }
        }
      }
    }
    
    
  10. Hierarchy で RigController を選択してから、 Inspector で RigControl コンポーネントを Remove Component します。
  11. Assets/Scripts/RigControl2.cs を Hierarchy の RigController 上にドロップします。 Hierarchy で RigControler を選択すると表示される、 Inspector の "Rig Control 2 (Script)" コンポーネント中の Humanoid に 6つのモデルをそれぞれ選択します。



  12. 実行すると、6人まで同時認識して、Humanoid が動きます。
  13. Unity のサンプルプロジェクトはこちら CheckNtKinectDll6b.zip
  14. 骨格認識だけではなく顔認識も用いて Humanoid を動かす方法は、こちらを参照して下さい。


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