pose_detector.dart 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import 'dart:async';
  2. import 'package:body_detection/models/pose_landmark.dart';
  3. import 'package:physigo/exercises/exercises_validation/models/exercise.dart';
  4. import 'package:physigo/exercises/services/exercises_service.dart';
  5. import 'package:rxdart/rxdart.dart';
  6. import 'package:body_detection/body_detection.dart';
  7. import 'package:body_detection/models/image_result.dart';
  8. import 'package:body_detection/models/pose.dart';
  9. import 'package:flutter/material.dart';
  10. import 'exercise_indicator.dart';
  11. import 'pose_painter.dart';
  12. typedef MeanFilteredData = List<List<double>>;
  13. typedef LandmarkVariations = List<List<double>>;
  14. enum StepExercise {
  15. notInPlace,
  16. ready,
  17. start,
  18. end,
  19. }
  20. class PoseDetector extends StatefulWidget {
  21. final Exercise exercise;
  22. const PoseDetector({required this.exercise, Key? key}) : super(key: key);
  23. @override
  24. State<PoseDetector> createState() => _PoseDetectorState();
  25. }
  26. class _PoseDetectorState extends State<PoseDetector> {
  27. static const meanFilterBuffer = 5;
  28. final List<StreamSubscription> _streamSubscriptions = [];
  29. final List<StreamController> _streamControllers = [];
  30. final StreamController<Pose> _poseController = StreamController.broadcast();
  31. final StreamController<StepExercise> _stepExerciseController = StreamController.broadcast();
  32. late final Stream<List<PoseLandmark>> _exerciseJointsStream;
  33. Image? _cameraImage;
  34. Pose? _detectedPose;
  35. Size _imageSize = Size.zero;
  36. late Future<void> _startCamera;
  37. late Stream<MeanFilteredData> _meanFilterStream;
  38. late Stream<int> _repCounter;
  39. @override
  40. initState() {
  41. super.initState();
  42. _streamControllers.add(_poseController);
  43. _streamControllers.add(_stepExerciseController);
  44. _exerciseJointsStream = _getExerciseJointsStream(_poseController.stream);
  45. CombineLatestStream.combine2(
  46. _exerciseJointsStream
  47. .map((event) => event.where((e) => widget.exercise.jointsOnScreen.contains(e.type)).toList()),
  48. _stepExerciseController.stream,
  49. (a, b) => [a, b]).listen(_handleNotInPlacePosition);
  50. CombineLatestStream.combine2(
  51. _exerciseJointsStream
  52. .map((event) => event.where((e) => widget.exercise.jointsOnScreen.contains(e.type)).toList())
  53. .bufferCount(5, 1),
  54. _stepExerciseController.stream,
  55. (a, b) => [a, b]).listen(_handleReadyPosition);
  56. _startCamera = _startCameraStream();
  57. _meanFilterStream = _getMeanFilterStream(_exerciseJointsStream);
  58. _streamSubscriptions.add(
  59. CombineLatestStream.combine2(_stepExerciseController.stream, _meanFilterStream, (a, b) => [a, b]).listen(
  60. (value) {
  61. final stepExercise = value.first as StepExercise;
  62. if (stepExercise == StepExercise.notInPlace) return;
  63. final meanFilteredData = value.last as MeanFilteredData;
  64. final isStartOfExerciseMovement = widget.exercise.isAtStartMovement(meanFilteredData);
  65. final isEndOfExerciseMovement = widget.exercise.isAtEndMovement(meanFilteredData);
  66. if ((stepExercise == StepExercise.start) && isEndOfExerciseMovement) {
  67. _stepExerciseController.add(StepExercise.end);
  68. }
  69. if ((stepExercise == StepExercise.end || stepExercise == StepExercise.ready) && isStartOfExerciseMovement) {
  70. _stepExerciseController.add(StepExercise.start);
  71. }
  72. },
  73. ),
  74. );
  75. _stepExerciseController.add(StepExercise.notInPlace);
  76. _repCounter = _stepExerciseController.stream
  77. .pairwise()
  78. .where((event) => event.first == StepExercise.end && event.last == StepExercise.start)
  79. .scan((int accumulated, value, index) => accumulated + 1, 0);
  80. _repCounter.listen((_) {
  81. ExercisesService.addScoreToExercise(widget.exercise, 5 * (widget.exercise.difficulty + 1));
  82. });
  83. }
  84. void _handleNotInPlacePosition(List<Object?> event) {
  85. final poseLandmarks = event.first as List<PoseLandmark>;
  86. final stepExercise = event.last as StepExercise;
  87. if (stepExercise == StepExercise.notInPlace) return;
  88. for (final poseLandmark in poseLandmarks) {
  89. if (poseLandmark.inFrameLikelihood < 0.8) {
  90. _stepExerciseController.add(StepExercise.notInPlace);
  91. return;
  92. }
  93. }
  94. }
  95. void _handleReadyPosition(List<Object?> event) {
  96. final poseLandmarksBuffered = event.first as List<List<PoseLandmark>>;
  97. final stepExercise = event.last as StepExercise;
  98. if (stepExercise != StepExercise.notInPlace) return;
  99. for (final poseLandmarks in poseLandmarksBuffered) {
  100. for (final poseLandmark in poseLandmarks) {
  101. if (poseLandmark.inFrameLikelihood < 0.8) return;
  102. }
  103. }
  104. _stepExerciseController.add(StepExercise.ready);
  105. }
  106. Stream<List<PoseLandmark>> _getExerciseJointsStream(Stream<Pose> stream) {
  107. return stream
  108. .where((pose) => pose.landmarks.isNotEmpty)
  109. .map((pose) => pose.landmarks.where((landmark) => Exercise.authorizedType.contains(landmark.type)).toList());
  110. }
  111. Stream<MeanFilteredData> _getMeanFilterStream(Stream<List<PoseLandmark>> stream) {
  112. return stream
  113. // Get last [buffer] poses
  114. .bufferCount(meanFilterBuffer, 1)
  115. // Swap matrix [buffer] * [authorizedType.length]
  116. .map(_swapMatrixDimensions)
  117. // For every landmarks, get meanFilter of size [buffer]
  118. .map((filteredLandmarks) => filteredLandmarks.map(_meanFilter).toList());
  119. }
  120. List<double> _meanFilter(List<PoseLandmark> landmarks) {
  121. return landmarks
  122. .map((landmark) => landmark.position)
  123. .map((position) => [
  124. position.x / meanFilterBuffer,
  125. position.y / meanFilterBuffer,
  126. position.z / meanFilterBuffer,
  127. ])
  128. .reduce((value, element) => [
  129. value[0] + element[0],
  130. value[1] + element[1],
  131. value[2] + element[2],
  132. ]);
  133. }
  134. List<List<T>> _swapMatrixDimensions<T>(List<List<T>> matrix) {
  135. final height = matrix.length;
  136. final width = matrix[0].length;
  137. List<List<T>> newMatrix = [];
  138. for (int col = 0; col < width; col++) {
  139. List<T> newRow = [];
  140. for (int row = 0; row < height; row++) {
  141. newRow.add(matrix[row][col]);
  142. }
  143. newMatrix.add(newRow);
  144. }
  145. return newMatrix;
  146. }
  147. Future<void> _startCameraStream() async {
  148. await BodyDetection.startCameraStream(onFrameAvailable: _handleCameraImage, onPoseAvailable: _handlePose);
  149. await BodyDetection.enablePoseDetection();
  150. }
  151. Future<void> _stopCameraStream() async {
  152. await BodyDetection.disablePoseDetection();
  153. await BodyDetection.stopCameraStream();
  154. }
  155. void _handleCameraImage(ImageResult result) {
  156. if (!mounted) return;
  157. // To avoid a memory leak issue.
  158. // https://github.com/flutter/flutter/issues/60160
  159. PaintingBinding.instance?.imageCache?.clear();
  160. PaintingBinding.instance?.imageCache?.clearLiveImages();
  161. final image = Image.memory(
  162. result.bytes,
  163. gaplessPlayback: true,
  164. fit: BoxFit.contain,
  165. );
  166. setState(() {
  167. _cameraImage = image;
  168. _imageSize = result.size;
  169. });
  170. }
  171. void _handlePose(Pose? pose) {
  172. if (!mounted) return;
  173. if (pose != null) {
  174. _poseController.add(pose);
  175. }
  176. setState(() {
  177. _detectedPose = pose;
  178. });
  179. }
  180. @override
  181. void dispose() {
  182. _stopCameraStream();
  183. for (final ss in _streamSubscriptions) {
  184. ss.cancel();
  185. }
  186. for (final sc in _streamControllers) {
  187. sc.close();
  188. }
  189. super.dispose();
  190. }
  191. @override
  192. Widget build(BuildContext context) {
  193. return FutureBuilder<void>(
  194. future: _startCamera,
  195. builder: (context, snapshot) {
  196. if (snapshot.connectionState == ConnectionState.waiting) {
  197. return const Center(child: CircularProgressIndicator());
  198. }
  199. return Column(
  200. children: [
  201. Center(
  202. child: CustomPaint(
  203. child: _cameraImage,
  204. foregroundPainter: PosePainter(
  205. pose: _detectedPose,
  206. imageSize: _imageSize,
  207. ),
  208. ),
  209. ),
  210. ExerciseIndicator(
  211. stepExerciseController: _stepExerciseController,
  212. repCounter: _repCounter,
  213. exercise: widget.exercise,
  214. ),
  215. ],
  216. );
  217. },
  218. );
  219. }
  220. }