Bläddra i källkod

feat: model exercise

Léo Salé 3 år sedan
förälder
incheckning
bd798315fe

+ 9 - 7
app/lib/exercises/exercises_validation/exercise_validation_page.dart

@@ -1,10 +1,12 @@
 import 'package:flutter/material.dart';
-import 'package:path_provider/path_provider.dart';
-import 'package:physigo/exercises/exercises_validation/utils/permissions_utils.dart';
-import 'package:physigo/exercises/exercises_validation/widgets/pose_detector.dart';
+
+import 'models/exercise.dart';
+import 'utils/permissions_utils.dart';
+import 'widgets/pose_detector.dart';
 
 class ExerciseValidationPage extends StatelessWidget {
-  const ExerciseValidationPage({Key? key}) : super(key: key);
+  final Exercise exercise;
+  const ExerciseValidationPage({required this.exercise, Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
@@ -12,15 +14,15 @@ class ExerciseValidationPage extends StatelessWidget {
       body: FutureBuilder<void>(
         future: PermissionUtils.determineCameraPermission(),
         builder: ((context, snapshot) {
-          getExternalStorageDirectory().then(print);
-
           if (snapshot.connectionState == ConnectionState.waiting) {
             return const Center(child: CircularProgressIndicator());
           }
           if (snapshot.hasError) {
             return Center(child: Text(snapshot.error.toString()));
           }
-          return const PoseDetector();
+          return PoseDetector(
+            exercise: exercise,
+          );
         }),
       ),
     );

+ 75 - 0
app/lib/exercises/exercises_validation/models/exercise.dart

@@ -0,0 +1,75 @@
+import '../../../navigation/utils/geometry_utils.dart';
+import '../widgets/pose_detector.dart';
+
+class Exercise {
+  final int reps;
+  final int series;
+  final Criteria startMovement;
+  final Criteria endMovement;
+
+  const Exercise({
+    required this.reps,
+    required this.series,
+    required this.startMovement,
+    required this.endMovement,
+  });
+
+  bool isAtStartMovement(MeanFilteredData meanFilteredData) {
+    return startMovement.isAtPosition(meanFilteredData);
+  }
+
+  bool isAtEndMovement(MeanFilteredData meanFilteredData) {
+    return endMovement.isAtPosition(meanFilteredData);
+  }
+}
+
+abstract class Criteria {
+  bool isAtPosition(MeanFilteredData meanFilteredData);
+}
+
+class CriteriaDistance implements Criteria {
+  final int jointStart;
+  final int jointEnd;
+  final int axis;
+  final int threshold;
+
+  const CriteriaDistance({
+    required this.jointStart,
+    required this.jointEnd,
+    required this.axis,
+    required this.threshold,
+  });
+
+  @override
+  bool isAtPosition(MeanFilteredData meanFilteredData) {
+    final landmarks = meanFilteredData.toList();
+    final start = landmarks[jointStart][axis];
+    final end = landmarks[jointEnd][axis];
+    final distance = (start - end).abs();
+    return distance < threshold;
+  }
+}
+
+class CriteriaAngle implements Criteria {
+  final int jointStart;
+  final int jointCenter;
+  final int jointEnd;
+  final int threshold;
+
+  const CriteriaAngle({
+    required this.jointStart,
+    required this.jointCenter,
+    required this.jointEnd,
+    required this.threshold,
+  });
+
+  @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 angle = DistanceUtils.angleBetweenThreePoints(start, center, end).round();
+    return angle > threshold;
+  }
+}

+ 29 - 0
app/lib/exercises/exercises_validation/models/squat.dart

@@ -0,0 +1,29 @@
+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,
+  threshold: 320,
+);
+
+const leftHip = 7;
+const leftKnee = 9;
+
+const endMovement = CriteriaDistance(
+  jointStart: leftHip,
+  jointEnd: leftKnee,
+  axis: 1,
+  threshold: 40,
+);
+
+const squat = Exercise(
+  reps: 3,
+  series: 3,
+  startMovement: startMovement,
+  endMovement: endMovement,
+);

+ 34 - 43
app/lib/exercises/exercises_validation/widgets/pose_detector.dart

@@ -2,7 +2,7 @@ import 'dart:async';
 
 import 'package:body_detection/models/pose_landmark.dart';
 import 'package:body_detection/models/pose_landmark_type.dart';
-import 'package:physigo/navigation/utils/geometry_utils.dart';
+import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
 import 'package:rxdart/rxdart.dart';
 import 'package:body_detection/body_detection.dart';
 import 'package:body_detection/models/image_result.dart';
@@ -21,34 +21,43 @@ 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 {
-  const PoseDetector({Key? key}) : super(key: key);
+  final Exercise exercise;
+  const PoseDetector({required this.exercise, Key? key}) : super(key: key);
 
   @override
   State<PoseDetector> createState() => _PoseDetectorState();
 }
 
 class _PoseDetectorState extends State<PoseDetector> {
-  static const buffer = 10;
+  static const buffer = 5;
+  final List<StreamSubscription> _streamSubscriptions = [];
+  final List<StreamController> _streamControllers = [];
   final StreamController<Pose> _streamController = StreamController.broadcast();
+  final StreamController<StepExercise> _stepExerciseStream = StreamController.broadcast();
   Image? _cameraImage;
   Pose? _detectedPose;
   Size _imageSize = Size.zero;
   late Future<void> _startCamera;
   late Stream<MeanFilteredData> _meanFilterStream;
-  StreamController<StepExercise> _stepExerciseStream = StreamController.broadcast();
   late Stream<int> _repCounter;
 
   @override
   initState() {
     super.initState();
+    _streamControllers.add(_streamController);
+    _streamControllers.add(_stepExerciseStream);
     _startCamera = _startCameraStream();
     _meanFilterStream = _getMeanFilterStream(_streamController.stream);
-    CombineLatestStream.combine2(_stepExerciseStream.stream, _meanFilterStream, (a, b) => [a, b]).listen((value) {
+    _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;
-      final isStartOfExerciseMovement = _isAtStartOfExerciseMovement(meanFilteredData);
-      final isEndOfExerciseMovement = _isAtEndOfExerciseMovement(meanFilteredData);
+      final isStartOfExerciseMovement = widget.exercise.isAtStartMovement(meanFilteredData);
+      final isEndOfExerciseMovement = widget.exercise.isAtEndMovement(meanFilteredData);
       if (stepExercise == StepExercise.notInPlace && isStartOfExerciseMovement) {
         _stepExerciseStream.add(StepExercise.ready);
       }
@@ -58,43 +67,13 @@ class _PoseDetectorState extends State<PoseDetector> {
       if (stepExercise == StepExercise.end && isStartOfExerciseMovement) {
         _stepExerciseStream.add(StepExercise.start);
       }
-    });
+    }));
     _stepExerciseStream.add(StepExercise.notInPlace);
     _repCounter = _stepExerciseStream.stream
         .where((event) => event == StepExercise.start)
         .scan((int accumulated, value, index) => accumulated + 1, 0);
   }
 
