【MoveNet/第3回前半戦】

MoveNet

第3回ではいよいよMoveNetを組み込む作業を進めます。

前回から2段階程度難易度が上がるので、お覚悟を。といっても、できる限り詳しく解説するので身構える必要はない…ハズ…

外部スクリプトを知る

第2回では、HTMLファイルにJavaScriptを記述する方法を取りました。コード作成が楽な反面、推奨はされていません。それはなぜでしょうか。

答えは前回でも取り上げた、可読性と再利用性を低下させるからです。しかもそれだけでなく、大規模開発ではコード読み込み順序やセキュリティで問題を引き起こすために忌避されています。

※セキュリティ問題の例

安全なウェブサイトの作り方 - 1.5 クロスサイト・スクリプティング | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構
情報処理推進機構(IPA)の「安全なウェブサイトの作り方 - 1.5 クロスサイト・スクリプティング」に関する情報です。

初学者の段階で上記のことを気にする必要があるかと言われれば、そんなことはないです。今回はこういうことより、作業を楽にする面について言及しようと思います。

今回は前回の課題②のコードを使って、外部スクリプトを扱う体験をしましょう。

ここから「ディレクトリ」「相対パス」と呼ばれる概念が出てくるので、もし不安であれば以下のサイトを確認してから作業に当たりましょう。

https://wa3.i-3-i.info/word199.html
相対パスとは?絶対パスとの違いやメリット・デメリット、使い分け方を解説
パソコン内やWeb上のディレクトリ構造の中で、あるファイルから別のファイルを指定する時に、「パス」を使います。パスの表記方法には絶対パスと相対パスがあり、それぞれ特徴が異なります。この記事では、それぞれの特徴や使い分け方と、もう1つの方法で...

外部スクリプトを作成する

まずはHTMLファイルと同じディレクトリにフォルダを作成しましょう。

サイドバーで「右クリック>新しいフォルダー」、フォルダ名は「js」としましょう。

うまくいくと、こんな感じになります。最上部に「js」ができています。

アイコンは拡張機能によるもの

ここからさらに「js」の上で「右クリック>新しいファイル」で「script.js」を作成します。

うまくいくとjs直下にファイルができる

そうしたら、script.jsのタブを画面右へドラッグアンドドロップしましょう。

うまくいくと前回のコードとの分割画面になると思います。

そうしたら、scriptタグで囲まれているJavaScriptコードをすべてコピーしましょう。

コピーが完了したら、コピー元の方は消して、以下のコードに書き換えましょう。

<script src="./js/script.js"></script> <!-- 外部スクリプト読み込み -->

書き換えた後も同じ動作をすれば、成功です。

JavaScriptライブラリを知る

当たり前の話ですが、何かソフトを作るときに自分の記述したコードのみで作ること(フルスクラッチと呼ばれる行為)は超大変です。

そこで、他者が開発したコード(ライブラリ)をインストールして使うことで作業効率を高めることができます。

下記はライブラリを使用したコードで、カラーコード時計です。海外ニキが作ったものになります。ちなみに、「CodePen」と呼ばれるサービスを使用しています。サイト埋め込みに便利ですし、プロトタイプ作成に便利なので知っておいて損はないですよ!

See the Pen Color Clock by Ellgine (@guardian) on CodePen.

下記がカラーコード時計で使われているライブラリになります。jqueryジェイクエリと呼ばれる有名ライブラリです。最近はReactリアクトVueビューの存在から下火ですが、今でも多くのサイトで使われています。

下の方は外部読み込みしているJS/CSSになります。今回はGoogleからフォントを取得しています。参考までにどうぞ。

<!-- 外部スクリプトをCDNから読み込み -->
<script src="https://code.jquery.com/jquery-latest.js"/></script>

<!-- 外部スタイル(CSS)読み込み -->
<link href='https://fonts.googleapis.com/css?family=Lato:100,300' rel='stylesheet' type='text/css'>

これから扱うMoveNetも例外ではなく、ライブラリが存在するのでバンバン使っていきましょう。

サイトにMoveNetを組み込む

第3回でいよいよ組み込みが始まります。お待たせしました。ここから作業が込み入ってくるので、注意深く確認してください。

まずは「MoveNet」というフォルダを作成しましょう。ここが作業場になります。そしてその配下に「css」「js」「index.html」を作成します。

さらに、「css」の配下に「styles.css」、「js」の配下に「script.js」を作成します。構造は下記の画像の通りです。

こんな感じ

そしたらそれぞれのファイルのコードを記述していきます。今回はコピペして動作確認をしていく形にします。

index.html

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- TensorFlow.jsとMoveNetモデルのスクリプトを読み込む -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/pose-detection"></script>
    <!-- タイトル -->
    <title>MoveNetデモ</title>
    <!-- 自作CSS -->
    <link href="./css/styles.css" rel="stylesheet" type="text/css">
