見出し画像

【Tech Blog】体の角度を基にしたWebAR姿勢検知

SEGA XDでは様々なプラットフォームを利用した XR の調査・検証を行っており、その中ではブラウザから実行できる WebAR の調査はもちろん、それらを活用した課題解決も行っています。
Web AR では、以前にフェイストラッキングや平面検知などを紹介させていただきましたが、今回は姿勢の検知についての内容を紹介させていただきます。

過去のWeb ARの記事:
【Tech Blog】Web AR の実現例とライブラリについて①
【Tech Blog】Web AR の実現例とライブラリについて②


姿勢検知の方法

まず、特定の姿勢を検出するといったポーズの検知についてですが、画面上の体の各パーツの位置座標を取得できたとしても、それが「どのような姿勢をしているか」という判定を行うためには工夫が必要となります。

これについては、大きく分けて2通りの実現方法があります。

  • 学習データのトレーニングセットを用意し、機械学習によりポーズそのものを判定する方法

  • 体パーツの画面上での位置座標を取得し、計算によって算出・判定する方法

ポーズそのものを判定する方法では、事前に学習データを作成する必要があり、また学習データの作成には多くのサンプルを必要とします。
一方、位置座標からの算出は既存のライブラリから提供されている学習データを用いて座標情報を取得する事が可能なため、サンプルデータを用意する必要はありません。

しかし、座標情報からの算出ロジックやポーズに一致しているか否かの判定ロジックを全てコードによって実装・管理する必要があります。

ここでは、後者の「体パーツの座標データを取得し、計算によって算出・判定する方法」について実装例と共に紹介させていただきます。


体パーツの座標データの取得と表示

体のパーツの座標データの取得は TensorFlow.js を利用する事で取得する事ができます。

tensorflow/tfjs-models (pose-detection)

ポーズ検出の学習モデルには下記の3種類があります。

・MoveNet  :  3つの中では一番新しく、17個のキーポイントを検出する高速で正確なモデル
・BlazePose  : 33個のキーポイントを検出でき、顔、手、足に追加のキーポイントを提供するモデル
・PoseNet  : 複数のポーズを検出でき、17個のキーポイントを検出するモデル

( 参考:Next-Generation Pose Detection with MoveNet and TensorFlow.js )


MoveNet での実装コード例は下記のような形になります。
まず、ライブラリを読み込みます。

<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-converter"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-webgl"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection"></script>

次に検出するための detector を作成します。
オプションは利用するモデルによって変わり、詳細はドキュメントをご確認ください。
モデルには、 MoveNet を指定します。

async createDetector() {
return await poseDetection.createDetector(
        poseDetection.SupportedModels.MoveNet,
        {
            modelType: poseDetection.movenet.modelType.MULTIPOSE_LIGHTNING,
        }
  );
}

続いて video 要素を渡し、ポーズの座標情報を検出します。

  async doDetect() {
    // 座標とスコアの取得
    var poses = await this.detector.estimatePoses(
      video,
      {maxPoses: 1, flipHorizontal: false}
    );

    // 画面へレンダリング
    this.render(poses);
  }

detector.estimatePoses を実行して得られた poses には下記のような座標とスコアが取得できます。

  [
    {
        "keypoints": [
            {
                "y": 580.7550357416126,
                "x": 576.6341362399489,
                "score": 0.40106430649757385,
                "name": "nose"
            },
            {
                "y": 453.5038921406648,
                "x": 641.1182161497595,
                "score": 0.5625677704811096,
                "name": "left_eye"
            },
            :
            : // 合計17個のキーポイントの座標が並びます
        ],
        "score": 0.5386236347258091,
    }
]

取得した座標情報を利用し、Canvas に描画します。

  render(poses) {
    this.ctx = canvas.getContext('2d');
    for (const pose of poses) {
        this.drawSkeleton(pose);
    }
  }
  drawSkeleton(keypoints) {
    this.ctx.fillStyle = 'White';
    this.ctx.strokeStyle = 'White';
    this.ctx.lineWidth = 2;

    poseDetection.util.getAdjacentPairs("MoveNet").forEach(([
      i, j
    ]) => {
      const kp1 = keypoints[i];
      const kp2 = keypoints[j];

      const score1 = kp1.score != null ? kp1.score : 1;
      const score2 = kp2.score != null ? kp2.score : 1;
      const scoreThreshold = MODEL_SCORE_THRESHOLD || 0;

      if (score1 >= scoreThreshold && score2 >= scoreThreshold) {
        this.ctx.beginPath();
        this.ctx.moveTo(kp1.x, kp1.y);
        this.ctx.lineTo(kp2.x, kp2.y);
        this.ctx.stroke();
      }
    });
  }

