ちまたで、MediaPipeが流行っているようで、私も触ってみました。
PCに接続したWebCamの映像をMediaPipeに通すことで、人のポーズが検出できます。
そこで、マウスが押されてから離されるまで、人の腕から指の部分をトレースして、軌跡を映像に重ね合わせてみました。
さあ、手にマウスをもって魔方陣を軌跡で描いてみましょう。
ちなみに、マウスはM5StickCを使っています。バッテリとBLEが付いているので、手にもって軌跡を描きながら、マウスクリックができるので、(多少は)軌跡を描きやすくなります。
ソースコードもろもろを以下に上げておきました。
poruruba/MagicPipe_Tracing
https://github.com/poruruba/MagicPipe_Tracing
#Webページの作成
MediaPipeにいくつかあるSolutionのうち、Poseを使いました。
以下のページに書いてあるサンプルソースを改造します。
https://google.github.io/mediapipe/solutions/pose#javascript-solution-api
結果がこちら。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose/pose.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="container" style="position: relative;">
<video class="input_video" style="position: absolute; visibility: hidden"></video>
<canvas class="video_canvas" width="1280px" height="720px" style="position: absolute;"></canvas>
<canvas class="output_canvas" width="1280px" height="720px" style="position: absolute;"></canvas>
</div>
</body>
<script type="module">
const videoElement = document.getElementsByClassName('input_video')[0];
const canvasElement = document.getElementsByClassName('output_canvas')[0];
const videoCanvasElement = document.getElementsByClassName('video_canvas')[0];
const canvasCtx = canvasElement.getContext('2d');
const videoCtx = videoCanvasElement.getContext('2d');
let mouse_pressed = false;
let prev_xy = { x : -1, y : -1 };
let draw_cleared = true;
const NUM_OF_DATA = 3;
window.onmousedown = (event) =>{
mouse_pressed = true;
};
window.onmouseup = (event) => {
mouse_pressed = false;
};
window.onmouseout = (event) =>{
mouse_pressed = false;
}
let x_ary = [];
let y_ary = [];
function reset_position(){
for( var i = 0 ; i < NUM_OF_DATA ; i++ ){
x_ary[i] = -1;
y_ary[i] = -1;
}
return { x: -1, y: -1 };
}
function push_positioin(x, y){
for( let i = 0 ; i < NUM_OF_DATA - 1 ; i++ ){
x_ary[NUM_OF_DATA - 1 - i] = x_ary[NUM_OF_DATA - 1 - i - 1];
y_ary[NUM_OF_DATA - 1 - i] = y_ary[NUM_OF_DATA - 1 - i - 1];
}
x_ary[0] = x;
y_ary[0] = y;
let sum_x = 0, sum_y = 0;
let i = 0;
for( i = 0 ; i < NUM_OF_DATA ; i++ ){
if (x_ary[i] < 0 || y_ary[i] < 0)
break;
sum_x += x_ary[i];
sum_y += y_ary[i];
}
if( i == 0 )
return { x: -1, y: -1 };
else
return { x: sum_x / i, y: sum_y / i };
}
function onResults(results) {
canvasCtx.save();
videoCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
canvasCtx.strokeStyle = '#800';
canvasCtx.fillStyle = '#800';
canvasCtx.lineWidth = 10;
if( results.poseLandmarks[13].visibility >= 0.7 && results.poseLandmarks[11].visibility >= 0.7 && prev_xy.x < 0 && prev_xy.y < 0 && !draw_cleared){
var w = results.poseLandmarks[13].x - results.poseLandmarks[11].x;
var h = results.poseLandmarks[13].y - results.poseLandmarks[11].y;
var fire = (Math.abs(w / h) < 0.5) && (h < 0.0) && mouse_pressed;
console.log(fire);
if( fire ){
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
draw_cleared = true;
mouse_pressed = false;
}
}
if (results.poseLandmarks[19].visibility >= 0.7 && results.poseLandmarks[15].visibility >= 0.7 & mouse_pressed) {
var x1 = results.poseLandmarks[19].x;
var y1 = results.poseLandmarks[19].y;
var x2 = results.poseLandmarks[15].x;
var y2 = results.poseLandmarks[15].y;
var x = x1 + (x1 - x2);
var y = y1 + (y1 - y2);
var pos = push_positioin(x, y);
if( prev_xy.x >= 0 && prev_xy.y >= 0 ){
canvasCtx.beginPath();
canvasCtx.moveTo(prev_xy.x * 1280, prev_xy.y * 720);
canvasCtx.lineTo(pos.x * 1280, pos.y * 720);
canvasCtx.stroke();
draw_cleared = false;
}
prev_xy = pos;
}else{
prev_xy = reset_position();
}
canvasCtx.restore();
}
const pose = new Pose({locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;
}});
pose.setOptions({
modelComplexity: 1,
smoothLandmarks: true,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5,
selfieMode: true,
});
pose.onResults(onResults);
const camera = new Camera(videoElement, {
onFrame: async () => {
await pose.send({image: videoElement});
},
width: 1280,
height: 720
});
reset_position();
camera.start();
</script>
</html>
ポイントだけ解説します。
〇映像をセルフィー視点に変更
映像をそのまま表示すると、自分が描いた軌跡が左右反転してしまいます。
そこで、セルフィーのように、映像を左右反転させます。
<video class="input_video" style="position: absolute; visibility: hidden"></video>
・・・
selfieMode: true,
として、左右反転させた映像とし、オリジナルの映像は非表示にしています。
〇指の位置を検出
ポーズの体の部品の位置は以下のインデックスのポイントで分かります。
https://google.github.io/mediapipe/images/mobile/pose_tracking_full_body_landmarks.png
以下の部分です。
ポイント15から19へのベクトルの2倍の長さの先に、指先がある想定としました。
if (results.poseLandmarks[19].visibility >= 0.7 && results.poseLandmarks[15].visibility >= 0.7 & mouse_pressed) {
var x1 = results.poseLandmarks[19].x;
var y1 = results.poseLandmarks[19].y;
var x2 = results.poseLandmarks[15].x;
var y2 = results.poseLandmarks[15].y;
var x = x1 + (x1 - x2);
var y = y1 + (y1 - y2);
var pos = push_positioin(x, y);
if( prev_xy.x >= 0 && prev_xy.y >= 0 ){
canvasCtx.beginPath();
canvasCtx.moveTo(prev_xy.x * 1280, prev_xy.y * 720);
canvasCtx.lineTo(pos.x * 1280, pos.y * 720);
canvasCtx.stroke();
draw_cleared = false;
}
prev_xy = pos;
}else{
prev_xy = reset_position();
}
〇軌跡を移動平均化する
そのまま軌跡にすると、ノイズが大きくギザギザするため、3点の移動平均にしました。
function reset_position(){
function push_positioin(x, y){
〇腕を上げてマウスクリックで軌跡クリア
そのまま軌跡を描き続けていると、軌跡で埋まってしまうので、軌跡をクリアするポーズを決めました。
ポイント11から13へのベクトルが上を向いた状態でマウスクリックした場合にクリアするようにしました。
if( results.poseLandmarks[13].visibility >= 0.7 && results.poseLandmarks[11].visibility >= 0.7 && prev_xy.x < 0 && prev_xy.y < 0 && !draw_cleared){
var w = results.poseLandmarks[13].x - results.poseLandmarks[11].x;
var h = results.poseLandmarks[13].y - results.poseLandmarks[11].y;
var fire = (Math.abs(w / h) < 0.5) && (h < 0.0) && mouse_pressed;
console.log(fire);
if( fire ){
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
draw_cleared = true;
mouse_pressed = false;
}
}
#M5StickCをBLEマウス化
PCにつないでいるマウスを使ってもいいのですが、ポーズの手が検出しにくくなるため、手に持ちやすいM5StickCをBLEマウス化しました。
以下を使わせていただきました。
以下、M5StickC側のソースコードです。
#include <M5Stickc.h>
#include <BleMouse.h>
BleMouse bleMouse;
#define BTN_WAIT 100
bool btnA_pressed = false;
bool btnB_pressed = false;
bool ble_connected = false;
void setup() {
// put your setup code here, to run once:
M5.begin();
Serial.begin(115200);
Serial.println("Starting BLE work!");
pinMode(M5_LED, OUTPUT);
digitalWrite(M5_LED, HIGH);
bleMouse.begin();
}
void loop() {
M5.update();
// put your main code here, to run repeatedly:
if (bleMouse.isConnected())
{
if (!ble_connected)
ble_connected = true;
if (M5.BtnA.isPressed() && !btnA_pressed)
{
btnA_pressed = true;
digitalWrite(M5_LED, LOW);
bleMouse.press(MOUSE_LEFT);
Serial.println("Pressed LEFT");
delay(BTN_WAIT);
}
else if (M5.BtnA.isReleased() && btnA_pressed)
{
btnA_pressed = false;
digitalWrite(M5_LED, HIGH);
bleMouse.release(MOUSE_LEFT);
Serial.println("Released LEFT");
delay(BTN_WAIT);
}
if (M5.BtnB.isPressed() && !btnB_pressed)
{
btnB_pressed = true;
bleMouse.press(MOUSE_RIGHT);
Serial.println("Pressed RIGHT");
delay(BTN_WAIT);
}
else if (M5.BtnB.isReleased() && btnB_pressed)
{
btnB_pressed = false;
bleMouse.release(MOUSE_RIGHT);
Serial.println("Released RIGHT");
delay(BTN_WAIT);
}
}else{
if (ble_connected){
ble_connected = false;
digitalWrite(M5_LED, HIGH);
}
}
}
platformio.iniは以下の通りです。
[env:m5stick-c]
platform = espressif32
board = m5stick-c
framework = arduino
monitor_speed = 115200
upload_port = COM5
monitor_port = COM5
lib_deps =
t-vk/ESP32 BLE Mouse@^0.3.1
m5stack/M5StickC@^0.2.0
#終わりに
実際に軌跡を描いているところを録画してアップしてもよかったのですが、ちょっと恥ずかしかったので、遠慮しました。。。
以上