「スクワットの深さは人間性の深さ」なんて名言があります。
しかしスクワットのフォームはなかなか難しく、家トレで鏡が無かったりすると深さのチェックも大変です。
そこで、ディープラーニングの助けを借りてディープなスクワットをしてみようと思います。
技術要素
- Flutter
- TensorFlow Lite (tfliteを使用)
できたもの
しっかり深くしゃがみ込めば、
回数を数えてくれます
しかし、しゃがみ込みが浅いと、
怒られます。
実装
コードは以下のリポジトリにあがっています。
リアルタイム骨格検知
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);
}
}
上記コードで膝角度、膝角速度が算出できました。
これを元に、次のルールでスクワットのフォームを判定します。
- しきい値を越える膝角速度(伸展方向)を監視・検知
- 膝角度が深い場合は、カウントアップ
- 膝角度が浅い場合は、怒る
- 3秒後、1に戻る
参考リンク
flutter-tflite作者の方によるサンプルです。
大いに参考にさせていただきました。
https://github.com/shaqian/flutter_realtime_detection
https://medium.com/@shaqian629/real-time-object-detection-in-flutter-b31c7ff9ef96