|
|
@@ -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),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
);
|
|
|
},
|
|
|
)
|