</head>

<body>
    <h1>MoveNetデモ</h1>

    <table border="1">
        <tr>
            <td>
                <!-- オーバーレイ用のコンテナ -->
                <div id="overlayContainer">
                    <!-- 動画プレーヤー要素 -->
                    <video id="videoPlayer" width="1920" height="1080"></video>
                    <!-- オーバーレイ用のキャンバス -->
                    <canvas id="overlayCanvas" width="1920" height="1080"></canvas>
                </div>
            </td>
        </tr>
    </table>

    <!-- 自作JavaScrip -->
    <script src="./js/script.js"></script>
</body>

</html>

styles.css

/* 動画とキャンバスを重ねるためのスタイル */
#overlayContainer {
    position: relative;
}

/* 動画のスタイル */
#videoPlayer {
    width: auto;
    height: 80vh;
}

/* キャンバスのスタイル */
#overlayCanvas {
    position: absolute;
    top: 0;
    left: 0;
    width: auto;
    height: 80vh;
}

script.js

let videoPlayer;
let detector;
let overlayCanvas;
let overlayCtx;

// MoveNetモデルを読み込む関数
async function loadMoveNet() {
    try {
        await tf.setBackend('webgl');
        detector = await poseDetection.createDetector(poseDetection.SupportedModels.MoveNet);
    } catch (error) {
        console.error('MoveNet モデルの読み込みに失敗しました:', error);
    }
}

// ビデオからポーズを推定する関数
async function estimatePoses(video) {
    try {
        return await detector.estimatePoses(video);
    } catch (error) {
        console.error('姿勢推定に失敗しました:', error);
        return [];
    }
}

// カメラの読み込みを行う関数
async function loadCamera() {
    const constraints = {
        video: {
            width: 1980,
            height: 1080,
            aspectRatio: 1.77
        }
    };
    try {
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        videoPlayer.srcObject = stream;
    } catch (error) {
        console.error('カメラのアクセスに失敗:', error);
    }
}

// 選択した動画を再生し、ポーズ検出を開始する関数
async function playSelectedVideo() {
    await videoPlayer.play();
    detect();
}

// キーポイントを描画する関数(オーバーレイ用)
function drawKeypoints(keypoints, context) {
    keypoints.forEach((keypoint) => {

        const color = `rgba(255, 0, 0, ${keypoint.score})`;

        // 円の描画
        context.beginPath();
        context.arc(keypoint.x, keypoint.y, 5, 0, 2 * Math.PI);
        context.fillStyle = color;
        context.fill();
        context.closePath();
    });
}

// ボディラインを描画する関数(オーバーレイ用)
function drawBodyLines(keypoints, context) {
    context.strokeStyle = 'rgba(0, 255, 0, 0.8)';
    context.lineWidth = 4;

    // 人体のラインを定義
    const bodyLines = [
        [3, 1], [1, 0], [0, 2], [2, 4], // 顔
        [9, 7], [7, 5], //左腕
        [10, 8], [8, 6], //右腕
        [6, 5], [5, 11], [11, 12], [12, 6],//胴体
        [12, 14], [14, 16], [11, 13], [13, 15],//脚
    ];

    bodyLines.forEach((line) => {
        const start = keypoints[line[0]];
        const end = keypoints[line[1]];

        context.beginPath();
        context.moveTo(start.x, start.y);
        context.lineTo(end.x, end.y);
        context.stroke();
        context.closePath();
    });
}

// ポーズを検出し続ける関数
async function detect() {
    try {
        if (!videoPlayer.paused && !videoPlayer.ended) {
            const poses = await estimatePoses(videoPlayer);
            if (poses.length > 0) {
                // オーバーレイ用のキャンバスをクリアしてキーポイントを描画
                overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
                poses.forEach((pose) => {
                    drawKeypoints(pose.keypoints, overlayCtx);
                    drawBodyLines(pose.keypoints, overlayCtx);
                });
            }
        }
    } catch (error) {
        console.error('姿勢推定に失敗しました:', error);
    }
    requestAnimationFrame(detect);
}

// ページの読み込みが完了したらMoveNetモデルを読み込み、要素を取得する
window.onload = async function () {
    await loadMoveNet();
    videoPlayer = document.getElementById('videoPlayer');
    overlayCanvas = document.getElementById('overlayCanvas');
    overlayCtx = overlayCanvas.getContext('2d');

    // カメラ映像の読み込み
    await loadCamera();

    videoPlayer.addEventListener('loadedmetadata', function () {
        playSelectedVideo();
    });
};

「http://127.0.0.1/MoveNet/」にアクセスして、動作を確認してみましょう。カメラ必須な点に注意。

うまくいくと、自分の体の上にキーポイントが表示されているはずです。

とりあえず前半戦クリアです。お疲れ様でした。次からコード解説と、いろいろコードを書き換えていく作業をやっていきましょう。

後半戦に進む