これを連続して行うことにより、取得した座標から画面に骨格を表示する事ができました。


座標データからの角度の算出とポーズの推定

ここから、ポーズの判定処理を追加してみます。
判定ロジックとしては、下記となっています。

1. キーポイントの座標情報をもとに、各関節(隣接するキーポイント)の角度を算出する
2. 事前に定義した判定したい正解となる各関節の角度と現在の角度の一致率を計算する
3. 取得した一致率が特定の閾値より高ければポーズの一致とみなす

角度の取得ロジック例は下記のようになります。
常に180°以下の数値として3点の座標から中心の角度を取得しています。

getFlexion(middle, bottom, top) {
    const P1 = {
      x : middle.x,
      y : middle.y,
    };
    const P2 = {
      x : bottom.x,
      y : bottom.y,
    };
    const P3 = {
      x : top.x,
      y : top.y,
    };

    let flexion = (
      Math.atan2(
        P3.y - P1.y,
        P3.x - P1.x
      )
      - Math.atan2(
        P2.y - P1.y,
        P2.x - P1.x
      )
    ) * (180 / Math.PI);
    flexion = Math.abs(flexion);
    if (flexion > 180) {
      flexion = Math.abs(flexion - 360);
    }

    return Math.floor(flexion);
  }

次に、キーポイントの座標をそれぞれ当てはめて各関節の角度を取得します。
例として左肘の角度では、左肘のキーポイントを中心とした隣接する点(左手首・左肩)の3点の座標から計算します。

// 各キーポイントの番号
const KEY_POINT_IND = {
    NOSE : 0,
    LEFT_EYE : 1,
    RIGHT_EYE : 2,
    LEFT_EAR : 3,
    RIGHT_EAR : 4,
    LEFT_SHOULDER : 5,
    RIGHT_SHOULDER : 6,
    LEFT_ELBOW : 7,
    RIGHT_ELBOW : 8,
    LEFT_WRIST : 9,
    RIGHT_WRIST : 10,
    LEFT_HIP : 11,
    RIGHT_HIP :12,
    LEFT_KNEE : 13,
    RIGHT_KNEE : 14,
    LEFT_ANKLE : 15,
    RIGHT_ANKLE : 16,
};

// 左肘の角度
pose.[KEY_POINT_IND.LEFT_ELBOW].flexion = getFlexion(
    pose.keypoints[KEY_POINT_IND.LEFT_ELBOW],   // 左肘のキーポイント
    pose.keypoints[KEY_POINT_IND.LEFT_WRIST],   // 左手首のキーポイント
    pose.keypoints[KEY_POINT_IND.LEFT_SHOULDER] // 左肩のキーポイント
);

続いて、取得した角度を事前定義していた正解のポーズ(角度)と照合します。

実際にポーズを検知しようと、ポーズによってはあまり重要ではない箇所や重視したいポイントなどもあるため、各ポイントの重みづけや、一致スコアの算出方法の調整など、多くの調整が必要となります。
簡単なサンプルは下記のようになります。

  // 一致率を取得する
  match(expects, flexionKeypoints) {
        let result = [];
        expects.forEach((expectPose, i) => {
        const rate = (flexionKeypoints.length > 0)
            ? getMatchRate(expectPose.flexions, flexionKeypoints)
            : 0;
      result[i] = rate;
    });

    return result;
  }

  getMatchRate(expectFlexions, flexionKeypoints) {
    let rate = 0;
    let poseLength = 0;
    Object.keys(expectFlexions).forEach((key) => {
      const index = KEY_POINT_IND[key];
      const poseFlexion = flexionKeypoints[index].flexion != null ? flexionKeypoints[index].flexion : 0;
      rate += 1 - (Math.abs(Math.pow(poseFlexion, 2) - Math.pow(expectFlexions[key], 2)) / Math.pow(180, 2)),
      poseLength++;
      });

    return Util.floor(Math.abs(rate / poseLength), 1000);
  }

判定するポーズは下記のようにしてみます。

