第5回です。このあたりから発表用に自分が作るゲームのイメージをしておくといいかも?
キーポイントの座標を取得
前回旗揚げゲームの出題と旗の状態管理について取り扱ったので、今回はMoveNetで手を挙げたかの判定をする仕組みを作ります。
MoveNetでキーポイントを使った条件分岐処理をするためには、キーポイントをが出力する値を理解する必要があります。
まずは第3回前半戦で使ったコードを用意します。
そのコードを少し書き換えます。
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) {
console.log(keypoints); //ここを追記
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();
});
};
drawKeypoints()の直後にconsole.log(keypoints);を追記しました。
コードを実行してコンソールを見てみると……なにやらたくさんの数字がありますね。
一つ選んで配列をクリックすると見ることができます。
さらにさらに展開すると…詳細な表示になりました!
各項目について表にまとめるとこうなります。
name | 検出部位の名前 |
score | 検出結果の信頼度 |
x | 検出部位のx座標 |
y | 検出部位のy座標 |
nameの一覧はこちらです。多分この表にはかなりお世話になるはず。
配列の要素 | キーポイント名 | 部位 |
---|---|---|
0 | nose | 鼻 |
1 | left_eye | 左目 |
2 | right_eye | 右目 |
3 | left_ear | 左耳 |
4 | right_ear | 右耳 |
5 | left_shoulder | 左肩 |
6 | right_shoulder | 右肩 |
7 | left_elbow | 左肘 |
8 | right_elbow | 右肘 |
9 | left_wrist | 左手首 |
10 | right_wrist | 右手首 |
11 | left_hip | 左腰 |
12 | right_hip | 右腰 |
13 | left_knee | 左膝 |
14 | right_knee | 右膝 |
15 | left_ankle | 左足首 |
16 | right_ankle | 右足首 |
scoreですが、1に近いほど信頼度が高い(=その部位である確率が高い)です。
座標ですが1920*1080のcanvasの場合、図に表すとこのようになります。左上が原点になることに注意。
試しに下記のコードをコピーしてみてください。鼻の座標をコンソールに表示し続けます。映像の四隅に鼻を移動させると、座標の感覚がつかみやすいです。
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) => {
if(keypoint.name = 'nose'){
console.log("nose:" + keypoint.x + "," + keypoint.y);
}
});
}
// ポーズを検出し続ける関数
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);
});
}
}
} 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();
});
};
MoveNetで手を挙げたか判定したい
ここまでの解説を読んだ方は、もうできるはずです。ということで課題にします。
課題
- 両手の座標をコンソールに表示せよ
- 【ちょい難問】手の位置が画面上半分にある場合、挙げた手の肩に任意の画像を表示せよ
- 例)左手が画面上半分に行く⇒左肩に画像が表示される
サンプルコードの一部を書き換えるだけで実現できます。
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) => {
if (keypoint.name = 'right_wrist') {
console.log("右手:" + keypoint.x + "," + keypoint.y);
}
if (keypoint.name = 'left_wrist') {
console.log("左手:" + keypoint.x + "," + keypoint.y);
}
});
}
// ポーズを検出し続ける関数
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);
});
}
}
} 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();
});
};
ここではpose.keypoints.forEach((keypoint) => {});を削除し、pose.keypoints[10].yのような方法で座標を取得しています。こうすることで、手首の座標をもとに、肩に対して何かをする、という処理をしやすくしています。
let videoPlayer;
let detector;
let overlayCanvas;
let overlayCtx;
// MoveNetモデルを読み込む関数
// 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 {
const poses = await detector.estimatePoses(video);
return poses;
} 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(poses, context) {
poses.forEach((pose) => {
if (pose.keypoints[10].y <= 540) {
drawImage(pose.keypoints[6].x, pose.keypoints[6].y, context);
}
if (pose.keypoints[9].y <= 540) {
drawImage(pose.keypoints[5].x, pose.keypoints[5].y, context);
}
});
}
// 追従する画像を描画する関数
function drawImage(x, y, context) {
if (image.complete) {
const imageWidth = 400;
const imageHeight = 400;
context.drawImage(image, x - imageWidth / 2, y - imageHeight / 2, imageWidth, imageHeight);
}
}
// 画像をロードする関数
function loadfaceImage() {
image = new Image();
image.src = '任意の画像パス';
}
// ポーズを検出し続ける関数
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);
drawKeypoints(poses, 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');
loadfaceImage(); // 追従する画像をロードする
// カメラ映像の読み込み
await loadCamera();
videoPlayer.addEventListener('loadedmetadata', function () {
playSelectedVideo();
});
};
まとめ
今回はMoveNetで座標を拾う方法をやりました。前回と比べると楽だったかも?
これで正誤判定の際、「旗の状態」と「手首の位置」を比較して評価できるようになります。
次回は大詰めである、コードの合体とゲーム全体の処理をやっていきます。お楽しみに。
それでは。