手の骨格が取得できたので、今度は手の位置を使って音楽のボリュームとスピードを制御してみましょう。
下図のように手の骨格を取得する画像領域を灰色の四角で表します。緑の点は手の外接四角形の中心とします。この中心位置により音楽のボリュームやスピードを制御します。手を左右に動かすと音楽のスピードを変化させ、手を上下に動かすと音楽のボリュームを変化させます。
下図の座標系を左上の頂点を原点(0,0)とし、右方向にx軸、下方向にy軸を取ります。画像領域の中心線を緑色でx軸、y軸に平行に引く。またそこからx軸方向にそれぞれdwだけの緩衝領域を設け、この領域内ではスピードを1.0とすることとする。またy軸方向にそれぞれdhだけの緩衝領域を設け、この領域内ではボリュームを0.6(標準ボリューム)とする。
緩衝領域を超えた位置に手が来るとそれに応じてボリュームやスピードを変化する。スピードについては緩衝領域より右側に来た場合、すなわち x > vw/2+dw となればスピードを1から2まで変化させます。x = vw/2+dw でスピードを1、x = vw でスピードを2とするように線形補間すればいいわけです。また左側の場合、すなわち x < vw/2-dw となればスピードを1から0.5まで変化させます。
下記にdw=40の場合のグラフを示します。
●このグラフのMathematicaのプログラム
dw = 30;
f[x_] := Piecewise[{{x/(320 - dw), x < (320 - dw)}, {1.0, x < (320 + dw)}}, 1 + (x - 320 - dw)/(640 - 320 - dw)]
Plot[f[x], {x, 0, 640}, GridLines -> Automatic, AxesLabel -> {x, speed}]
ボリュームについても同様に緩衝領域より上側に来た場合、すなわち y < vh/2-dh となればボリュームを0.6から1.0まで変化させます。y = vh/2-dh でボリュームを0.6、y = 0 でボリュームを1.0とするように線形補間します。また下側の場合、すなわち y > vh/2+dh となればボリュームを0.6から0.0まで変化させます。
下記にdh=40の場合のグラフを示します。
●このグラフのMathematicaのプログラム
dh = 30;
f[y_] := Piecewise[{{(240 - dh - y)/(240 - dh)*0.4 + 0.6, y < 240 - dh}, {0.6, y < 240 + dh}}, 0.6 - (y - 240 - dh)/(480 - 240 - dh)*0.6]
Plot[f[y], {y, 0, 480}, GridLines -> Automatic, AxesLabel -> {y, volume}]
//HandPose detection and song control
//手の骨格検出と音楽の制御
//Copyright (C) Oz all rights reserved.
//rev 0.1 2022/12/20
// https://learn.ml5js.org/#/reference/handpose
let handpose; //手の骨格用変数
let video; //ビデオ用変数
let flippedVideo; //上のvideoを左右反転させた動画を表す変数
let predictions = []; //手の骨格の配列(2つ以上の手を検出する場合のため)
let ftipSize = 6;
let ftip = new Array(ftipSize); //各指先を保存するための配列
let vw = 640; //ビデオサイズ 幅
let vh = 480; //ビデオサイズ 高さ
let minX, minY, maxX, maxY; //外接四角形の頂点の座標値
let minTX, minTY, maxTX, maxTY; //指先と手首の外接四角形の頂点の座標値
let song; //音楽用変数
let dw = 30; //緩衝帯の1/2幅
let dh = 30; //緩衝帯の1/2高さ
let cx, cy; //外接四角形の中心座標x、y
const options = {
flipHorizontal: true, // boolean value for if the video should be flipped, defaults to false
maxContinuousChecks: Infinity, // How many frames to go without running the bounding box detector. Defaults to infinity, but try a lower value if the detector is consistently producing bad predictions.
detectionConfidence: 0.5, // Threshold for discarding a prediction. Defaults to 0.8.
scoreThreshold: 0.75, // A threshold for removing multiple (likely duplicate) detections based on a "non-maximum suppression" algorithm. Defaults to 0.75
iouThreshold: 0.3, // A float representing the threshold for deciding whether boxes overlap too much in non-maximum suppression. Must be between [0, 1]. Defaults to 0.3.
}
function preload() {
song = loadSound('party kaimaku1.mp3'); //画像の読み込み
}
function setup() {
createCanvas(vw * 2, vh); //Canvasサイズの指定
video = createCapture(VIDEO); //カメラ入力をvideoという変数に代入
video.size(vw, vh); //ビデオのサイズを指定
for (let i = 0; i < ftipSize; i++) {
ftip[i] = new Array(2); // 2個x,y
}
flippedVideo = ml5.flipImage(video); //ビデオを左右反転
handpose = ml5.handpose(video, options, modelReady); //手の骨格検出のための準備
// This sets up an event that fills the global variable "predictions"
// with an array every time new hand poses are detected
handpose.on("predict", results => { //手の骨格検出をスタート
predictions = results; //結果をpredictionsという変数に入れる
});
// Hide the video element, and just show the canvas
video.hide(); //ビデオの表示を隠す
strokeWeight(5); //線の幅の指定
}
function modelReady() {
console.log("Model ready!"); //手の骨格検出の準備ができたらModel ready!と表示する
}
let drawCount = 0; //draw()の呼ばれた回数のカウント
function draw() {
background(255); //背景を白に設定
flippedVideo = ml5.flipImage(video); //ビデオの左右反転処理
image(flippedVideo, 0, 0, vw, vh); //ビデオを(0,0)の位置にwidth幅、height高さで表示
//緩衝領域に線を引く
strokeWeight(1); //線幅を1に設定
stroke(255, 255, 0); //黄色を指定
line(vw / 2 - dw, 0, vw / 2 - dw, vh); //左緩衝領域境界
line(vw / 2 + dw, 0, vw / 2 + dw, vh); //右緩衝領域境界
line(0, vh / 2 - dw, vw, vh / 2 - dw); //上緩衝領域境界
line(0, vh / 2 + dw, vw, vh / 2 + dw); //下緩衝領域境界
// We can call both functions to draw all keypoints and the skeletons
strokeWeight(5); //線幅を5に設定
drawKeypoints(); //keypoints(関節位置およびリンク)を描く
drawCount++; //draw()の呼ばれた回数のカウントアップ
if (!song.isPlaying()) { //曲が流れていなければ、曲を流す
song.play();
}
}
// A function to draw ellipses over the detected keypoints
function drawKeypoints() {
for (let i = 0; i < predictions.length; i += 1) { //predictions.length:検出した手の数(今のところ1のみ)
const prediction = predictions[i]; //predictionという変数にredictionsのi番目を入れる(今のところ0のみ)
let pKeypoint;
minX = vw; //外接四角形のxの最小値の初期値(最大にしておく)
minY = vh; //外接四角形のyの最小値の初期値(最大にしておく)
maxX = maxY = 0; //外接四角形の最大値の初期値(最小にしておく)
minTX = vw; //指外接四角形のxの最小値の初期値(最大にしておく)
minTY = vh; //指外接四角形のyの最小値の初期値(最大にしておく)
maxTX = maxTY = 0; //指外接四角形の最大値の初期値(最小にしておく)
// for (let j = 4; j < 5; j += 1) {
for (let j = 0; j < prediction.landmarks.length; j += 1) { //0から関節の数(今は20)分ループを回す
const keypoint = prediction.landmarks[j]; //各関節の位置(x、y)座標値をkeypointに入れる
if (minX > keypoint[0]) minX = keypoint[0]; //関節位置のx座標の最小値を計算
if (maxX < keypoint[0]) maxX = keypoint[0]; //関節位置のx座標の最大値を計算
if (minY > keypoint[1]) minY = keypoint[1]; //関節位置のy座標の最小値を計算
if (maxY < keypoint[1]) maxY = keypoint[1]; //関節位置のy座標の最大値を計算
//keypoint[0]:keypointのx座標値、keypoint[1]:y座標値
if (j > 0 && (j != 5 && j != 9 && j != 13 && j != 17)) { //リンクを描く関節間のみ扱うため、0番、5,9,13,17番(手首と指先)は描かないようにする。
stroke(255, 255, 0); //リンクの色を黄色とする
line(pKeypoint[0], pKeypoint[1], keypoint[0], keypoint[1]); //関節間を線で結ぶ
}
noStroke();
fill(0, 255, 0); //fillを緑色にする
ellipse(keypoint[0], keypoint[1], 10, 10); //keypointの位置に直径10の円を描く
pKeypoint = keypoint; //現在のkeypointをpKeypointとして保存し、次回関節間を線で結ぶ時の1つ前のkeypointとして使う
}
//外接四角形の面積の計算
let sr = (maxX - minX) * (maxY - minY);
//外接四角形の中心座標の計算
cx = (maxX + minX) / 2;
cy = (maxY + minY) / 2;
fill(255, 0, 0);
circle(cx, cy, 15);
[rate, volume] = calculateRateVol(cx, cy);
song.rate(rate); //曲のrateを指定
song.setVolume(volume); //曲のvolumeを指定
count = 0; //指先位置のカウント
for (let i = 0; i <= 20; i += 4) { //手首が0、指先は4, 8, 12, 16, 20番目なのでそれらの(x、y)座標値をftipに保存する
ftip[count][0] = prediction.landmarks[i][0]; //手首および指先位置のx座標値
ftip[count][1] = prediction.landmarks[i][1]; //手首および指先位置のy座標値
if (minTX > ftip[count][0]) minTX = ftip[count][0]; //指先位置のx座標の最小値を計算
if (maxTX < ftip[count][0]) maxTX = ftip[count][0]; //指先位置のx座標の最大値を計算
if (minTY > ftip[count][1]) minTY = ftip[count][1]; //指先位置のy座標の最小値を計算
if (maxTY < ftip[count][1]) maxTY = ftip[count][1]; //指先位置のy座標の最大値を計算
count++;
}
//指の外接四角形の面積の計算
let srTip = (maxTX - minTX) * (maxTY - minTY);
//外接四角形の描画
noFill(); //着色なし
stroke(0, 255, 0); //緑色の線
rect(minX, minY, maxX - minX, maxY - minY); //全体の外接四角形
stroke(0, 0, 255); //青色の線
rect(minTX, minTY, maxTX - minTX, maxTY - minTY); //指先の外接四角形
//srTip/srが1に近ければ手を開いている、0に近ければ手を閉じている
if (srTip / sr > 0.95) song.stop(); //手が開いていたら曲を停止
}
}
//rate, volume calculation function
function calculateRateVol(x, y) {
//rate Number: Set the playback rate. 1.0 is normal, .5 is half-speed, 2.0 is twice as fast. Values less than zero play backwards. (Optional)
//volume Number|Object: Volume (amplitude) between 0.0 and 1.0 or modulating signal/oscillator
let rate = 1.0; //rate(スピード)の初期値
let volume = 0.6; //volumeの初期値
if (x > vw / 2 + dw) { //手の中心(外接四角形の中心)が右緩衝ラインより右にあれば
rate = map(x - vw / 2 - dw, 0, vw / 2 - dw, 1, 2); //緩衝領域よりxがどれだけ右にあるかを計算し、それを1~2に線形補間
} else {
if (x < vw / 2 - dw) { //手の中心(外接四角形の中心)が左緩衝ラインより左にあれば
rate = map(vw / 2 - dw - x, 0, vw / 2 - dw, 1, 0.5); //緩衝領域よりxがどれだけ左にあるかを計算し、それを1~0.5に線形補間
}else{
rate = 1.0; //rateの標準値
}
}
if (y > vh / 2 + dh) { //手の中心(外接四角形の中心)が下緩衝ラインより下にあれば
volume = map(y - vh / 2 - dh, 0, vh / 2 - dh, 0.6, 0.0); //緩衝領域よりyがどれだけ下にあるかを計算し、それを0.6~0.0に線形補間
} else {
if (y < vh / 2 - dh) { //手の中心(外接四角形の中心)が上緩衝ラインより上にあれば
volume = map(vh / 2 - dh - y, 0, vh / 2 - dh, 0.6, 1.0); //緩衝領域よりyがどれだけ上にあるかを計算し、それを0.6~1.0に線形補間
}else{
volume = 0.6; //volumeの標準値
}
}
return [rate, volume];
}