MATCH_POSES = [
      {
    name : "スクワット(上)",
    flexions : {
      LEFT_SHOULDER  : 90,
      RIGHT_SHOULDER : 90,

      LEFT_ELBOW : 165,
      RIGHT_ELBOW : 165,

      LEFT_HIP : 170,
      RIGHT_HIP : 170,

      LEFT_KNEE : 175,
      RIGHT_KNEE : 175,
    }
  },
  {
    name : "スクワット(下)",
    flexions : {
      LEFT_SHOULDER  : 90,
      RIGHT_SHOULDER : 90,

      LEFT_ELBOW : 170,
      RIGHT_ELBOW : 170,

      LEFT_HIP : 100,
      RIGHT_HIP : 100,

      LEFT_KNEE : 92,
      RIGHT_KNEE : 92,
    }
  },
]

判定処理を追加します。

  pose.match = FlexionsMatcher.match(MATCH_POSES, pose);

角度の情報と、一致率の情報を追加した結果は下記のようなイメージになります。

 keypoints: Array(17)
    0: {y: 89.65358887759369, x: 148.89799009242412, score: 0.6714035868644714, name: 'nose'}
    1: {y: 75.8034639511876, x: 159.05252890778598, score: 0.7091502547264099, name: 'left_eye'}
    2: {y: 77.77038233865294, x: 136.0596835926852, score: 0.790132999420166, name: 'right_eye'}
    3: {y: 79.31350661412354, x: 171.94136989819918, score: 0.8024827837944031, name: 'left_ear'}
    4: {y: 84.99224191734673, x: 117.34430113426059, score: 0.7871670722961426, name: 'right_ear'}
    5: {y: 125.94701783754157, x: 202.13171920119663, score: 0.6451787352561951, name: 'left_shoulder', flexion: 106}
    6: {y: 130.05192044566238, x: 93.7619178574428, score: 0.5995876789093018, name: 'right_shoulder', flexion: 101}
    7: {y: 107.96835932889775, x: 279.94250683689074, score: 0.6093297004699707, name: 'left_elbow', flexion: 104}
    8: {y: 126.09920917266585, x: 40.16119674159055, score: 0.502634584903717, name: 'right_elbow', flexion: 108}
    9: {y: 24.873621879746388, x: 281.70742586580997, score: 0.43484848737716675, name: 'left_wrist'}
    10: {y: 47.83638154114726, x: 20.398188321898203, score: 0.2764095067977905, name: 'right_wrist'}
    11: {y: 249.59281973731316, x: 193.78266836041263, score: 0.6038393974304199, name: 'left_hip', flexion: 58}
    12: {y: 255.2247652873008, x: 108.96092715095301, score: 0.39512649178504944, name: 'right_hip', flexion: 0}
    13: {y: 225.20651703128644, x: 240.2936070333966, score: 0.04032120853662491, name: 'left_knee', flexion: 25}
    14: {y: 199.01015054955312, x: 101.46669316406037, score: 0.013594210147857666, name: 'right_knee', flexion: 57}
    15: {y: 228.10841642252592, x: 175.42741586043815, score: 0.012550413608551025, name: 'left_ankle'}
    16: {y: 228.5490663630536, x: 165.53047947141897, score: 0.013443450443446636, name: 'right_ankle'}
    length: 17
match: Array(4)
    0: 0.348 // "スクワット(上)"の一致率
    1: 0.702 // "スクワット(下)"の一致率
score: 0.6020993292331696

※動画ファイル:

ポーズの一致率を算出することが出来ました。体パーツの認識の精度自体は良いため、ポーズの一致率の精度を高めるには算出するロジックが重要となってきます。


まとめ

WebAR での体の位置座標からアプリケーション側で角度を算出し、姿勢を検知する方法を紹介させていただきました。

姿勢の検知は、エンタテインメントだけではなくスポーツやヘルスケアの分野などでも活用され、アプリだけではなくブラウザ上での表示についても需要が高まっています。
機械学習におけるデバッグや調整の難しさや動作環境の制約など、まだまだ課題が多くあるかと思いますが、同時に今後大きく発展・活用していける分野でもあります。

セガエックスディーでは幅広い技術を組み合わせ、今まで培ったゲーミフィケーション要素などを活かして今後も面白いものを生み出し課題解決に役立てていきたいと思います。

プロモーションの相談や新しいアイデア、「こんな事はできないか?」と言った質問もぜひお問い合わせください。


執筆:藤木 直希|株式会社セガ エックスディー

セガ エックスディーで XR 系の R & D やプロダクトの開発など、様々な開発・推進に取り組んでいます。


■ SEGA XD HP:https://segaxd.co.jp/
■ SEGA XD 公式 Twitter:https://twitter.com/SEGAXD_PR
■ SEGA XD 公式 Facebook:https://www.facebook.com/segaxd.fb/