今何回目だっけ
日課の筋トレ、ゆっくりと筋肉を傷めつけるために10秒でしゃがみ戻る、これを10回繰り返す1,2,3,4,5...
あれ?今何回目のスクワットだっけ???10秒数えてる間に今何回目か忘れた・・
私は頻繁にこの状態になります。
今回、プロトアウトスタジオの授業でml5.jsのPoseNetサンプルに触れているときに、姿勢推定モデルを利用して鼻やお尻の位置がある領域を下回ったときにカウントできないかと考えました。
実際にできたもの
https://thirsty-austin-efa702.netlify.app/

# 苦戦した点姿勢推定モデルでスクワットカウンタ作りました。ぜひ試してみてください!https://t.co/TcALvcLsbU#protoout #JavaScript #機械学習 pic.twitter.com/pgvFvqExDM
— Toshiki (@Hirasawa1987) September 1, 2020
画像の原点
表示される画像の原点がどこにあるのわかりませんでした。
console.log(pose.nose);
を途中に入れ鼻がどの位置に来たら0,0になるか探したところ、左上が原点であることがわかりました、X座標は右に行くと増え、Y座標は下に行くと増えることがわかりました。
カウント
指定のY座標(今回は画面中央)より鼻(nose)が下にいったら1カウントするものを想定して作っていましたが、鼻が下に入った点でカウントが始まり、下回っている間、無限にカウントしていました、こちらに関してはQAで投げかけたところ、@uasiさんに答えていただき、今回はフラグ(should_count = true)を立てて下回ったらfalseにするという方法で1カウントのみで止まるようになりました。@uasiさんありがとうございました。
自分なりに調べました。
答えていただいた内容を自分なりに調べました。そのなかで&&の使い方につまずきましたが、以下のサイトで解消しました。
- [JavaScriptの「&&」「||」について盛大に勘違いをしていた件]
(https://qiita.com/Imamotty/items/bc659569239379dded55) 
ディレクトリ構成図
root/
 ├ index.html
 ├ sketch.js
 └ style.css
コード
<html>
<head>
  <meta charset="UTF-8">
  <title>スクワットカウンタ</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/addons/p5.dom.min.js"></script>
  <script src="https://unpkg.com/ml5@0.3.1/dist/ml5.min.js" type="text/javascript"></script>
</head>
<body>
  <h1>スクワットカウンタ</h1>
  <p id='status'></p>
  <div class="container">
    <div class="dash-unit">
      <h2 class="a-spacing-none">回数</h2>
      <div class="cont">
      <div id="disp_count">0</div>
      </div>
      <div class="reset">
        <input type="button" value="リセット" id="btn_reset" />
      </div>
      <div class="twitter"></div>
      <a href="https://twitter.com/Hirasawa1987?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">Follow @Hirasawa1987</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
      </div>
    </div>
  </div>
  <script src="sketch.js"></script>
</body>
<link href="style.css" rel="stylesheet">
</html>
// Copyright (c) 2018 ml5
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
/* ===
ml5 Example
PoseNet example using p5.js
=== */
let video;
let poseNet;
let poses = [];
let count_value = 0;
let should_count = true; // フラグ
let count_disp = document.getElementById("disp_count");  
let reset_btn = document.getElementById("btn_reset");
function setup() {
  console.log('セットアップ');
  createCanvas(600, 500);
  video = createCapture(VIDEO);
  video.size(width, height);
  // Create a new poseNet method with a single detection
  poseNet = ml5.poseNet(video, modelReady);
  // This sets up an event that fills the global variable "poses"
  // with an array every time new poses are detected
  poseNet.on('pose', function(results) {
    poses = results;
  });
  // Hide the video element, and just show the canvas
  video.hide();
}
function modelReady() {
  select('#status').html('');
}
function count() {
  
  count_value += 1;
  console.log(count_value);
  count_disp.innerHTML = count_value;
}
function draw() {
    if (poses.length > 0) {
      let pose = poses[0].pose;
      let keypoint = pose.keypoints[0];
      // console.log('部位名:' + keypoint.part);
        for (let i = 0; i < poses.length; i++) {
            // poseが持つ情報を出力
            let pose = poses[i].pose;
            // console.log('全体の精度' + pose.score);
            // console.log('nose' + pose.nose.x);
            // console.log('nose' + pose.nose.y);
            if (pose.score >= 0.6){
              // console.log("ok");
              if (pose.nose.y >= 250.0 && should_count) {
                console.log(pose.nose.y);
                count_value += 1;
                should_count = false;
                console.log(count_value);
                console.log(should_count);
                count_disp.innerHTML = count_value;
              } else if (pose.nose.y <= 240.0) {
                // ↑姿勢が元に戻った判定(数値は適当)。
                // もしここを > 250.0 にすると姿勢が250.0前後で震えたとき何度もカウントしてしまう。
                should_count = true;
              }
              if(count_value == 10 ){
                document.getElementById('disp_count').style.color="#FF0000";
              }
            }
            
        }
        for (let i = 0; i < poses.length; i++) {
            let skeleton = poses[i].skeleton;
            
        }
        // イメージをp5.jsのキャンバスに描画する。<= createCanvas(640, 360)で作成したキャンバス
        // image(img, x, y, [width], [height])
        // https://p5js.org/reference/#/p5/image
        image(video, 0, 0, width, height);
        drawSkeleton();
        drawKeypoints();
        // p5.jsがdraw()内のコードの連続的な実行を行うのを停める
        // https://p5js.org/reference/#/p5/noLoop
        // noLoop(); // posesの推定時はループを停める
    }
    
// image(video, 0, 0, width, height);
// We can call both functions to draw all keypoints and the skeletons
fill(255, 0, 0);
//線を引く
stroke('red');
line(0, 250, 600, 250);
}
// A function to draw ellipses over the detected keypoints
function drawKeypoints()  {
  // Loop through all the poses detected
  for (let i = 0; i < poses.length; i++) {
    // For each pose detected, loop through all the keypoints
    let pose = poses[i].pose;
    for (let j = 0; j < pose.keypoints.length; j++) {
      // A keypoint is an object describing a body part (like rightArm or leftShoulder)
      let keypoint = pose.keypoints[j];
      // Only draw an ellipse is the pose probability is bigger than 0.2
      if (keypoint.score > 0.2) {
        fill(color(0, 0, 255));
        stroke(20);
        // noStroke();
        ellipse(keypoint.position.x, keypoint.position.y, 10, 10);
      }
    }
  }
}
// A function to draw the skeletons
function drawSkeleton() {
  // Loop through all the skeletons detected
  for (let i = 0; i < poses.length; i++) {
    let skeleton = poses[i].skeleton;
    // For every skeleton, loop through all body connections
    for (let j = 0; j < skeleton.length; j++) {
      let partA = skeleton[j][0];
      let partB = skeleton[j][1];
      stroke('rgb(0,255,0)');
      strokeWeight(5);
      line(partA.position.x, partA.position.y, partB.position.x, partB.position.y);
    }
  }
}
reset_btn.onclick = function (){
  count_value = 0; count_disp.innerHTML = count_value;
}
body {
    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
    font-size: 14px;
    line-height: 1.42857143;
    background: #1f1f1f;
    color: #fff;
}
h1{
    font-size: 45px;
  }
div {
    display: block;
}
.cont {
    text-align: center;
    margin-top: 30px;
}
.container {
    padding-right: 15px;
    padding-left: 15px;
    margin-right: auto;
    margin-left: auto;
}
.a-spacing-none {
    font-size: 30px
}
# btn_reset {
    font-size: 15px;
    border-radius: 10px;
    padding: 10px;
    margin-top: 10px;
    margin-bottom: 10px;
}
# disp_count {
    font-size: 70px;
    font-weight: bold; 
}
# defaultCanvas0{
    display: block;
    /* float: left; */
    /* box-shadow: 0px 10px 10px -5px; */
}
.dash-unit {
    margin: 0px 30px 0px 30px;
    padding-bottom: 10px;
    text-align: center;
    border: 1px solid #383737;
    background-image: url(../images/sep-half.png);
    background-color: #3d3d3d;
    /* color: white; */
    /* height: 200px; */
    width: 290px;
    float: left;
    box-shadow: 0px 5px 5px -5px;
}
# temperature {
    display: inline-block;
    color: #000;
    text-align: center;
    padding: 1% 5%;
    /* background-color: #99cc00; */
    margin: 10px;
}
.twitter{
    margin-top: 1px;
    float: left;
  }
考察
今回のスクワットカウンタは、画面内にいても立ち位置を調整しなくてはならないため、実用レベルではありません。可能かどうかはわかりませんが、体の一部を原点にすることで画面内にいれば、立ち位置を調整する必要はなさそうです。