オブジェクトの広場は株式会社オージス総研グループのエンジニアによる技術発表サイトです

その他

Azure Kinect DK で遊んでみた.

導管・ガス製造・発電システム部
飯田 哲士
2020年3月26日

会社の取り組みのひとつに「自分が探した新しい技術を使って好きな活動していい」制度があり,これを活用し将来何かの役に立つ(かもしれない)ことを日々調べております.今回 Azure Kinect DK というデバイスを手に入れましたのでご紹介いたします.

はじめに

ここに日本未発売のデプスカメラ Azure Kinect DK があります.いきおいで上海在住の方にお願いして(会社の金で)買ってもらったものなのですが使いみち全然考えてなかったためもっぱら私の遊び道具と化してしまったこの楽しいデバイスを紹介させていただきます.

Azure Kinect DK とは

Microsoft が出している “Kinect” シリーズの最新作です.物の距離や大きさを測るものです.前方に赤外線を照射して反射時間を測り,前方の物体との距離をラスタ画像の各ピクセルの属性値として取得することができる未来的なやつです.

Azure Kinect DK

名前に「Azure」とか付いてますが普通にスタンドアロンで動きます.ですので本稿では Azure 要素は見なかったことにして本製品を「Kinect DK」と略します.

SDKについて

Kinect DK のハードウエアは,MR/ARヘッドセットの Hololens2 から目と耳(深度カメラとマイクアレイ)を取り出したようなものです.しかしソフトウエア的には Hololens 系とほとんど互換性がありません.SDK も違えば主な開発言語も違います.Kinect DK は基本的には C/C++ API での開発が想定されています.

Kinect シリーズ以外にも各社からデプスセンサが売られてますが,プログラミングインターフェースにスタンダード的なものはないようです.どこかがカラー点群を取得するハードウエア独立な API を作ってくれるとありがたいんですが.現状は各デプスカメラのベンダがそれぞれ専用の SDK を出しているので,機種選定にあたっては使いたいユースケースにフィットする機能をもっているか,候補機種の API をよく調べておくことをお勧めします.

Kinect DK の場合は,比較的抽象度の高い API に力を入れようとしてるように見えます.一方,Microsoftが重視してないユースケース,たとえば生の点群データを永続化したり Python スクリプトと連携させたりなどはやや面倒くさい印象です(Intel 製のデプスカメラ “RealSense” 比).

今回は Kinect DK の目玉機能,人体の姿勢を推定する Body Tracking API を使ってみます.

Azure Kinect DK (Body Tracking API)の開発環境

ボディトラッキングを行うには,"Sensor SDK" と “Body Tracking SDK” の2つが必要です.

https://docs.microsoft.com/ja-jp/azure/Kinect-dk/about-azure-kinect-dk

Body Tracking API は以前は C/C++ IF だけだったのですが 2020年1月下旬にリリースされた v1.0 あたりからは C# バインディングも入るようになりました. VisualStudio2019 で C# の x64 の新規プロジェクトを作成し,nuget で “Microsoft.Azure.Kinect.BodyTracking” を追加すると依存ライブラリもろもろ含めインストールしてくれます.簡単!

プロジェクトには “cudnn647.dll” や “dnnmodel20.onnx” なんて名前のファイルが追加されます.NVIDIA の GPU 使って DNN で処理してます.カッコイイ!

GPU 要件は公式ドキュメントによると GTX1070 以上.私は GTX1060 で動かしましたが1~2人認識する程度ならこれで余裕もって動きます.仕様的には同時に6人トラッキングできるらしいです.スゴイ!

Body Trackingのプログラミング

1) 人体の姿勢データをキャプチャする

さっそく API のマニュアルをみつつプログラムを組んでみるわけですが…

https://docs.microsoft.com/ja-jp/azure/Kinect-dk/access-data-body-frame

都合よく GitHub 上に C# ビュアーのサンプルコードがあります.これのキャプチャ部分をコピペすればOK.以上!

https://github.com/microsoft/Azure-Kinect-Samples/tree/master/body-tracking-samples/csharp_3d_viewer

