RyotaIkeuchi が 2021年05月14日22時48分17秒 に編集
コメント無し
記事種類の変更
製作品
本文の変更
# デモ動画 @[youtube](https://www.youtube.com/embed/q8kcjFUpJAo) # しくみ、使い方 事前準備. お好みのチロルチョコ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から電源をとって動作できるモーターもあるが、今回使用したモーターは別電源をとらないとエラーとなった # ソースコード ```python: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のみを書き換えた - 頭の傾きに合わせてサーボモーターを動かす(obnizサンプル)https://blog.obniz.com/make/move-the-servomotor-tilt-the-head # その他参考 - 第一回 フリースローチャレンジ(WRO) https://www.wroj.org/2020/rsports-2020-01 …投げる構造の参考として - 3代目マシュマロ・カタパルト https://sweetelectronics.wordpress.com/2014/03/21/marshmallow/ …今回のアイデアを思いついたとき、「おやつを投げる」というのは既にありそうだなと思って調べると見つかったもの。発射機構が美しいです。そしてジェスチャー認識まで既にやってらっしゃるようです。リスペクトです。 # こだわり レゴは、DUPLOと通常サイズのハイブリッド。 モーターをくっつけるなど細かいところは通常サイズで、下部の高さを稼ぐところはDUPLO。 obniz boardのメリットが出せるような構成にしようと思い、サーボモーターを複数接続し(モータードライバー回路がたくさん入っていて扱いやすい)、スマホ上でのtensorflowjsの姿勢推定と組み合わせた。 # ご注意 機構部は簡易的にレゴを使ったが、発射するたびにレゴ部が外れたり、スマホが落下したりするので、普段使いする方は真面目につくってください。 パン用のサーボモーターがたびたび振動するので、そちらも真面目に設計・調整が必要。 # おわりに サンプルを流用して開発もスムーズに進めることが出来た。さすがobnizです。