16
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

技術要素

  • Flutter
  • TensorFlow Lite (tfliteを使用)

できたもの

しっかり深くしゃがみ込めば、
f1_resized.png
回数を数えてくれます
f2_resized.png
しかし、しゃがみ込みが浅いと、
f3_resized.png
怒られます。
f4_resized.png

実装

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

リアルタイム骨格検知

PoseNet を使用しています。
カメラの扱い方などはサンプルコードのほうが詳しいため、割愛します。

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

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

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

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);
}

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

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. しきい値を越える膝角速度(伸展方向)を監視・検知
  2. 膝角度が深い場合は、カウントアップ
  3. 膝角度が浅い場合は、怒る
  4. 3秒後、1に戻る

参考リンク

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

16
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?