詳細は上記サンプルコード見てもらうとして,Microsoft.Azure.Kinect.BodyTracking.Tracker ってクラスにキャプチャ結果を enqueue して pop すると,各関節のワールド座標系でのミリメートル単位の三次元座標と向き,およびお尻(Hip)を基準にした各関節の相対接続角度が取れます.

2) Bodyの各ノードをシリアライズする

API の戻り値は,腰をルートとしたツリー構造で人体の姿勢が表現されてます. 取得した姿勢データの数値みても我々凡人にはさっぱりわかりません.人体の形にビジュアライズしたいです.元サンプルコードの棒人間ビジュアライザは楽しくないので今回は Unity を使った表示に挑戦してみます. まずは Unity で扱いやすいデータに変換してみます.

Microsoft が想定している人体モデルと Unity が想定している Humanoid モデルの関節のつながりや名称には互換性がないので,Microsoft の API の戻り値を Unity の対応する関節名に変換して,Json.NET 使ってシリアライズします.

Kinect DK と Unity の関節対応表

Dictionary<JointId, string> bodyNodeNames = new Dictionary<JointId, string>()
{
   // Kinect BodyTracking API のJointID と Unityの HumanBodyBones との対応表
   // Unity のライブラリとリンクしなくて済むよう Unity 側は文字列で.
   {JointId.Pelvis,        "Hips"},                    //  0.骨盤
   {JointId.SpineNavel,    "Spine"},                   //  1.へそ     0と繋がる
   {JointId.SpineChest,    "Chest"},                   //  2.胸      1と繋がる
   {JointId.Neck,          "Neck" },                   //  3.首       2と繋がる

   {JointId.ClavicleLeft,  "LeftShoulder"},            //  4.左鎖骨   2と繋がる
   {JointId.ShoulderLeft,  "LeftUpperArm"},            //  5.左肩     4と繋がる
   {JointId.ElbowLeft,     "LeftLowerArm"},            //  6.左肘     5と繋がる
   {JointId.WristLeft,     "LeftHand"},                //  7.左手首   6と繋がる
   {JointId.HandLeft,      "LeftIndexProximal"},       //  8.左手     7と繋がる
   {JointId.HandTipLeft,   "LeftIndexDistal"},         //  9.左手先   8と繋がる
   {JointId.ThumbLeft,     "LeftThumbDistal"},         // 10.左親指   9と繋がる

   {JointId.ClavicleRight, "RightShoulder"},           // 11.右鎖骨   2と繋がる
   {JointId.ShoulderRight, "RightUpperArm"},           // 12.右肩    11と繋がる
   {JointId.ElbowRight,    "RightLowerArm"},           // 13.右肘    12と繋がる
   {JointId.WristRight,    "RightHand"},               // 14.右手首  13と繋がる
   {JointId.HandRight,     "RightIndexProximal"},      // 15.右手    14と繋がる
   {JointId.HandTipRight,  "RightIndexDistal"},        // 16.右手先  15と繋がる
   {JointId.ThumbRight,    "RightThumbDistal"},        // 17.右親指  16と繋がる

   {JointId.HipLeft,       "LeftUpperLeg"},            // 18.左尻     0と繋がる
   {JointId.KneeLeft,      "LeftLowerLeg"},            // 19.左膝    18と繋がる
   {JointId.AnkleLeft,     "LeftFoot"},                // 20.左足首  19と繋がる
   {JointId.FootLeft,      "LeftToe"},                 // 21.左足    20と繋がる

   {JointId.HipRight,      "RightUpperLeg"},           // 22.右尻     0と繋がる
   {JointId.KneeRight,     "RightLowerLeg"},           // 23.右膝    22と繋がる
   {JointId.AnkleRight,    "RightFoot"},               // 24.右足首  23と繋がる
   {JointId.FootRight,     "RightToe"},                // 25.右足    24と繋がる

   {JointId.Head,          "Head"},                    // 26.頭       3と繋がる
   {JointId.Nose,          ""},                        // 27.鼻      26と繋がる -

   {JointId.EyeLeft,       "LeftEye"},                 // 28.左目    27と繋がる
   {JointId.EarLeft,       ""},                        // 29.左耳    28と繋がる -
   {JointId.EyeRight,      "RightEye"},                // 30.右耳    27と繋がる
   {JointId.EarRight,      ""},                        // 31.右耳    30と繋がる -
};

