浏览代码

feat: add exercises

Léo Salé 3 年之前
父节点
当前提交
510c9b5bcc

+ 6 - 5
app/lib/exercises/exercises_page.dart

@@ -1,5 +1,7 @@
 import 'package:flutter/material.dart';
+import 'package:physigo/exercises/exercises_validation/models/arm_raises.dart';
 import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
+import 'package:physigo/exercises/exercises_validation/models/leg_curl.dart';
 import 'package:physigo/exercises/exercises_validation/models/squat.dart';
 import 'package:physigo/walking/walking_page.dart';
 
@@ -67,11 +69,10 @@ class ExercisesPage extends StatelessWidget {
             ),
             const SizedBox(height: 48),
             const WalkExerciseTile(),
-            const ExerciseTile(
-              exerciseName: "push up",
-              exercise: pushUp,
-            ),
-            const ExerciseTile(exerciseName: "squat", exercise: squat),
+            ExerciseTile(exerciseName: "push up", exercise: pushUp),
+            ExerciseTile(exerciseName: "squat", exercise: squat),
+            ExerciseTile(exerciseName: "arm raises", exercise: armRaises),
+            ExerciseTile(exerciseName: "leg curl", exercise: legCurl),
           ],
         ),
       ),

+ 28 - 0
app/lib/exercises/exercises_validation/models/arm_raises.dart

@@ -0,0 +1,28 @@
+import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
+
+const startMovement = CriteriaDistance(
+  jointStart: AuthorizedTypeIndex.rightWrist,
+  jointEnd: AuthorizedTypeIndex.rightHip,
+  axis: 1,
+  threshold: 40,
+  comparator: Comparator.lesser,
+);
+const endMovement = CriteriaDistance(
+  jointStart: AuthorizedTypeIndex.rightWrist,
+  jointEnd: AuthorizedTypeIndex.rightShoulder,
+  axis: 1,
+  threshold: 80,
+  comparator: Comparator.lesser,
+);
+
+final armRaises = Exercise(
+  reps: 3,
+  series: 3,
+  startMovement: startMovement,
+  endMovement: endMovement,
+  jointsOnScreen: [
+    AuthorizedTypeIndex.rightWrist,
+    AuthorizedTypeIndex.rightHip,
+    AuthorizedTypeIndex.rightShoulder,
+  ],
+);

+ 35 - 6
app/lib/exercises/exercises_validation/models/exercise.dart

@@ -19,18 +19,27 @@ enum AuthorizedTypeIndex {
   rightAnkle,
 }
 