-  bool _isAtStartOfExerciseMovement(MeanFilteredData meanFilteredData) {
-    final landmarks = meanFilteredData.toList();
-    final rightShoulder = Point3D(x: landmarks[2][0], y: landmarks[2][1], z: landmarks[2][2]);
-    final rightHip = Point3D(x: landmarks[8][0], y: landmarks[8][1], z: landmarks[8][2]);
-    final rightKnee = Point3D(x: landmarks[10][0], y: landmarks[10][1], z: landmarks[10][2]);
-    final angleRight = DistanceUtils.angleBetweenThreePoints(rightShoulder, rightHip, rightKnee).round();
-    final leftShoulder = Point3D(x: landmarks[1][0], y: landmarks[1][1], z: landmarks[1][2]);
-    final leftHip = Point3D(x: landmarks[7][0], y: landmarks[7][1], z: landmarks[7][2]);
-    final leftKnee = Point3D(x: landmarks[9][0], y: landmarks[9][1], z: landmarks[9][2]);
-    final angleLeft = DistanceUtils.angleBetweenThreePoints(leftShoulder, leftHip, leftKnee).round();
-    if (angleLeft > 320 && angleRight > 320) {
-      return true;
-    }
-    return false;
-  }
-
-  bool _isAtEndOfExerciseMovement(MeanFilteredData meanFilteredData) {
-    final landmarks = meanFilteredData.toList();
-    final yRightHip = landmarks[8][1];
-    final yRightKnee = landmarks[10][1];
-    final yDistanceRightHipKnee = (yRightHip - yRightKnee).abs();
-    final yLeftHip = landmarks[8][1];
-    final yLeftKnee = landmarks[10][1];
-    final yDistanceLeftHipKnee = (yLeftHip - yLeftKnee).abs();
-    if (yDistanceRightHipKnee < 40 && yDistanceLeftHipKnee < 40) {
-      return true;
-    }
-    return false;
-  }
-
   Stream<MeanFilteredData> _getMeanFilterStream(Stream<Pose> stream) {
     return stream
         .where((pose) => pose.landmarks.isNotEmpty)
@@ -177,7 +156,12 @@ class _PoseDetectorState extends State<PoseDetector> {
   @override
   void dispose() {
     _stopCameraStream();
-    _streamController.close();
+    for (final ss in _streamSubscriptions) {
+      ss.cancel();
+    }
+    for (final sc in _streamControllers) {
+      sc.close();
+    }
     super.dispose();
   }
 
@@ -193,7 +177,6 @@ class _PoseDetectorState extends State<PoseDetector> {
           children: [
             Center(
               child: CustomPaint(
-                // size: _imageSize,
                 child: _cameraImage,
                 foregroundPainter: PosePainter(
                   pose: _detectedPose,
@@ -237,9 +220,17 @@ class _PoseDetectorState extends State<PoseDetector> {
                 if (snapshot.hasData) {
                   repCounter = snapshot.data!;
                 }
-                return Text(
-                  "$repCounter",
-                  style: TextStyle(fontSize: 40),
+                return Column(
+                  children: [
+                    Text(
+                      "${repCounter % widget.exercise.reps}/${widget.exercise.reps}",
+                      style: TextStyle(fontSize: 40),
+                    ),
+                    Text(
+                      "${repCounter ~/ widget.exercise.reps}/${widget.exercise.series}",
+                      style: TextStyle(fontSize: 40),
+                    ),
+                  ],
                 );
               },
             )

+ 2 - 1
app/lib/main.dart

@@ -1,6 +1,7 @@
 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 'navigation/navigation_page.dart';
 
 import 'firebase_options.dart';
@@ -58,7 +59,7 @@ class HomePage extends StatelessWidget {
               Navigator.push(
                 context,
                 MaterialPageRoute(
-                  builder: (context) => ExerciseValidationPage(),
+                  builder: (context) => const ExerciseValidationPage(exercise: squat,),
                 ),
               );
             },

+ 1 - 92
app/pubspec.lock

@@ -64,20 +64,6 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.2.0"
-  ffi:
-    dependency: transitive
-    description:
-      name: ffi
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.1.2"
-  file:
-    dependency: transitive
-    description:
-      name: file
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "6.1.2"
   firebase_core:
     dependency: "direct main"
     description:
@@ -268,55 +254,6 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.0"
-  path_provider:
-    dependency: "direct main"
-    description:
-      name: path_provider
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.0.9"
-  path_provider_android:
-    dependency: transitive
-    description:
-      name: path_provider_android
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.0.13"
-  path_provider_ios:
-    dependency: transitive
-    description:
-      name: path_provider_ios
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.0.8"
-  path_provider_linux:
-    dependency: transitive
-    description:
-      name: path_provider_linux
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.1.5"
-  path_provider_macos:
-    dependency: transitive
-    description:
-      name: path_provider_macos
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.0.5"
-  path_provider_platform_interface:
-    dependency: transitive
-    description:
-      name: path_provider_platform_interface
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.0.3"
-  path_provider_windows:
-    dependency: transitive
-    description:
-      name: path_provider_windows
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.0.5"
   permission_handler:
     dependency: "direct main"
     description:
@@ -352,13 +289,6 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.1.0"
-  platform:
-    dependency: transitive
-    description:
-      name: platform
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "3.1.0"
   plugin_platform_interface:
     dependency: transitive
     description:
@@ -373,13 +303,6 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.4"
-  process:
-    dependency: transitive
-    description:
-      name: process
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "4.2.4"
   proj4dart:
     dependency: transitive
     description:
@@ -483,13 +406,6 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.1"
-  win32:
-    dependency: transitive
-    description:
-      name: win32
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "2.5.2"
   wkt_parser:
     dependency: transitive
     description:
@@ -497,13 +413,6 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.0.0"
-  xdg_directories:
-    dependency: transitive
-    description:
-      name: xdg_directories
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "0.2.0+1"
 sdks:
   dart: ">=2.16.2 <3.0.0"
-  flutter: ">=2.8.1"
+  flutter: ">=2.8.0"

+ 0 - 1
app/pubspec.yaml

@@ -41,7 +41,6 @@ dependencies:
   permission_handler: ^9.2.0
   body_detection: ^0.0.3
   rxdart: ^0.27.3
-  path_provider: ^2.0.9
 
 dev_dependencies:
   flutter_test: