😎

モーショントラッキングXRアプリを作ってみた

2024/07/01に公開

1. はじめに

近年、"メタバース"の用語と共に、XR(Xを変数と見立てたMR、AR、VRといった技術の総称。Mixed Reality:複合現実、Augumented Reality:拡張現実、Virtual Reality:仮想現実)が注目されています。

XRは、ポストスマホとの声もある次世代の入出力インターフェースにもなり得る技術でもあるため、業界問わずその動向が注視されています。

金融業界でも活用の余地があるのではないかと考え、今回は『バーチャル空間での接客の実現可能性』を検証するために、モーショントラッキングによるバーチャル空間上のアバターをうまくリアルタイム操作可能かを試してみました。

2. 今回実装したアプリの概要

1. 最終的なアプリのデモ

今回試作したプロトタイプアプリの実行画面は以下になります。
Webカメラで取得した人物の動きを、リアルタイムにバーチャル空間上に反映させ、3Dモデルを現実の人物と同じように動かします。

2. アプリケーション構成

アプリは以下のような構成になっています。

①Webカメラの映像を入力として渡し、③Mediapipeというライブラリを使用します。
②カメラ映像から人物の動きを推定し、④3Dモデルに反映します。

3. 開発環境

開発環境は以下となります。
基本的には、UnityとMediapipeが動作すれば問題なく動くかと思います。

  • ハードウェア

    • MacBook Pro M3 Max
  • ソフトウェア

    • Unity: 2022.3.18
    • MediapipeUnityPlugin: 0.12.0

3. 人物のポーズ推定

1. 使用するライブラリ(Mediapipe)

Webカメラに映った人の動きを識別するために、Mediapipeというライブラリを使用します。

Mediapipeとは、Googleが開発しているオープンソースの機械学習ライブラリです。
画像や動画から、顔、手、全身の姿勢などをリアルタイムに識別することができます。

https://developers.google.com/mediapipe/

今回はMediapipeをUnityで利用するために、下記のMediapipe Unity Pluginを使用します。

https://github.com/homuler/MediaPipeUnityPlugin

2. Webカメラの映像から人物の姿勢を識別する

下記画像のように、UnityのScene内に以下のコンポーネントを配置します。

  • ① 空のオブジェクト(Solutionという名前)
  • ② RawImage(カメラ映像を投影するために配置)

また、Asset直下に"BodyTrace.cs"という名前でスクリプトを作成します。

BodyTrace.csは下記のようになります。


public class BodyTrace : MonoBehaviour
{

    // 省略