変換表使って JSON シリアライズ

private struct JointGeom {
    public Vector3 pos;       // 関節の絶対座標
    public Quaternion qt;     // 上位関節との相対角度
}
private String CreatePose()
{
   using (var lastFrame = visualizerData.TakeFrameWithOwnership())
   {
      // ~略~

      var skeleton = lastFrame.GetBodySkeleton(i);
      Dictionary<string, JointGeom> bodyBones = new Dictionary<string, JointGeom>();
      for (int jointId = 0; jointId < (int)JointId.Count; ++jointId)
      {
         var joint = skeleton.GetJoint(jointId);
         var nodeName = bodyNodeNames[(JointId)jointId];
         if (nodeName.Length > 0) {
               bodyBones[nodeName] = new JointGeom { pos = joint.Position, qt = joint.Quaternion };
         }
      }
      return JsonConvert.SerializeObject(bodyBones);
   }
}

からの適当な HTTP 配信.

   TcpListener tcp = new TcpListener(IPAddress.Any, MY_SERVICE_PORT);
   tcp.Start();
   Task.Run(() =>
   {
         string httpHeader = "HTTP/1.1 200 OK\n\n"; // 適当なヘッダ
         string lastPose = "";
         while (true)
         {
            // 雑にWebの待ち受け
            var sock = tcp.AcceptSocket();
            byte[] data = new byte[1024];
            sock.Receive(data);

            // ポーズを取り出す
            var poseJson = CreatePose();
            lastPose = poseJson.Length > 0 ? poseJson : lastPose;

            // 出力する
            byte[] content = Encoding.UTF8.GetBytes(httpHeader + lastPose);
            sock.Send(content);
            sock.Close();
         }
   });

これで,姿勢のリアルタイム配信基盤が出来ました(雑!)

3) Unity使ったUWPアプリでレンダリング

つぎに配信された姿勢をビジュアライズします.Unity 2018.4 と Visual Studio 2019 を使って,配信された姿勢データでアバターを動かしてみましょう.

Unity 2018.4 で新規プロジェクトを作成し,シーンに適当なアバターを配置します.つぎに HTTP配信された JSON ストリームを Unity の JSON デシリアライザ使って受け取るためのクラスを作ります.

Unity側で姿勢を受け取るクラス

[System.Serializable]
public class PoseInfo
{
    public PoseElem Hips;
    public PoseElem Spine;
    public PoseElem Chest;
    public PoseElem Neck;

    public PoseElem LeftShoulder;
    public PoseElem LeftUpperArm;
    public PoseElem LeftLowerArm;
    public PoseElem LeftHand;
    public PoseElem LeftIndexProximal;
    public PoseElem LeftIndexDistal;
    public PoseElem LeftThumbDistal;

    public PoseElem RightShoulder;
    public PoseElem RightUpperArm;
    public PoseElem RightLowerArm;
    public PoseElem RightHand;
    public PoseElem RightIndexProximal;
    public PoseElem RightIndexDistal;
    public PoseElem RightThumbDistal;

    public PoseElem LeftUpperLeg;
    public PoseElem LeftLowerLeg;
    public PoseElem LeftFoot;
    public PoseElem LeftToe;

    public PoseElem RightUpperLeg;
    public PoseElem RightLowerLeg;
    public PoseElem RightFoot;
    public PoseElem RightToe;

    public PoseElem Head;
    public PoseElem LeftEye;
    public PoseElem RightEye;

    public static PoseInfo CreateFromJSON(string jsonString)
    {
        return UnityEngine.JsonUtility.FromJson<PoseInfo>(jsonString);
    }
}
[System.Serializable]
public class PoseElem
{
    public PoseElemPos pos;
    public PoseElemQt qt;
    public bool valid = true;
}
[System.Serializable]
public struct PoseElemPos
{
    public float X;
    public float Y;
    public float Z;
    public Vector3 GetVector3()
    {
        // 右手系->左手系変換して mm->m に変換。向きはあとで調整
        return new Vector3(-X * 0.001f, -Y * 0.001f, -Z * 0.001f);
    }
}
[System.Serializable]
public struct PoseElemQt
{
    public float X;
    public float Y;
    public float Z;
    public float W;
    public Quaternion GetQuaternion()
    {
        // 右手系->左手系変換。向きはあとで調整
        return new Quaternion(-X, -Y, -Z, -W);
    }
}

