3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザで動く!リアルタイム姿勢推定 AI を作ってみた (MediaPipe Tasks Vision 入門(TypeScript + React サンプル付き)

Posted at

フィットネスやリハビリ支援、モーションキャプチャなどで注目される 姿勢推定(Pose Estimation)
いまや ブラウザだけで、しかも リアルタイム に人体の関節を認識できます。
本記事では Google MediaPipe Tasks Vision を使い、Web カメラ映像から姿勢を推定するアプリを わずか数十行 で実装する方法を紹介します。

姿勢ランドマーク検出ガイド.png
引用: https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker?hl=ja

姿勢推定とは?

姿勢推定は画像や映像から 人体 33 点 のランドマーク(鼻・目・肩・肘・手首・股関節・膝・足首など)を検出し、 姿勢や動きを数値化する技術です。

例えばこのようなシーンで活用されます。

  • フィットネスフォームの自動チェック

  • ダンス/ヨガの姿勢分析

  • リハビリの動作評価

  • ゲーム/AR インターフェース

MediaPipe Tasks Vision について

MediaPipeはGoogleが提供する機械学習のマルチメディア処理フレームワークで、高精度な姿勢検出が可能です。MediaPipe Tasks Vision の JavaScript 版は内部で TensorFlow Lite(Wasm + WebGL/GPU) を利用しており、追加で TensorFlow.js を用意する必要はありません。ブラウザに読み込むのは @mediapipe/tasks-vision のみで OK です。

MediaPipe Tasks Visionを使うメリット

項目 説明
機械学習の環境構築が必要ない npm → ビルド → ブラウザで即動作
WebGL/GPU に対応 専用 GPU があれば 20〜30 fps、CPU でも 10 fps 前後
3D 座標も取得可 worldLandmarks からミリ単位の 3D 座標を取得

アプリ構成

[Webカメラ] → [MediaPipe Tasks Vision] → [ランドマーク33点から関節の座標取得] → [Canvas/WebGL 描画]

流れとしては、

  1. ブラウザでカメラを起動

  2. MediaPipe Tasks Visionでリアルタイム推定

  3. キーポイントをCanvasで描画

ライブラリインストール

まずは必要なライブラリをnpmでインストールします。
npm install @mediapipe/tasks-vision
※ TypeScript + React (Vite) プロジェクトを前提にしています

サンプルコード

以下はMediaPipe Tasks Visionを使ったReact用のコード例です。

TypeScript + React

App.tsx
import { useEffect, useRef } from "react";
import {
  FilesetResolver,
  PoseLandmarker,
  DrawingUtils,
} from "@mediapipe/tasks-vision";

export default function App() {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    let landmarker: PoseLandmarker | undefined;
    let stream: MediaStream | undefined;

    // 初期化
    const init = async () => {
      // Wasm バンドルの読み込み
      const vision = await FilesetResolver.forVisionTasks(
        "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm"
      );

      // PoseLandmarker 初期化
      landmarker = await PoseLandmarker.createFromOptions(vision, {
        baseOptions: {
          modelAssetPath:
            "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/latest/pose_landmarker_lite.task",
          delegate: "GPU", // GPU 利用。CPU しかない環境でも自動フォールバック
        },
        runningMode: "VIDEO",
        numPoses: 1,
      });

      // カメラ起動
      stream = await navigator.mediaDevices.getUserMedia({ video: true });
      if (!videoRef.current) return;
      videoRef.current.srcObject = stream;
      await videoRef.current.play();

      // 描画コンテキスト
      const canvas = canvasRef.current!;
      const ctx2d = canvas.getContext("2d")!;

      // GPU 描画を完全に使い切る場合は WebGL2 コンテキストも取得する
      // const gl = canvas.getContext("webgl2") as WebGL2RenderingContext;
      // const drawer = new DrawingUtils(ctx2d, gl);
      const drawer = new DrawingUtils(ctx2d);

      // 推定ループ
      const detect = () => {
        if (!videoRef.current || !landmarker) return;

        const ts = performance.now();
        const res = landmarker.detectForVideo(videoRef.current, ts);

        ctx2d.clearRect(0, 0, canvas.width, canvas.height);
        ctx2d.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height);

        if (res.landmarks.length) {
          const lm = res.landmarks[0];
          drawer.drawLandmarks(lm, {
            color: "red",
            lineWidth: 2,
          });
          drawer.drawConnectors(lm, PoseLandmarker.POSE_CONNECTIONS);
        }
        requestAnimationFrame(detect);
      };
      detect();
    };

    init();

    // クリーンアップ
    return () => {
      landmarker?.close();
      stream?.getTracks().forEach((t) => t.stop());
    };
  }, []);

  return (
    <div>
      <video
        ref={videoRef}
        width={640}
        height={480}
        style={{ display: "none" }}
      />
      <canvas ref={canvasRef} width={640} height={480} />
    </div>
  );
}

実装ポイント

  • delegate: "GPU" を指定すると WebGL2 が使える環境で自動的に GPU 推論

  • タイムスタンプdetectForVideo(…, **ts** ) に渡すことで内部でフレーム同期

  • worldLandmarks から 3D 座標(ミリメートル単位)が取れるので、転倒検知やスイング解析などにも応用可能

  • useEffectreturnclose()stop() を呼び出し、メモリとカメラを解放。

応用アイデア

これだけで関節位置がリアルタイムに取得できるので、少し工夫すれば色んな応用が可能です。

アイデア 実装ヒント
スクワットフォーム判定 肩・膝・足首 3 点の角度を計算し閾値チェック
ヨガポーズのスコア 各関節角度の目標値との差分を rms 誤差でスコア化
リズム運動の可視化 腕の y 座標の周期を FFT で解析し bpm を表示
3D 転倒検知 worldLandmarks の胴体 z 座標が急落したら警告

特にフィットネス分野では、アプリから動きを指導するニーズが高く、AIによる姿勢認識はUXを大きく向上させます。

パフォーマンスの目安

  • ノート PC/ハイエンドスマホ(WebGL2 + delegate:"GPU")
    1280×720 入力で おおむね 20〜30 fps

  • ミドル〜エントリースマホ(CPU フォールバック)
    640×480 入力で 10〜15 fps 程度

解像度を下げる、modelAssetPath を lite ではなく full/float16 に替える、マルチポーズ検出数を増やすなどで速度と精度のトレードオフを調整できます。

他モデルとの比較 (2025 年時点)

モデル 特徴 精度 速度
MediaPipe PoseLandmarker (BlazePose v3) 本記事の実装。Web/モバイル対応、3D 座標取得可
OpenPose マルチパーソン高精度だが重い。GPU 必須
PoseNet 軽量・レガシー。33 点ではなく 17 点

まとめ

  • MediaPipe Tasks Vision だけで ブラウザ上のリアルタイム姿勢推定が完結
  • 3D 座標まで取得できるため、フィットネス・リハビリ・VR 等の高度なアプリにも応用しやすい
  • React なら カメラ起動・推論・描画を数十行で実装可能

Web 技術と組み合わせれば、取得した姿勢データを WebSocket でサーバーに送って解析したり、ゲーム内アバターを動かしたりと、いろんな方向に展開できそうです。

参考

採用拡大中!

アシストエンジニアリングでは一緒に働くフロントエンド、バックエンドのエンジニアを募集しています!
少しでも興味ある方は、カジュアル面談からでもぜひお気軽にお話ししましょう!

お問い合わせはこちらから↓
https://official.assisteng.co.jp/contact/

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?