【Hands/第3回】

Hands

今回は手に表示している画像を手の角度に合わせて回転させてみたいと思います。

前回行った画像の表示方法を知っておく必要があるので、覚えていない人は確認しましょう。

確認

ctx.drawImage(Img, canvas.width-(centerX+magnification/2), centerY-magnification / 2, magnification, magnification); // 画像を中心に描画

画像をキャンバスに表示しています。表示するためにはdrawImageを使います。それぞれの意味は(表示する画像, 画像のx座標の始点, 画像のy座標の始点, 画像の横幅, 画像の縦幅)となっています。映像が反転しているのでキャンバスの横幅から値を引いて表示させています。

手の角度に合わせて画像を回転

今回は「Hands-3」というフォルダを作成しましょう。ここが作業場になります。flag_red.pngをインストールしてHands-3に入れます。右クリックをして名前を付けて画像を保存してください。名前は変えないでください。その後、Hands-3に「index.html」「script.js」を作成します。

次にそれぞれのファイルのコードを記述していきます。下にあるコードをコピペして動作確認をします。

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <!-- 3つのライブラリを読み込む -->
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>

    <style>
        /*ビデオとキャンバスを重ねるためのスタイル*/
        #input, #output {
            position: absolute;
            top: 0;
            left: 0;
        }
        #input {
            z-index: 1;
            transform: scaleX(-1);
        }
        #output {
            z-index: 2;
        }
    </style>
</head>

<body>
    <div class="container">
        <!-- Webカメラの映像(入力) -->
        <video id="input"autoplay"></video>
        <!-- 認識した手の形状を可視化した映像(出力) -->
        <canvas id="output" width="600" height="400"></canvas>
    </div>
    <script src="script.js"></script>
</body>

</html>

script.js

const video = document.getElementById('input');
  const canvas = document.getElementById('output');
  const ctx = canvas.getContext('2d');
  
  const Img = new Image();
  Img.src = "flag_red.png";
  
  const config = {
      locateFile: file => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
  };
  
  const hands = new Hands(config);
  
  const camera = new Camera(video, {
      onFrame: async () => {
          await hands.send({ image: video });
      },
      width: 600,
      height: 400
  });
  
  camera.start();
  
  hands.setOptions({
      maxNumHands: 5,
      modelComplexity: 1,
      minDetectionConfidence: 0.5,
      minTrackingConfidence: 0.5
  });
  
  hands.onResults(results => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      if (results.multiHandLandmarks) {
          let i=0;
          results.multiHandLandmarks.forEach(marks => {
              const wrist = marks[0]; // 手首
              const indexFingerMCP = marks[5]; // 人差し指の中手骨
              const pinkyMCP = marks[17]; // 小指の中手骨
              const handedness = results.multiHandedness[i].label;
              if (wrist && indexFingerMCP && pinkyMCP) {
                  imgWrite(wrist, indexFingerMCP, pinkyMCP, handedness);  //手の位置に合わせて画像を表示する
              }
              i++;
          })
      }
  });
  
  function imgWrite(wrist, indexFingerMCP, pinkyMCP, handedness) {
      const wrists = [wrist.x * canvas.width, wrist.y * canvas.height];
      const indexFingerMCPs = [indexFingerMCP.x * canvas.width, indexFingerMCP.y * canvas.height];
      const pinkyMCPs = [pinkyMCP.x * canvas.width, pinkyMCP.y * canvas.height];
      const centerX = (indexFingerMCPs[0] + wrists[0]) / 2;
      const centerY = (indexFingerMCPs[1] + wrists[1]) / 2;
      const magnification = (Math.sqrt((indexFingerMCPs[0] - pinkyMCPs[0]) ** 2 + (indexFingerMCPs[1] - pinkyMCPs[1]) ** 2)) * 2;
  
      let angle;
      // 角度の計算
      if (handedness === "Left") {
          angle = Math.atan2(indexFingerMCPs[1] - pinkyMCPs[1], indexFingerMCPs[0] - pinkyMCPs[0]);
      } else {
          angle = Math.atan2(pinkyMCPs[1] - indexFingerMCPs[1], pinkyMCPs[0] - indexFingerMCPs[0]);
      }
  
      // キャンバスの描画
      ctx.save();
      ctx.translate(canvas.width - centerX, centerY);
      ctx.rotate(-angle);
      ctx.drawImage(Img, -magnification / 2, -magnification / 2, magnification, magnification);
      ctx.restore();
  }

index.htmlを開いてください。上手くいくと手に画像が表示され、手を回転させるとそれに合わせて画像も回転します。

コード解説

const handedness = results.multiHandedness[i].label;

results.multiHandedness[i].labelでカメラに写っている手が右手か左手の情報を取得できます。値はLeftかRightです。

let angle;
if (handedness === "Left") {
  angle = Math.atan2(indexFingerMCPs[1] - pinkyMCPs[1], indexFingerMCPs[0] - pinkyMCPs[0]);
} else {
  angle = Math.atan2(pinkyMCPs[1] - indexFingerMCPs[1], pinkyMCPs[0] - indexFingerMCPs[0]);
}

上のコードで手の角度を計算しています。Math.atan2の使い方は(y, x)です。今回は2点の角度を計算するので(y2 – y, x2 – x)で計算しています。角度(ラジアン)の値を取得します。

const wrists = [wrist.x * canvas.width, wrist.y * canvas.height];
const indexFingerMCPs = [indexFingerMCP.x * canvas.width, indexFingerMCP.y * canvas.height];
const pinkyMCPs = [pinkyMCP.x * canvas.width, pinkyMCP.y * canvas.height];

imgWriteファンクションでは手に画像を表示するための処理を行っています。上のコードでキャンバスに表示できるようにするために、それぞれの座標にキャンバスのサイズをかけています。canvas.widthはキャンバスの横幅です。canvas.heightはキャンバスの縦幅です。その後、配列に入れて管理します。

ctx.save();
ctx.translate(canvas.width - centerX, centerY);
ctx.rotate(-angle);
ctx.drawImage(Img, -magnification / 2, -magnification / 2, magnification, magnification);
ctx.restore();

上のコードで画像を表示しています。saveは今のキャンバスの状態を保存します。translateでキャンバスの位置を変更しています。rotateでキャンバスを回転させています。画像を回転させることはできないので、キャンバスを回転させています。restoreでキャンバスを保存した時の状態に戻します。

課題

今回の課題は右手が写っているか左手が写っているかによってコンソールに表示してみましょう。

  • どの手が写っているかによってコンソールに表示しなさい。
    右手が写っている場合「マスターハンド」
    左手が写っている場合「クレイジーハンド」
  1. 値はLeftかRightです。
  2. handednessの中を確認しましたか。

handednessを参照しましょう。Leftが右手でRightが左手です。

if (handedness === "Left") {
  console.log("マスターハンド");
} else {
  console.log("クレイジーハンド");
}