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

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


![姿勢ランドマーク検出ガイド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3843712/fa5bb5aa-d75c-49c9-a525-c04828c4d15a.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

```tsx: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 座標（ミリメートル単位）が取れるので、転倒検知やスイング解析などにも応用可能
    
-   `useEffect` の **return** で `close()` と `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://www.npmjs.com/package/%40mediapipe/tasks-vision?utm_source=chatgpt.com
- https://ai.google.dev/edge/api/mediapipe/js/tasks-vision.poselandmarker?utm_source=chatgpt.com
- https://ai.google.dev/edge/mediapipe/solutions/vision/gesture_recognizer/web_js?hl=ja
- https://ai.google.dev/edge/mediapipe/solutions/vision/pose_landmarker?hl=ja


## **採用拡大中！**

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

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