スクワットが浅いと怒られる

「スクワットの深さは人間性の深さ」なんて名言があります。
しかしスクワットのフォームはなかなか難しく、家トレで鏡が無かったりすると深さのチェックも大変です。
そこで、ディープラーニングの助けを借りてディープなスクワットをしてみようと思います。

## 技術要素

- Flutter
- TensorFlow Lite ([tflite](https://pub.dev/packages/tflite)を使用）

## できたもの

しっかり深くしゃがみ込めば、
![f1_resized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148726/cc7fb594-f1e3-0231-3760-16c345714d67.png)
回数を数えてくれます
![f2_resized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148726/ad8ddc24-dea7-5923-4fe5-9cb3fbb5cbd2.png)
しかし、しゃがみ込みが浅いと、
![f3_resized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148726/8e584f3a-ee48-d0c7-f348-b0ef17d0d36b.png)
怒られます。
![f4_resized.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148726/824df6d8-0f18-6bfe-a594-f6d35d26220e.png)


## 実装

コードは以下のリポジトリにあがっています。

https://github.com/uhey22e/squat-deeply.git



### リアルタイム骨格検知

[PoseNet](https://www.tensorflow.org/lite/examples/pose_estimation/overview) を使用しています。
カメラの扱い方などはサンプルコードのほうが詳しいため、割愛します。

### スクワットフォームの検知

フォーム検知のために必要なポイントは、「しゃがみ込みの深さを検知すること」、「切り返し（立ち上がり）のタイミングを検知すること」の二点です。
今回はこの二点を、膝の角度および角速度から評価しました。

実装はというと、まずPoseNetによって推定された各関節位置を `KeyPoints` におさめています。
ここに膝の角度を算出処理などをまとめています。

```dart
enum KeyPointPart {
  leftHip,
  leftKnee,
  leftAnkle,
}

class KeyPoints {
  final Map<KeyPointPart, KeyPoint> _points;

  double? get leftKneeAngle {
    final hip = _points[KeyPointPart.leftHip]?.vec;
    final knee = _points[KeyPointPart.leftKnee]?.vec;
    final ankle = _points[KeyPointPart.leftAnkle]?.vec;
    if (hip == null || knee == null || ankle == null) {
      return null;
    }
    return (hip - knee).angleTo(ankle - knee);
  }
}

class KeyPoint {
  final KeyPointPart part;
  final Vector2 vec;
  final double score;
  const KeyPoint(this.part, this.vec, this.score);
}
```

推論は無限ループで実行しており、各推論結果は数回分保持、膝角度の履歴から膝角速度を算出しています。
ひとつポイントとして、推論結果はかなりブレが大きいため、
各推論結果を保持する際に簡単なノイズフィルタ（移動平均）をかけています。

```dart
class KeyPointsSeries {
  final List<DateTime> timestamps;
  final List<KeyPoints> keyPoints;
  final List<double> kneeAngles;

  const KeyPointsSeries(this.timestamps, this.keyPoints, this.kneeAngles);

  const KeyPointsSeries.init()
      : timestamps = const [],
        keyPoints = const [],
        kneeAngles = const [];

  KeyPointsSeries push(DateTime timestamp, KeyPoints kp) {
    if (kp.leftHip == null || kp.leftKneeAngle == null) {
      return this;
    }

    final timestamps = [timestamp, ...this.timestamps];
    final keyPoints = [kp, ...this.keyPoints];
    if (keyPoints.length == 1) {
      return KeyPointsSeries(timestamps, keyPoints, [kp.leftKneeAngle!]);
    }

    // 移動平均
    const k = 0.7;
    final kneeAngles = [
      this.kneeAngles.first * (1 - k) + kp.leftKneeAngle! * k,
      ...this.kneeAngles,
    ];
    return KeyPointsSeries(
      timestamps.length > _bufferSize
          ? timestamps.sublist(0, _bufferSize - 1)
          : timestamps,
      keyPoints.length > _bufferSize
          ? keyPoints.sublist(0, _bufferSize - 1)
          : keyPoints,
      kneeAngles.length > _bufferSize
          ? kneeAngles.sublist(0, _bufferSize - 1)
          : kneeAngles,
    );
  }

  double get kneeAngleSpeed {
    // radian / sec
    if (kneeAngles.length < 2) {
      return 0;
    }
    final dt = timestamps[0].difference(timestamps[1]);
    return (kneeAngles[0] - kneeAngles[1]) /
        (dt.inMicroseconds.toDouble() / 1000000);
  }

}
```

上記コードで膝角度、膝角速度が算出できました。
これを元に、次のルールでスクワットのフォームを判定します。

1. しきい値を越える膝角速度（伸展方向）を監視・検知
  1. 膝角度が深い場合は、カウントアップ
  1. 膝角度が浅い場合は、怒る
1. 3秒後、1に戻る




## 参考リンク

flutter-tflite作者の方によるサンプルです。
大いに参考にさせていただきました。
https://github.com/shaqian/flutter_realtime_detection
https://medium.com/@shaqian629/real-time-object-detection-in-flutter-b31c7ff9ef96