受信データをデシリアライズします.

IEnumerator PoseReceiver(string url, string path = "")
{
   string poseJson = "";
   while (true)
   {
      using (UnityWebRequest request = UnityWebRequest.Get(url))
      {
            yield return request.SendWebRequest();

            if (request.isHttpError || request.isNetworkError)
            {
               poseJson = "";
               yield return new WaitForSeconds(5.0f);
            }
            else
            {
               poseJson = request.downloadHandler.text;
            }
            // さらっとテキスト化だけしていったん処理を返す
            yield return null;
      }
      if (poseJson.Length > 0)
      {
            // がっつりJSONパースして体の動きを反映する
            pose = PoseInfo.CreateFromJSON(poseJson);

            // ~略~ (手足のターゲットとなるGameObjectを動かす処理.後述)
      }
      // リクエストの間隔をちょっと開ける
      yield return new WaitForSeconds(0.2f);
   }
}

取得した姿勢グラフをアバターに適用するには2つの方法があります.

  1. 各関節が接続される上位関節との相対角度をもとにアバターを動かす
  2. 各関節の絶対位置(デカルト座標)を使ってアバターを動かす

いろんなプロポーションの人の姿勢を抽象的に取り扱いたい場合や人の動きから意図を推測するといった用途なら前者がよいでしょうし,骨格の構造がそもそも人でないアバターを人間ぽく動かすとかであれば後者がいいでしょう.

最初アバターの各関節の角度を直接設定して動かす前者の方法にチャレンジしたのですが Kinect DK の 骨格と Unity が想定している骨格の定義の違いに面倒くさくなって後者のやり方に変更しました.手足の先端の目標位置を設定して関節を逆算する Unity の IK ( Inverse Kinematics ) という機能を使って実装したら簡単 1 かつ自然に動きました.

IK の実装詳細はいろんなところに書いてあるので省略します.簡単に説明すると上記 PoseReceiver() 内にて手足移動のターゲットとなる非表示の GameObject を動かし,アバターへのモーション反映と補完は Update() ハンドラ内で実施,という二段階の操作になります.

Unity で UWP(Universal Windows Platform) 形式でビルドして実行するとこんな感じになります.

UWPアプリの動画

(↑画像クリックで動画再生.音声なし)

デモ動画で常にひざが曲がり加減なのは Unity ちゃん 3D モデルの足の長さとキャプチャした人の足の長さが合わずやや余るからです.なお Unity ちゃんの身長は約1.5m.草はえる

今回有償のアセットは使わない方針で自前で雑な補完かけてます.がっつり補完かけるとヌルヌルうごきますがやりすぎると動きにキレがなくなるのでパラメータ調整が悩ましいです.きれいに動かすなら FinalIK 等の有償アセットを使うと楽できるらしい(買ってないからよく知らない)

4) Hololens2 で現実世界とMix

せっかくなので私はこの Hololens2 を使ってみます.

Hololens2

システム構成はこういう感じ.

荒ぶるオワタのポーズ

Unity2018.4 + VisualStudio2019 + MRTK2.1 で作ってみた.

この文章は Hololens2 のプログラミングを広く知らしめるために書いてるわけではなく単に入手困難な Hololens2 を見せびらかしたかっただけなので Hololens2 や MRTK (Mixed Reality Toolkit) の詳細は省略します.

…だけだと怒られそうなので説明すると Hololens2 アプリは Unity の UWP アプリとして作れるので先のセクションで作った UWP 版ビュアーを小変更するだけで動くよ,ってことです. 具体的な手順はUnityでMRTK2パッケージをImportしてメインカメラ削除してUnityのメニューから"Add to Scene and Configure"を実行してパラメータ調整してビルド,PCにHololens2繋いでVisualStudioでARM形式で“デバッグなし実行"するだけ.はい完成!

Hololens2アプリの動画

(↑画像クリックで動画再生、音声あり)

