RyotaIkeuchiのアイコン画像
RyotaIkeuchi 2021年05月14日作成 (2021年05月14日更新)
製作品 製作品 閲覧数 1968
RyotaIkeuchi 2021年05月14日作成 (2021年05月14日更新) 製作品 製作品 閲覧数 1968

お菓子とってー → 発射!

お菓子とってー → 発射!

デモ動画

ここに動画が表示されます

しくみ、使い方

事前準備. お好みのチロルチョコ1個を発射台に置いておく
キャプションを入力できます

  1. 帽子の人が、レーダーと装置の稼働状態を確認しつつ、目視でユーザーの位置を確認し、装置が常にユーザーの方に向くようにパン角度をハンドルで操作する
    キャプションを入力できます
  2. 帽子の人が、ユーザーの挙手したことを目視確認すると、ヘルメットの人に司令を出す
  3. ヘルメットの人はトランシーバーで司令を受け取り、装置を発射させる(投石機)
    キャプションを入力できます

実際のしくみ

  1. スマホカメラでユーザーの顔の位置(姿勢推定の鼻の座標)を検出し、顔がスマホカメラの正面の向きにくるように、パン用のサーボモーターを動かす
  2. 姿勢推定で挙手を検知(右手首の座標が鼻よりも上にあり、一定以上の差があることを検知)すると、もう1つのサーボモーターを動かして発射する、

部品

数量
obniz Board 1Y 1
Servo Kit 180‘ (M5Stack) モーター2個(1セット)
USB microB ピッチ変換基板 1

別途、USB電源が必要(モバイルバッテリー or ACアダプター+コンセント)

配線図

キャプションを入力できます
図のブレッドボードのGND, 電源にはモバイルバッテリーをピッチ変換基板経由で接続する
※obniz boardから電源をとって動作できるモーターもあるが、今回使用したモーターは別電源をとらないとエラーとなった

ソースコード

code.js

let canvas = document.getElementById("canvas"); let ctx = canvas.getContext("2d"); ctx.strokeStyle = "#00ff33"; const obniz = new Obniz("xxxx-xxxx"); obniz.onconnect = async function () { let servo_pan = obniz.wired("ServoMotor", { gnd: 0, vcc: 1, signal: 2 }); let servo_throw = obniz.wired("ServoMotor", { gnd: 9, vcc: 10, signal: 11 }); const imageScaleFactor = 0.2; const outputStride = 16; const contentWidth = 800; const contentHeight = 600; let pan_angle_set = 90; let throw_angle_set = 90; bindPage(); async function bindPage() { const net = await posenet.load(); let video; try { video = await loadVideo(); } catch (e) { console.error(e); return; } detectPoseInRealTime(video, net); } async function loadVideo() { const video = await setupCamera(); video.play(); return video; } async function setupCamera() { const video = document.getElementById("video"); if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true }); video.srcObject = stream; return new Promise((resolve) => { video.onloadedmetadata = () => { resolve(video); }; }); } else { const errorMessage = "This browser does not support video capture, or this device does not have a camera"; alert(errorMessage); return Promise.reject(errorMessage); } } function detectPoseInRealTime(video, net) { const flipHorizontal = true; async function poseDetectionFrame() { let poses = []; const pose = await net.estimateSinglePose( video, imageScaleFactor, flipHorizontal, outputStride ); poses.push(pose); throw_something(poses); pan(poses); setTimeout(arguments.callee, 1000); } poseDetectionFrame(); } function pan(poses) { let nose = poses[0].keypoints[0].position; let sholderLeft = poses[0].keypoints[5].position; let sholderRight = poses[0].keypoints[6].position; let center = { x: 0, y: 0 }; center.x = (sholderLeft.x + sholderRight.x) / 2; center.y = (sholderLeft.y + sholderRight.y) / 2; draw(nose, sholderLeft, sholderRight, center); const delta = nose.x - canvas.width / 2; const th_stop = 100; // 十分中央付近にあればパン動作させない if (delta < th_stop && delta > -1 * th_stop) return; if (delta < 0) { pan_angle_set += 10; if (pan_angle_set > 180) pan_angle_set = 180; } else { pan_angle_set -= 10; if (pan_angle_set < 0) pan_angle_set = 0; } servo_pan.angle(pan_angle_set); } function throw_something(poses) { let nose = poses[0].keypoints[0].position; let wristLeft = poses[0].keypoints[9].position; let wristRight = poses[0].keypoints[10].position; draw(nose, wristLeft, wristRight); const wrist_y = Math.max(wristLeft.y, wristRight.y); const delta = nose.y - wrist_y; if (delta > 100) { throw_angle_set = 40; } else { // slowly to 110 if (throw_angle_set < 150 - 10) throw_angle_set += 20; } servo_throw.angle(throw_angle_set); } function draw(nose, sholderLeft, sholderRight, center) { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.beginPath(); ctx.moveTo(canvas.width / 2, canvas.height / 2); ctx.lineTo(nose.x, nose.y); ctx.closePath(); ctx.stroke(); ctx.font = "20pt Arial"; ctx.strokeText(Math.round(nose.x, 4), 100, 20); ctx.strokeText(Math.round(nose.x - canvas.width / 2, 4), 100, 50); ctx.strokeText(pan_angle_set, 100, 100); ctx.strokeText(throw_angle_set, 100, 130); } };

こちらのobnizサンプルをベースにcode.jsのみを書き換えた

その他参考

  • 第一回 フリースローチャレンジ(WRO) https://www.wroj.org/2020/rsports-2020-01
     …投げる構造の参考として
  • 3代目マシュマロ・カタパルト https://sweetelectronics.wordpress.com/2014/03/21/marshmallow/
     …今回のアイデアを思いついたとき、「おやつを投げる」というのは既にありそうだなと思って調べると見つかったもの。発射機構が美しいです。そしてジェスチャー認識まで既にやってらっしゃるようです。リスペクトです。

こだわり

レゴは、DUPLOと通常サイズのハイブリッド。
モーターをくっつけるなど細かいところは通常サイズで、下部の高さを稼ぐところはDUPLO。
obniz boardのメリットが出せるような構成にしようと思い、サーボモーターを複数接続し(モータードライバー回路がたくさん入っていて扱いやすい)、スマホ上でのtensorflowjsの姿勢推定と組み合わせた。

ご注意

機構部は簡易的にレゴを使ったが、発射するたびにレゴ部が外れたり、スマホが落下したりするので、普段使いする方は真面目につくってください。
パン用のサーボモーターがたびたび振動するので、そちらも真面目に設計・調整が必要。

おわりに

サンプルを流用して開発もスムーズに進めることが出来た。さすがobnizです。

ログインしてコメントを投稿する