    private IEnumerator Start()
    {
        // 接続されているWebカメラを選択して、初期設定を行う。
        if (WebCamTexture.devices.Length == 0)
        {
            throw new System.Exception("Web Camera devices are not found");
        }
        var webCamDevice = WebCamTexture.devices[0];

        _webCamTexture = new WebCamTexture(webCamDevice.name, _width, _height, _fps);
        _webCamTexture.Play();
        yield return new WaitUntil(() => _webCamTexture.width > 16);
        _screen.rectTransform.sizeDelta = new Vector2(_webCamTexture.width, _webCamTexture.height);
        _inputTexture = new Texture2D(_webCamTexture.width, _webCamTexture.height, TextureFormat.RGBA32, false);
        _inputPixelData = new Color32[_webCamTexture.width * _webCamTexture.height];
        _outputTexture = new Texture2D(_webCamTexture.width, _webCamTexture.height, TextureFormat.RGBA32, false);
        _outputPixelData = new Color32[_webCamTexture.width * _webCamTexture.height];
        _screen.texture = _outputTexture;

        // mediapipeの設定・起動を行う
        _resourceManager = new StreamingAssetsResourceManager();
        yield return _resourceManager.PrepareAssetAsync("pose_landmark_lite.bytes");
        yield return _resourceManager.PrepareAssetAsync("pose_detection.bytes");
        var stopwatch = new Stopwatch();
        _graph = new CalculatorGraph(_configAsset.text);
        var poseLandmarksStream = new OutputStream<NormalizedLandmarkListPacket, NormalizedLandmarkList>(_graph, "pose_landmarks");
        poseLandmarksStream.StartPolling().AssertOk();
        var sidePacket = new SidePacket();
        sidePacket.Emplace("model_complexity", new IntPacket((int)_modelComplexity));
        sidePacket.Emplace("input_rotation", new IntPacket(0));
        sidePacket.Emplace("input_horizontally_flipped", new BoolPacket(true));
        sidePacket.Emplace("input_vertically_flipped", new BoolPacket(true));
        sidePacket.Emplace("smooth_landmarks", new BoolPacket(true));
        sidePacket.Emplace("enable_segmentation", new BoolPacket(true));
        sidePacket.Emplace("smooth_segmentation", new BoolPacket(true));
        sidePacket.Emplace("output_rotation", new IntPacket(0));
        sidePacket.Emplace("output_horizontally_flipped", new BoolPacket(false));
        sidePacket.Emplace("output_vertically_flipped", new BoolPacket(false));
        _graph.StartRun(sidePacket).AssertOk();
        stopwatch.Start();
        var screenRect = _screen.GetComponent<RectTransform>().rect;
        _screen.texture = _webCamTexture;

        // ループ
        while (true)
        {
            // Webカメラからの入力を取得
            _inputTexture.SetPixels32(_webCamTexture.GetPixels32(_inputPixelData));
            var imageFrame = new ImageFrame(ImageFormat.Types.Format.Srgba, _webCamTexture.width, _webCamTexture.height, _webCamTexture.width * 4, _inputTexture.GetRawTextureData<byte>());
            var currentTimestamp = stopwatch.ElapsedTicks / (System.TimeSpan.TicksPerMillisecond / 1000);
            _graph.AddPacketToInputStream("input_video", new ImageFramePacket(imageFrame, new Timestamp(currentTimestamp))).AssertOk();

            yield return new WaitForEndOfFrame();

            // Mediapipeで座標が取得できたら
            if (poseLandmarksStream.TryGetNext(out var landmarks))
            {
                if (landmarks != null)
                {
                    // 初回だけmediapipeがポーズ推定した座標の位置にSphereを生成する。
                    if (flag)
                    {
                        int i = 0;
                        foreach (var l in landmarks.Landmark)
                        {
                            GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                            sphere.name = "sphere" + i;
                            sphere.transform.position = new Vector3(0, 0, 0);
                            sphere.transform.localScale = new Vector3(0.15f, 0.15f, 0.15f);
                            sphereList.Add(sphere);
                            sphere.SetActive(this.MarkerEnable);
                            this.originalScale = Vector3.one;
                            i++;
                        }
                        flag = false;
                    }
                    // ポーズ推定した位置にsphereを移動する。
                    int j = 0;
                    foreach (var l in landmarks.Landmark)
                    {
                        GameObject sphere = sphereList[j];
                        sphere.transform.position = transform.TransformPoint(screenRect.GetPoint(l).x / (400 + this.size), screenRect.GetPoint(l).y / (400 + this.size), screenRect.GetPoint(l).z / 8000);
                        sphere.SetActive(this.MarkerEnable);
                        j++;
                    }
                }
            }
        }
    }

    // 省略

}

作成したBodyTrace.csは、Solutionオブジェクトにアタッチします。
アタッチしたスクリプトには下記のような値を設定します。

ScreenにはRawImageを、configにはMediapipeのConfigファイルを指定します。

MediapipeはConfigファイル内に、複数のノードからなる有向グラフの形で、処理の流れを記述します。
このノードの組み合わせを使って、入力画像の前処理、推論、後処理、描画などを順番に行います。

今回はMediapipeUnityPluginのポーズトラッキング用のConfigファイルをそのまま使用しました。
https://github.com/homuler/MediaPipeUnityPlugin/blob/master/Assets/MediaPipeUnity/Samples/Scenes/Pose Tracking/pose_tracking_cpu.txt