+enum Comparator {
+  greater,
+  lesser,
+}
+
 class Exercise {
   final int reps;
   final int series;
   final Criteria startMovement;
   final Criteria endMovement;
+  late final List<PoseLandmarkType> jointsOnScreen;
 
-  const Exercise({
+  Exercise({
     required this.reps,
     required this.series,
     required this.startMovement,
     required this.endMovement,
-  });
+    required List<AuthorizedTypeIndex> jointsOnScreen,
+  }) {
+    this.jointsOnScreen = jointsOnScreen.map((joint) => authorizedType[joint.index]).toList();
+  }
 
   bool isAtStartMovement(MeanFilteredData meanFilteredData) {
     return startMovement.isAtPosition(meanFilteredData);
@@ -65,13 +74,15 @@ class CriteriaDistance implements Criteria {
   final AuthorizedTypeIndex jointStart;
   final AuthorizedTypeIndex jointEnd;
   final int axis;
-  final int threshold;
+  final num threshold;
+  final Comparator comparator;
 
   const CriteriaDistance({
     required this.jointStart,
     required this.jointEnd,
     required this.axis,
     required this.threshold,
+    required this.comparator,
   });
 
   @override
@@ -79,7 +90,15 @@ class CriteriaDistance implements Criteria {
     final start = meanFilteredData[jointStart.index][axis];
     final end = meanFilteredData[jointEnd.index][axis];
     final distance = (start - end).abs();
-    return distance < threshold;
+    return _compare(distance, threshold);
+  }
+
+  bool _compare(num distance, num threshold) {
+    if (comparator == Comparator.greater) {
+      return distance > threshold;
+    } else {
+      return distance < threshold;
+    }
   }
 }
 
@@ -87,13 +106,15 @@ class CriteriaAngle implements Criteria {
   final AuthorizedTypeIndex jointStart;
   final AuthorizedTypeIndex jointCenter;
   final AuthorizedTypeIndex jointEnd;
-  final int threshold;
+  final num threshold;
+  final Comparator comparator;
 
   const CriteriaAngle({
     required this.jointStart,
     required this.jointCenter,
     required this.jointEnd,
     required this.threshold,
+    required this.comparator,
   });
 
   @override
@@ -111,6 +132,14 @@ class CriteriaAngle implements Criteria {
         y: meanFilteredData[jointEnd.index][1],
         z: meanFilteredData[jointEnd.index][2]);
     final angle = DistanceUtils.angleBetweenThreePoints(start, center, end).round();
-    return angle > threshold;
+    return _compare(angle, threshold);
+  }
+
+  bool _compare(num angle, num threshold) {
+    if (comparator == Comparator.greater) {
+      return angle > threshold;
+    } else {
+      return angle < threshold;
+    }
   }
 }

+ 30 - 0
app/lib/exercises/exercises_validation/models/leg_curl.dart

@@ -0,0 +1,30 @@
+import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
+
+const startMovement = CriteriaDistance(
+  jointStart: AuthorizedTypeIndex.rightAnkle,
+  jointEnd: AuthorizedTypeIndex.leftAnkle,
+  axis: 1,
+  threshold: 40,
+  comparator: Comparator.lesser,
+);
+
+const endMovement = CriteriaDistance(
+  jointStart: AuthorizedTypeIndex.rightAnkle,
+  jointEnd: AuthorizedTypeIndex.rightKnee,
+  axis: 1,
+  threshold: 40,
+  comparator: Comparator.lesser,
+);
+
+final legCurl = Exercise(
+  reps: 3,
+  series: 3,
+  startMovement: startMovement,
+  endMovement: endMovement,
+  jointsOnScreen: [
+    AuthorizedTypeIndex.rightAnkle,
+    AuthorizedTypeIndex.leftAnkle,
+    AuthorizedTypeIndex.rightKnee,
+    AuthorizedTypeIndex.rightHip,
+  ],
+);

+ 17 - 8
app/lib/exercises/exercises_validation/models/push_up.dart

@@ -1,22 +1,31 @@
 import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
 
-const startMovement = CriteriaAngle(
+const startMovement = CriteriaDistance(
   jointStart: AuthorizedTypeIndex.rightShoulder,
-  jointCenter: AuthorizedTypeIndex.rightElbow,
-  jointEnd: AuthorizedTypeIndex.rightWrist,
-  threshold: 300,
+  jointEnd: AuthorizedTypeIndex.rightElbow,
+  axis: 1,
+  threshold: 130,
+  comparator: Comparator.greater,
 );
 
 const endMovement = CriteriaDistance(
-  jointStart: AuthorizedTypeIndex.leftShoulder,
-  jointEnd: AuthorizedTypeIndex.leftElbow,
+  jointStart: AuthorizedTypeIndex.rightShoulder,
+  jointEnd: AuthorizedTypeIndex.rightElbow,
   axis: 1,
-  threshold: 40,
+  threshold: 110,
+  comparator: Comparator.lesser,
 );
 
-const pushUp = Exercise(
+final pushUp = Exercise(
   reps: 3,
   series: 3,
   startMovement: startMovement,
   endMovement: endMovement,
+  jointsOnScreen: [
+    AuthorizedTypeIndex.rightShoulder,
+    AuthorizedTypeIndex.rightElbow,
+    AuthorizedTypeIndex.rightWrist,
+    AuthorizedTypeIndex.leftShoulder,
+    AuthorizedTypeIndex.leftElbow,
+  ],
 );

+ 11 - 2
app/lib/exercises/exercises_validation/models/squat.dart

@@ -5,18 +5,27 @@ const startMovement = CriteriaAngle(
   jointCenter: AuthorizedTypeIndex.rightHip,
   jointEnd: AuthorizedTypeIndex.rightKnee,
   threshold: 320,
+  comparator: Comparator.greater,
 );
 
 const endMovement = CriteriaDistance(
   jointStart: AuthorizedTypeIndex.leftHip,
   jointEnd: AuthorizedTypeIndex.leftKnee,
   axis: 1,
-  threshold: 40,
+  threshold: 80,
+  comparator: Comparator.lesser,
 );
 
-const squat = Exercise(
+final squat = Exercise(
   reps: 3,
   series: 3,
   startMovement: startMovement,
   endMovement: endMovement,
+  jointsOnScreen: [
+    AuthorizedTypeIndex.rightShoulder,
+    AuthorizedTypeIndex.rightHip,
+    AuthorizedTypeIndex.rightKnee,
+    AuthorizedTypeIndex.leftHip,
+    AuthorizedTypeIndex.leftKnee,
+  ],
 );

+ 69 - 25
app/lib/exercises/exercises_validation/widgets/pose_detector.dart

@@ -34,6 +34,7 @@ class _PoseDetectorState extends State<PoseDetector> {
   final List<StreamController> _streamControllers = [];
   final StreamController<Pose> _poseController = StreamController.broadcast();
   final StreamController<StepExercise> _stepExerciseController = StreamController.broadcast();
+  late final Stream<List<PoseLandmark>> _exerciseJointsStream;
   Image? _cameraImage;
   Pose? _detectedPose;
   Size _imageSize = Size.zero;
@@ -46,34 +47,76 @@ class _PoseDetectorState extends State<PoseDetector> {
     super.initState();
     _streamControllers.add(_poseController);
     _streamControllers.add(_stepExerciseController);
+    _exerciseJointsStream = _getExerciseJointsStream(_poseController.stream);
+    CombineLatestStream.combine2(
+        _exerciseJointsStream
+            .map((event) => event.where((e) => widget.exercise.jointsOnScreen.contains(e.type)).toList()),
+        _stepExerciseController.stream,
+        (a, b) => [a, b]).listen(_handleNotInPlacePosition);
+    CombineLatestStream.combine2(
+        _exerciseJointsStream
+            .map((event) => event.where((e) => widget.exercise.jointsOnScreen.contains(e.type)).toList())
+            .bufferCount(5, 1),
+        _stepExerciseController.stream,
+        (a, b) => [a, b]).listen(_handleReadyPosition);
     _startCamera = _startCameraStream();
-    _meanFilterStream = _getMeanFilterStream(_poseController.stream);
+    _meanFilterStream = _getMeanFilterStream(_exerciseJointsStream);
     _streamSubscriptions.add(
-        CombineLatestStream.combine2(_stepExerciseController.stream, _meanFilterStream, (a, b) => [a, b]).listen((value) {
-      final stepExercise = value.first as StepExercise;
-      final meanFilteredData = value.last as MeanFilteredData;
-      final isStartOfExerciseMovement = widget.exercise.isAtStartMovement(meanFilteredData);
-      final isEndOfExerciseMovement = widget.exercise.isAtEndMovement(meanFilteredData);
-      if (stepExercise == StepExercise.notInPlace && isStartOfExerciseMovement) {
-        _stepExerciseController.add(StepExercise.ready);
-      }
-      if ((stepExercise == StepExercise.ready || stepExercise == StepExercise.start) && isEndOfExerciseMovement) {
-        _stepExerciseController.add(StepExercise.end);
-      }
-      if (stepExercise == StepExercise.end && isStartOfExerciseMovement) {
-        _stepExerciseController.add(StepExercise.start);
-      }
-    }));
+      CombineLatestStream.combine2(_stepExerciseController.stream, _meanFilterStream, (a, b) => [a, b]).listen(
+        (value) {
+          final stepExercise = value.first as StepExercise;
+          if (stepExercise == StepExercise.notInPlace) return;
+          final meanFilteredData = value.last as MeanFilteredData;
+          final isStartOfExerciseMovement = widget.exercise.isAtStartMovement(meanFilteredData);
+          final isEndOfExerciseMovement = widget.exercise.isAtEndMovement(meanFilteredData);
+          if ((stepExercise == StepExercise.start) && isEndOfExerciseMovement) {
+            _stepExerciseController.add(StepExercise.end);
+          }
+          if ((stepExercise == StepExercise.end || stepExercise == StepExercise.ready) && isStartOfExerciseMovement) {
+            _stepExerciseController.add(StepExercise.start);
+          }
+        },
+      ),
+    );
     _stepExerciseController.add(StepExercise.notInPlace);
     _repCounter = _stepExerciseController.stream
-        .where((event) => event == StepExercise.start)
+        .pairwise()
+        .where((event) => event.first == StepExercise.end && event.last == StepExercise.start)
         .scan((int accumulated, value, index) => accumulated + 1, 0);
   }
 
-  Stream<MeanFilteredData> _getMeanFilterStream(Stream<Pose> stream) {
+  void _handleNotInPlacePosition(List<Object?> event) {
+    final poseLandmarks = event.first as List<PoseLandmark>;
+    final stepExercise = event.last as StepExercise;
+    if (stepExercise == StepExercise.notInPlace) return;
+    for (final poseLandmark in poseLandmarks) {
+      if (poseLandmark.inFrameLikelihood < 0.8) {
+        _stepExerciseController.add(StepExercise.notInPlace);
+        return;
+      }
+    }
+  }
+
+  void _handleReadyPosition(List<Object?> event) {
+    final poseLandmarksBuffered = event.first as List<List<PoseLandmark>>;
+    final stepExercise = event.last as StepExercise;
+    if (stepExercise != StepExercise.notInPlace) return;
+    for (final poseLandmarks in poseLandmarksBuffered) {
+      for (final poseLandmark in poseLandmarks) {
+        if (poseLandmark.inFrameLikelihood < 0.8) return;
+      }
+    }
+    _stepExerciseController.add(StepExercise.ready);
+  }
+
+  Stream<List<PoseLandmark>> _getExerciseJointsStream(Stream<Pose> stream) {
     return stream
         .where((pose) => pose.landmarks.isNotEmpty)
-        .map((pose) => pose.landmarks.where((landmark) => Exercise.authorizedType.contains(landmark.type)).toList())
+        .map((pose) => pose.landmarks.where((landmark) => Exercise.authorizedType.contains(landmark.type)).toList());
+  }
+
+  Stream<MeanFilteredData> _getMeanFilterStream(Stream<List<PoseLandmark>> stream) {
+    return stream
         // Get last [buffer] poses
         .bufferCount(meanFilterBuffer, 1)
         // Swap matrix [buffer] * [authorizedType.length]
@@ -143,7 +186,9 @@ class _PoseDetectorState extends State<PoseDetector> {
 
   void _handlePose(Pose? pose) {
     if (!mounted) return;
-    if (pose != null) _poseController.add(pose);
+    if (pose != null) {
+      _poseController.add(pose);
+    }
     setState(() {
       _detectedPose = pose;
     });
@@ -174,10 +219,10 @@ class _PoseDetectorState extends State<PoseDetector> {
             Center(
               child: CustomPaint(
                 child: _cameraImage,
-                foregroundPainter: PosePainter(
-                  pose: _detectedPose,
-                  imageSize: _imageSize,
-                ),
+                // foregroundPainter: PosePainter(
+                //   pose: _detectedPose,
+                //   imageSize: _imageSize,
+                // ),
               ),
             ),
             StreamBuilder<StepExercise>(
@@ -235,5 +280,4 @@ class _PoseDetectorState extends State<PoseDetector> {
       },
     );
   }
-
 }