Prechádzať zdrojové kódy

feat: add push up exercise

Léo Salé 3 rokov pred
rodič
commit
3688e869fd

+ 0 - 2
app/android/app/src/main/AndroidManifest.xml

@@ -4,8 +4,6 @@
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.CAMERA" />
-   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-   <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <application
         android:label="PhysiGo"
         android:name="${applicationName}"

+ 50 - 0
app/lib/exercises/exercises_page.dart

@@ -0,0 +1,50 @@
+import 'package:flutter/material.dart';
+import 'package:physigo/exercises/exercises_validation/models/squat.dart';
+
+import 'exercises_validation/exercise_validation_page.dart';
+import 'exercises_validation/models/push_up.dart';
+
+class ExercisesPage extends StatelessWidget {
+  const ExercisesPage({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Center(
+            child: ElevatedButton(
+              onPressed: () {
+                Navigator.push(
+                  context,
+                  MaterialPageRoute(
+                    builder: (context) => const ExerciseValidationPage(
+                      exercise: pushUp,
+                    ),
+                  ),
+                );
+              },
+              child: const Text('Push up'),
+            ),
+          ),
+          Center(
+            child: ElevatedButton(
+              onPressed: () {
+                Navigator.push(
+                  context,
+                  MaterialPageRoute(
+                    builder: (context) => const ExerciseValidationPage(
+                      exercise: squat,
+                    ),
+                  ),
+                );
+              },
+              child: const Text('Squat'),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 53 - 12
app/lib/exercises/exercises_validation/models/exercise.dart

@@ -1,6 +1,24 @@
+import 'package:body_detection/models/pose_landmark_type.dart';
+
 import '../../../navigation/utils/geometry_utils.dart';
 import '../widgets/pose_detector.dart';
 
+enum AuthorizedTypeIndex {
+  nose,
+  leftShoulder,
+  rightShoulder,
+  leftElbow,
+  rightElbow,
+  leftWrist,
+  rightWrist,
+  leftHip,
+  rightHip,
+  leftKnee,
+  rightKnee,
+  leftAnkle,
+  rightAnkle,
+}
+
 class Exercise {
   final int reps;
   final int series;
@@ -21,6 +39,22 @@ class Exercise {
   bool isAtEndMovement(MeanFilteredData meanFilteredData) {
     return endMovement.isAtPosition(meanFilteredData);
   }
+
+  static const authorizedType = [
+    PoseLandmarkType.nose,
+    PoseLandmarkType.leftShoulder,
+    PoseLandmarkType.rightShoulder,
+    PoseLandmarkType.leftElbow,
+    PoseLandmarkType.rightElbow,
+    PoseLandmarkType.leftWrist,
+    PoseLandmarkType.rightWrist,
+    PoseLandmarkType.leftHip,
+    PoseLandmarkType.rightHip,
+    PoseLandmarkType.leftKnee,
+    PoseLandmarkType.rightKnee,
+    PoseLandmarkType.leftAnkle,
+    PoseLandmarkType.rightAnkle,
+  ];
 }
 
 abstract class Criteria {
@@ -28,8 +62,8 @@ abstract class Criteria {
 }
 
 class CriteriaDistance implements Criteria {
-  final int jointStart;
-  final int jointEnd;
+  final AuthorizedTypeIndex jointStart;
+  final AuthorizedTypeIndex jointEnd;
   final int axis;
   final int threshold;
 
@@ -42,18 +76,17 @@ class CriteriaDistance implements Criteria {
 
   @override
   bool isAtPosition(MeanFilteredData meanFilteredData) {
-    final landmarks = meanFilteredData.toList();
-    final start = landmarks[jointStart][axis];
-    final end = landmarks[jointEnd][axis];
+    final start = meanFilteredData[jointStart.index][axis];
+    final end = meanFilteredData[jointEnd.index][axis];
     final distance = (start - end).abs();
     return distance < threshold;
   }
 }
 
 class CriteriaAngle implements Criteria {
-  final int jointStart;
-  final int jointCenter;
-  final int jointEnd;
+  final AuthorizedTypeIndex jointStart;
+  final AuthorizedTypeIndex jointCenter;
+  final AuthorizedTypeIndex jointEnd;
   final int threshold;
 
   const CriteriaAngle({
@@ -65,10 +98,18 @@ class CriteriaAngle implements Criteria {
 
   @override
   bool isAtPosition(MeanFilteredData meanFilteredData) {
-    final landmarks = meanFilteredData.toList();
-    final start = Point3D(x: landmarks[jointStart][0], y: landmarks[jointStart][1], z: landmarks[jointStart][2]);
-    final center = Point3D(x: landmarks[jointCenter][0], y: landmarks[jointCenter][1], z: landmarks[jointCenter][2]);
-    final end = Point3D(x: landmarks[jointEnd][0], y: landmarks[jointEnd][1], z: landmarks[jointEnd][2]);
+    final start = Point3D(
+        x: meanFilteredData[jointStart.index][0],
+        y: meanFilteredData[jointStart.index][1],
+        z: meanFilteredData[jointStart.index][2]);
+    final center = Point3D(
+        x: meanFilteredData[jointCenter.index][0],
+        y: meanFilteredData[jointCenter.index][1],
+        z: meanFilteredData[jointCenter.index][2]);
+    final end = Point3D(
+        x: meanFilteredData[jointEnd.index][0],
+        y: meanFilteredData[jointEnd.index][1],
+        z: meanFilteredData[jointEnd.index][2]);
     final angle = DistanceUtils.angleBetweenThreePoints(start, center, end).round();
     return angle > threshold;
   }

+ 22 - 0
app/lib/exercises/exercises_validation/models/push_up.dart

@@ -0,0 +1,22 @@
+import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
+
+const startMovement = CriteriaAngle(
+  jointStart: AuthorizedTypeIndex.rightShoulder,
+  jointCenter: AuthorizedTypeIndex.rightElbow,
+  jointEnd: AuthorizedTypeIndex.rightWrist,
+  threshold: 300,
+);
+
+const endMovement = CriteriaDistance(
+  jointStart: AuthorizedTypeIndex.leftShoulder,
+  jointEnd: AuthorizedTypeIndex.leftElbow,
+  axis: 1,
+  threshold: 40,
+);
+
+const pushUp = Exercise(
+  reps: 3,
+  series: 3,
+  startMovement: startMovement,
+  endMovement: endMovement,
+);

+ 5 - 12
app/lib/exercises/exercises_validation/models/squat.dart

@@ -1,22 +1,15 @@
 import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
 
-const rightShoulder = 2;
-const rightHip = 8;
-const rightKnee = 10;
-
 const startMovement = CriteriaAngle(
-  jointStart: rightShoulder,
-  jointCenter: rightHip,
-  jointEnd: rightKnee,
+  jointStart: AuthorizedTypeIndex.rightShoulder,
+  jointCenter: AuthorizedTypeIndex.rightHip,
+  jointEnd: AuthorizedTypeIndex.rightKnee,
   threshold: 320,
 );
 
-const leftHip = 7;
-const leftKnee = 9;
-
 const endMovement = CriteriaDistance(
-  jointStart: leftHip,
-  jointEnd: leftKnee,
+  jointStart: AuthorizedTypeIndex.leftHip,
+  jointEnd: AuthorizedTypeIndex.leftKnee,
   axis: 1,
   threshold: 40,
 );

+ 26 - 59
app/lib/exercises/exercises_validation/widgets/pose_detector.dart

@@ -1,7 +1,6 @@
 import 'dart:async';
 
 import 'package:body_detection/models/pose_landmark.dart';
-import 'package:body_detection/models/pose_landmark_type.dart';
 import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
 import 'package:rxdart/rxdart.dart';
 import 'package:body_detection/body_detection.dart';
@@ -11,7 +10,7 @@ import 'package:flutter/material.dart';
 
 import 'pose_painter.dart';
 
-typedef MeanFilteredData = Iterable<List<double>>;
+typedef MeanFilteredData = List<List<double>>;
 typedef LandmarkVariations = List<List<double>>;
 
 enum StepExercise {
@@ -21,9 +20,6 @@ enum StepExercise {
   end,
 }
 
-// TODO: authorized joint in exercise
-// TODO: correct axis depending on phone orientation. Determine axis based on skeleton(shoulder line, ...)
-
 class PoseDetector extends StatefulWidget {
   final Exercise exercise;
   const PoseDetector({required this.exercise, Key? key}) : super(key: key);
@@ -33,11 +29,11 @@ class PoseDetector extends StatefulWidget {
 }
 
 class _PoseDetectorState extends State<PoseDetector> {
-  static const buffer = 5;
+  static const meanFilterBuffer = 5;
   final List<StreamSubscription> _streamSubscriptions = [];
   final List<StreamController> _streamControllers = [];
-  final StreamController<Pose> _streamController = StreamController.broadcast();
-  final StreamController<StepExercise> _stepExerciseStream = StreamController.broadcast();
+  final StreamController<Pose> _poseController = StreamController.broadcast();
+  final StreamController<StepExercise> _stepExerciseController = StreamController.broadcast();
   Image? _cameraImage;
   Pose? _detectedPose;
   Size _imageSize = Size.zero;
@@ -48,28 +44,28 @@ class _PoseDetectorState extends State<PoseDetector> {
   @override
   initState() {
     super.initState();
-    _streamControllers.add(_streamController);
-    _streamControllers.add(_stepExerciseStream);
+    _streamControllers.add(_poseController);
+    _streamControllers.add(_stepExerciseController);
     _startCamera = _startCameraStream();
-    _meanFilterStream = _getMeanFilterStream(_streamController.stream);
+    _meanFilterStream = _getMeanFilterStream(_poseController.stream);
     _streamSubscriptions.add(
-        CombineLatestStream.combine2(_stepExerciseStream.stream, _meanFilterStream, (a, b) => [a, b]).listen((value) {
-      StepExercise stepExercise = value.first as StepExercise;
-      MeanFilteredData meanFilteredData = value.last as MeanFilteredData;
+        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) {
-        _stepExerciseStream.add(StepExercise.ready);
+        _stepExerciseController.add(StepExercise.ready);
       }
       if ((stepExercise == StepExercise.ready || stepExercise == StepExercise.start) && isEndOfExerciseMovement) {
-        _stepExerciseStream.add(StepExercise.end);
+        _stepExerciseController.add(StepExercise.end);
       }
       if (stepExercise == StepExercise.end && isStartOfExerciseMovement) {
-        _stepExerciseStream.add(StepExercise.start);
+        _stepExerciseController.add(StepExercise.start);
       }
     }));
-    _stepExerciseStream.add(StepExercise.notInPlace);
-    _repCounter = _stepExerciseStream.stream
+    _stepExerciseController.add(StepExercise.notInPlace);
+    _repCounter = _stepExerciseController.stream
         .where((event) => event == StepExercise.start)
         .scan((int accumulated, value, index) => accumulated + 1, 0);
   }
@@ -77,22 +73,22 @@ class _PoseDetectorState extends State<PoseDetector> {
   Stream<MeanFilteredData> _getMeanFilterStream(Stream<Pose> stream) {
     return stream
         .where((pose) => pose.landmarks.isNotEmpty)
-        .map((pose) => pose.landmarks.where((landmark) => authorizedType.contains(landmark.type)).toList())
+        .map((pose) => pose.landmarks.where((landmark) => Exercise.authorizedType.contains(landmark.type)).toList())
         // Get last [buffer] poses
-        .bufferCount(buffer, 1)
+        .bufferCount(meanFilterBuffer, 1)
         // Swap matrix [buffer] * [authorizedType.length]
         .map(_swapMatrixDimensions)
         // For every landmarks, get meanFilter of size [buffer]
-        .map((filteredLandmarks) => filteredLandmarks.map(_meanFilter));
+        .map((filteredLandmarks) => filteredLandmarks.map(_meanFilter).toList());
   }
 
   List<double> _meanFilter(List<PoseLandmark> landmarks) {
     return landmarks
         .map((landmark) => landmark.position)
         .map((position) => [
-              position.x / buffer,
-              position.y / buffer,
-              position.z / buffer,
+              position.x / meanFilterBuffer,
+              position.y / meanFilterBuffer,
+              position.z / meanFilterBuffer,
             ])
         .reduce((value, element) => [
               value[0] + element[0],
@@ -147,7 +143,7 @@ class _PoseDetectorState extends State<PoseDetector> {
 
   void _handlePose(Pose? pose) {
     if (!mounted) return;
-    if (pose != null) _streamController.add(pose);
+    if (pose != null) _poseController.add(pose);
     setState(() {
       _detectedPose = pose;
     });
@@ -185,7 +181,7 @@ class _PoseDetectorState extends State<PoseDetector> {
               ),
             ),
             StreamBuilder<StepExercise>(
-              stream: _stepExerciseStream.stream,
+              stream: _stepExerciseController.stream,
               builder: (context, snapshot) {
                 Color color;
                 if (!snapshot.hasData) {
@@ -202,7 +198,7 @@ class _PoseDetectorState extends State<PoseDetector> {
                       color = Colors.blue;
                       break;
                     case StepExercise.end:
-                      color = Colors.red;
+                      color = Colors.yellow;
                       break;
                   }
                 }
@@ -224,11 +220,11 @@ class _PoseDetectorState extends State<PoseDetector> {
                   children: [
                     Text(
                       "${repCounter % widget.exercise.reps}/${widget.exercise.reps}",
-                      style: TextStyle(fontSize: 40),
+                      style: const TextStyle(fontSize: 40),
                     ),
                     Text(
                       "${repCounter ~/ widget.exercise.reps}/${widget.exercise.series}",
-                      style: TextStyle(fontSize: 40),
+                      style: const TextStyle(fontSize: 40),
                     ),
                   ],
                 );
@@ -240,33 +236,4 @@ class _PoseDetectorState extends State<PoseDetector> {
     );
   }
 
-  static const authorizedType = [
-    PoseLandmarkType.nose,
-    PoseLandmarkType.leftShoulder,
-    PoseLandmarkType.rightShoulder,
-    PoseLandmarkType.leftElbow,
-    PoseLandmarkType.rightElbow,
-    PoseLandmarkType.leftWrist,
-    PoseLandmarkType.rightWrist,
-    PoseLandmarkType.leftHip,
-    PoseLandmarkType.rightHip,
-    PoseLandmarkType.leftKnee,
-    PoseLandmarkType.rightKnee,
-    PoseLandmarkType.leftAnkle,
-    PoseLandmarkType.rightAnkle,
-  ];
 }
-
-
-/*
-
-GETTING IN POSITION:
-  CHECK IF EVERY NECESSARY JOINT ARE ON SCREEN (reliability > 0.8)
-  CHECK IF START POSITION IS OKAY (for squat, if knee, hip, shoulder aligned)
-
-COUTING REPETITION:
-  FROM BEGINNING TO END:
-    - BEGINNING: defined by start position, get position of interesting joint
-    - END: defined by positions/distance interesting joints (knee and hip same level for squat,
-          elbow and should same level for push up)
- */

+ 0 - 52
app/lib/exercises/exercises_validation/widgets/pose_painter.dart

@@ -36,35 +36,6 @@ class PosePainter extends CustomPainter {
         canvas.drawLine(point1, point2, linePaint);
       }
     }
-    // for (final part in pose!.landmarks) {
-    //   // Landmark points
-    //   canvas.drawCircle(offsetForPart(part), 5, pointPaint);
-    //   if (part.type.isLeftSide) {
-    //     canvas.drawCircle(offsetForPart(part), 3, leftPointPaint);
-    //   } else if (part.type.isRightSide) {
-    //     canvas.drawCircle(offsetForPart(part), 3, rightPointPaint);
-    //   }
-
-    //   // Landmark labels
-    //   TextSpan span = TextSpan(
-    //     text: part.type.toString().substring(16),
-    //     style: const TextStyle(
-    //       color: Color.fromRGBO(0, 128, 255, 1),
-    //       fontSize: 10,
-    //       shadows: [
-    //         Shadow(
-    //           color: Color.fromRGBO(255, 255, 255, 1),
-    //           offset: Offset(1, 1),
-    //           blurRadius: 1,
-    //         ),
-    //       ],
-    //     ),
-    //   );
-    //   TextPainter tp = TextPainter(text: span, textAlign: TextAlign.left);
-    //   tp.textDirection = TextDirection.ltr;
-    //   tp.layout();
-    //   tp.paint(canvas, offsetForPart(part));
-    // }
   }
 
   @override
@@ -73,23 +44,11 @@ class PosePainter extends CustomPainter {
   }
 
   static const List<List<PoseLandmarkType>> connections = [
-    // [PoseLandmarkType.leftEar, PoseLandmarkType.leftEyeOuter],
-    // [PoseLandmarkType.leftEyeOuter, PoseLandmarkType.leftEye],
-    // [PoseLandmarkType.leftEye, PoseLandmarkType.leftEyeInner],
-    // [PoseLandmarkType.leftEyeInner, PoseLandmarkType.nose],
-    // [PoseLandmarkType.nose, PoseLandmarkType.rightEyeInner],
-    // [PoseLandmarkType.rightEyeInner, PoseLandmarkType.rightEye],
-    // [PoseLandmarkType.rightEye, PoseLandmarkType.rightEyeOuter],
-    // [PoseLandmarkType.rightEyeOuter, PoseLandmarkType.rightEar],
-    // [PoseLandmarkType.mouthLeft, PoseLandmarkType.mouthRight],
     [PoseLandmarkType.leftShoulder, PoseLandmarkType.rightShoulder],
     [PoseLandmarkType.leftShoulder, PoseLandmarkType.leftHip],
     [PoseLandmarkType.rightShoulder, PoseLandmarkType.rightHip],
     [PoseLandmarkType.rightShoulder, PoseLandmarkType.rightElbow],
     [PoseLandmarkType.rightWrist, PoseLandmarkType.rightElbow],
-    // [PoseLandmarkType.rightWrist, PoseLandmarkType.rightThumb],
-    // [PoseLandmarkType.rightWrist, PoseLandmarkType.rightIndexFinger],
-    // [PoseLandmarkType.rightWrist, PoseLandmarkType.rightPinkyFinger],
     [PoseLandmarkType.leftHip, PoseLandmarkType.rightHip],
     [PoseLandmarkType.leftHip, PoseLandmarkType.leftKnee],
     [PoseLandmarkType.rightHip, PoseLandmarkType.rightKnee],
@@ -97,16 +56,5 @@ class PosePainter extends CustomPainter {
     [PoseLandmarkType.leftKnee, PoseLandmarkType.leftAnkle],
     [PoseLandmarkType.leftElbow, PoseLandmarkType.leftShoulder],
     [PoseLandmarkType.leftWrist, PoseLandmarkType.leftElbow],
-    // [PoseLandmarkType.leftWrist, PoseLandmarkType.leftThumb],
-    // [PoseLandmarkType.leftWrist, PoseLandmarkType.leftIndexFinger],
-    // [PoseLandmarkType.leftWrist, PoseLandmarkType.leftPinkyFinger],
-    // [PoseLandmarkType.leftAnkle, PoseLandmarkType.leftHeel],
-    // [PoseLandmarkType.leftAnkle, PoseLandmarkType.leftToe],
-    // [PoseLandmarkType.rightAnkle, PoseLandmarkType.rightHeel],
-    // [PoseLandmarkType.rightAnkle, PoseLandmarkType.rightToe],
-    // [PoseLandmarkType.rightHeel, PoseLandmarkType.rightToe],
-    // [PoseLandmarkType.leftHeel, PoseLandmarkType.leftToe],
-    // [PoseLandmarkType.rightIndexFinger, PoseLandmarkType.rightPinkyFinger],
-    // [PoseLandmarkType.leftIndexFinger, PoseLandmarkType.leftPinkyFinger],
   ];
 }

+ 3 - 4
app/lib/main.dart

@@ -1,11 +1,10 @@
 import 'package:firebase_core/firebase_core.dart';
 import 'package:flutter/material.dart';
 import 'package:latlong2/latlong.dart';
-import 'package:physigo/exercises/exercises_validation/models/squat.dart';
+import 'package:physigo/exercises/exercises_page.dart';
 import 'navigation/navigation_page.dart';
 
 import 'firebase_options.dart';
-import 'package:physigo/exercises/exercises_validation/exercise_validation_page.dart';
 
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
@@ -59,11 +58,11 @@ class HomePage extends StatelessWidget {
               Navigator.push(
                 context,
                 MaterialPageRoute(
-                  builder: (context) => const ExerciseValidationPage(exercise: squat,),
+                  builder: (context) => const ExercisesPage(),
                 ),
               );
             },
-            child: const Text('Exercise Validation'),
+            child: const Text('Exercises'),
           ),
         ],
       ),