こんにちは!GeminiとMediaPipeを使ったWebアプリ開発、楽しんでいますか?
前回の記事では、手の形を認識する方法を紹介しました。
今回はその続編として、全身の骨格をリアルタイムに検出する「ポーズ検出」に挑戦します。もちろん、このコードもGeminiのCanvasでそのまま動きます!
できあがるアプリのイメージ
- カメラに映った人物の骨格(ポーズ)をリアルタイムに検出します。
- 検出した骨格を、カメラ映像の上に線で描画します。
- この基本コードを応用して、体を動かして遊ぶゲームなどを作れます。
スターターコード
まずは基本となる、骨格を検出して表示するだけのシンプルなコードです。
以下のHTMLをGeminiのCanvasに貼り付けるだけで、すぐに試せます。
ソースコード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MediaPipe 姿勢検知サンプル</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
#vision-container {
position: relative;
width: 100%;
max-width: 640px;
aspect-ratio: 4 / 3;
overflow: hidden;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
#webcam, #skeletonCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
#webcam {
transform: scaleX(-1);
z-index: 0;
}
#skeletonCanvas {
z-index: 1;
}
</style>
</head>
<body class="bg-gray-100 flex flex-col items-center justify-center min-h-screen p-4">
<div class="w-full max-w-2xl text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800 mb-2">姿勢検知サンプル</h1>
<p class="text-gray-600 mb-4">MediaPipe PoseLandmarkerを使用して、Webカメラからリアルタイムに骨格を検出します。</p>
</div>
<div id="vision-container" class="bg-black">
<video id="webcam" autoplay playsinline></video>
<canvas id="skeletonCanvas"></canvas>
</div>
<div class="mt-4 text-center">
<button id="startCamButton" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg transition-transform transform hover:scale-105 disabled:bg-gray-400 disabled:cursor-not-allowed">
カメラを有効にする
</button>
<p id="loadingText" class="mt-2 text-gray-500 h-5"></p>
</div>
<script type="module">
import * as vision from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/vision_bundle.js";
const { PoseLandmarker, FilesetResolver, DrawingUtils } = vision;
const video = document.getElementById('webcam');
const skeletonCanvas = document.getElementById('skeletonCanvas');
const skeletonCtx = skeletonCanvas.getContext('2d');
const startCamButton = document.getElementById('startCamButton');
const loadingText = document.getElementById('loadingText');
let poseLandmarker;
let drawingUtils;
let lastVideoTime = -1;
document.addEventListener('DOMContentLoaded', () => {
drawingUtils = new DrawingUtils(skeletonCtx);
});
startCamButton.addEventListener('click', setupCameraAndPose);
async function setupCameraAndPose() {
startCamButton.disabled = true;
loadingText.textContent = "AIモデルを読み込んでいます...";
try {
const visionResolver = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm"
);
poseLandmarker = await PoseLandmarker.createFromOptions(visionResolver, {
baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task`,
delegate: "GPU"
},
runningMode: "VIDEO"
});
loadingText.textContent = "カメラへのアクセスを許可してください...";
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
video.addEventListener("loadeddata", () => {
loadingText.textContent = "準備完了です!";
startCamButton.classList.add('hidden');
predictWebcam();
});
} catch (error) {
console.error("初期化に失敗しました:", error);
loadingText.textContent = `エラー: ${error.message}`;
startCamButton.disabled = false;
}
}
async function predictWebcam() {
if (video.currentTime !== lastVideoTime && poseLandmarker) {
lastVideoTime = video.currentTime;
const poseResult = poseLandmarker.detectForVideo(video, performance.now());
drawPoseResults(poseResult);
}
requestAnimationFrame(predictWebcam);
}
function drawPoseResults(results) {
skeletonCanvas.width = video.videoWidth;
skeletonCanvas.height = video.videoHeight;
skeletonCtx.save();
skeletonCtx.clearRect(0, 0, skeletonCanvas.width, skeletonCanvas.height);
skeletonCtx.scale(-1, 1);
skeletonCtx.translate(-skeletonCanvas.width, 0);
if (results.landmarks && results.landmarks.length > 0) {
const landmarks = results.landmarks[0];
drawingUtils.drawConnectors(landmarks, PoseLandmarker.POSE_CONNECTIONS, { color: '#00FF00', lineWidth: 3 });
drawingUtils.drawLandmarks(landmarks, { radius: 4, color: '#FF0000' });
}
skeletonCtx.restore();
}
</script>
</body>
</html>
ポイント
- 前回使った
GestureRecognizer
の代わりにPoseLandmarker
を使います。 - 基本的な初期化の流れはハンドジェスチャー認識とほとんど同じです。
-
DrawingUtils
が、検出したランドマーク(関節点)とコネクタ(骨格線)の描画をよしなにやってくれて便利です。
応用例:体で遊ぶ!恐竜ジャンプゲーム
このポーズ検出技術を使って、「自分がジャンプすると、ゲーム内のキャラクターもジャンプする」ゲームを作ってみました。
👇 こちらのURLで実際に遊べます!
どうやって動いているの?
このゲームの裏側はとてもシンプルです。
- スターターコードと同じように、リアルタイムであなたの骨格を検出します。
- 両肩のY座標(垂直位置)を常に監視します。
- 肩の位置が急に上に動いたら、「ユーザーがジャンプした!」と判断します。
- その瞬間に、ゲーム内の恐竜をジャンプさせる命令を送ります。
たったこれだけで、自分の体をコントローラーにした新しいゲーム体験が生まれます。
Geminiへの指示例
このポーズ検出のスターターコードをベースに、Geminiにこんなふうにお願いしてみましょう。
指示例:
(上記のスターターコードを貼り付けて)このコードをベースに、ユーザーが腕を上げたら(両手首のy座標が両肩のy座標より小さくなったら)、画面に「ばんざい!」と表示する機能を追加してください。
このように、検出した骨格の座標を使えば、特定のポーズを判定するロジックを簡単に追加できます。
おわりに
MediaPipeを使えば、特別な機材なしに、Webカメラだけで身体の動きを捉えることができます。GeminiのCanvasと組み合わせることで、アイデアをすぐに形にして試せるのが本当に面白いところです。
フィットネスアプリ、ジェスチャーによるUI操作、新しい形のゲームなど、可能性は無限大です。ぜひ、このスターターコードを改造して、あなただけの「体感型Webアプリ」を作ってみてください!