今回 Hololens2 側アバターを切り替える機能を追加しておじさん表示にしてみました.Unity の Humanoid (HumanBodyBones) 準拠で動かしてるのでパソコン画面の Unity ちゃんも Hololens2 で受肉したおじさんも同じコードでだいたい同じ感じに動きます. なお動画の音声は Azure Cognitive Services の Speech API 使って作りました(「Azure 要素入れなきゃ!」って使命感で).

Hololens2 と Kinect DK は対象との距離を測るにあたってどっちも赤外線を照射するので同じ空間内で干渉しないで使えるのか不安だったのですが,実際やってみるととくに何も対策せずとも問題なく動くようで安心しました. Kinect DK を複数台使って同じ対象に向ける場合は同期とったほうがいいみたいです(SYNC 用の物理的な信号線が出てます).

座標の扱いが面倒くさい件

Kinect DK と Hololens2 を組み合わせて使うにあたってややこしいのは座標のすりあわせです.Unity ユーザーなら Quaternion 操作でマレーグマのツヨシ君的なポーズになったことの一度や二度はあるはず.

頭いたい

Kinect DK と Hololens2 の座標出力は,原点も向きも回転方向もスケールも違います. Hololens2 は現実空間に張り付いた相対座標値を取れます.ユーザーの移動にあわせて自視点(カメラ位置)の座標が刻々と変わるのですが,Kinect DK で取れる座標は自カメラ座標固定での対象物との相対座標です.Kinect DK のハードウエア的にはIMU(加速度と姿勢がわかるセンサー)付いてますが,出力座標に自動的に補償かけてくれたりはしません. 移動する Kinect DK と Hololens との座標を同じ空間内で使いたい場合は Kinect DK に 物理的なAR マーカーつけて Hololens 側座標系を基準にして Vuforia 使って相対位置と向きを調整してやるのが楽です.2

まとめ

Kinect DK を使って,遠隔地で誰かが踊った映像を今いる場所にリアルタイム再現できます.

バ美肉おじさんしたいだけならレンダリング後映像のストリーミング中継でいいんですが高解像度で遅延なく流したいならそれなりの回線の確保が必要です. 今回作ったのは軽量なベクトルデータ配信ですので外の貧弱なモバイル回線でもリアルタイム配信できるはず. 姿勢の配信側を 5フレ/sec 程度に抑えても Unity 側でばんばんフレーム補完して 60fps とかに水増しすれば結構見られる動きします.電文サイズは無圧縮で 1 フレーム 4KB ほどですのでチューニングすれば 64Kbps 程度の上り帯域で充分.なんなら格安 SIM でも余裕です.

クライアント側レンダリング前提なので視聴者は視点を上下左右自由に変えられます.より実用的なユースケースとしてはたとえば在宅勤務者とプライバシーに配慮したテレビ会議したい時や東尋坊の先端で安全にアッガイを踊らせるといった業務で役に立つ可能性を感じています3

Unity も VisualStudio も個人の非営利開発だと無償で使えます.GPU や Kinect への数万円の投資はゲームなどにも使えるから無視できるとして,誰でもお金をかけずにモーションキャプチャできる時が来てます4.ぜひ試してみてください.


  1. 「簡単」といいつつもUnityとKinectDKで座標軸の向きや右手系左手系が違ってたりで調整はいります.ダサいですが現物合わせで回して目視で調整しました. 

  2. ガチの業務利用したい人が現実空間とHololensの位置合わせする場合,位置固定した測量機器(トータルステーション)でHololensを追尾して,トータルステーションを基準にHololens側の座標を動かして地理座標に変換する,って逆パターンの製品を先日見た. 

  3. キャプチャする人とアバターの体形はある程度似てないと自然には動かない.私の場合Unityちゃんよりアッガイに近いのでたぶん問題ない. 

  4. この記事の公開準備中(2020年3月18日)に新iPad Proが発表されました,アウトカメラ側に新たに搭載された LiDAR(距離スキャナ)がモーションキャプチャーに使えるとこのと.ARKit API でも LiDAR が標準サポートされます.これはあと1~2年もしたらあらゆるモバイルデバイスに当たり前のように距離センサが付くのではないだろうか.というかむしろ「新iPad Pro買え!」で済む話ではないだろうか.