これにより、カメラに映った身体の各部位(目や肩、肘、手など)の三次元座標が取得できます。

3. 実行結果

上記スクリプトを実行すると、下記のようにWebカメラに映った身体の上に赤いSphereオブジェクトが表示され、身体の動きに追従して動きます。

4. 3Dモデルの制御

Mediapipeで識別した身体の部位ごとの座標を利用して、Unity内に配置した3Dモデルを動かします。

下記の画像のように、Humanoid型の3DモデルをScene内に配置します。

3Dモデルには、Vroid Studioで制作したものをfbx形式に変換して利用しました。

https://vroid.com/studio

この3DモデルがMediapipeで識別した実際の人間の身体の動きに追従するように実装を行います。

例えば、3Dモデルの右上腕をMediapipeで推定した実際の人間の右上腕と同じように動かしたい場合を考えます。
下記の画像で、青い楕円を腕の3Dモデル、赤い丸をMediapipeで推定した座標とします。

基本的なアプローチとしては、Mediapipeで検出した右上腕の座標(x',y',z')と、3Dモデルの右上腕の座標(x,y,z)から方向ベクトルを求め、求めたベクトルと同じ方向に右上腕を向けるために必要な回転R2を求めます。
これを3Dモデルの右上腕が持つ初期状態の回転R1に対して掛け合わせてR3の状態にしてあげることで、Mediapipeで求めた方向と同じ方向に3Dモデルの右上腕を向けることができます。

これを実装すると下記のようになります。


// 省略

// 右上腕
// mediapipeの右上腕に割り当てられたGameObjectを取得(座標x',y',z')
GameObject rightArmSphere = sphereList[14];
// mediapipeの右上腕の座表 - 3Dモデルの右腕の位置で、方向ベクトルを求める
targetDirection = rightArmSphere.transform.position - modelRightArm.getTransform().position;
// ターゲットの方向への回転R2を求め、モデルの初期の回転R1に対して掛け合わせることで、移動後の回転R3を求める
targetRotation = Quaternion.LookRotation(targetDirection) * modelIntialRotate;
// 回転を3Dモデルに適用する
modelRightArm.getTransform().rotation = targetRotation;

// 省略

右腕だけでなく、左腕、頭、胸(Mediapipeでは胸の座標はないため左右の肩の中点から推定)などの他の部位も同様に実装します。
実装したアプリを実行すると下記のように現実の動きに同期して3Dモデルも動くことが確認できました。

5. まとめ

Mediapipeというライブラリを用いる事で、モーショントラッキングセンサーなどの特殊な機器を使用せずにWebカメラだけでモーショントラッキングを実現し、バーチャル空間上のアバターへリアルタイムに現実世界の人の動きを反映させる事ができました。

奥行きの推定精度などはモーショントラッキングセンサーを利用したものには及ばないものの、縦横方向の位置推定精度は十分な追従性を実現できており、専用機器なしでアバター操作を実現できる事は大きな強みになるかと思います。

今回はモーショントラッキング結果を画面上に表示するに留めておりますが、XR機器を接続することで、バーチャル店舗にお越し頂いたお客様に接客を行う、遠隔地のお客様に没入感や奥行き表現を生かした3Dプレゼンテーションを行う、といったアプリケーションが実現できるのではないかと考えています。
(※あくまでイメージであり、実在するシステムと関連するものではございません)

XR技術の普及にはさまざまなハードルがありますが、ここ数年で大幅に身近になりつつあります。
機器も安価になり、手軽に試せる環境も増えているので、引き続き動向をアンテナ高くウォッチしたいと思います。

なお、掲載したソースコードはサンプルになります。本ソースコードを使用することで発生するいかなる損害や不利益について、当社は一切の責任を負いませんので自己の責任においてご利用ください。

この記事を読んで、当社にご興味を持たれましたら下記よりアクセス頂ければと思います。

Discussion