pose_detector.dart 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import 'dart:async';
  2. import 'package:body_detection/models/pose_landmark.dart';
  3. import 'package:body_detection/models/pose_landmark_type.dart';
  4. import 'package:physigo/exercises/exercises_validation/models/exercise.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 'pose_painter.dart';
  11. typedef MeanFilteredData = Iterable<List<double>>;
  12. typedef LandmarkVariations = List<List<double>>;
  13. enum StepExercise {
  14. notInPlace,
  15. ready,
  16. start,
  17. end,
  18. }
  19. // TODO: authorized joint in exercise
  20. // TODO: correct axis depending on phone orientation. Determine axis based on skeleton(shoulder line, ...)
  21. class PoseDetector extends StatefulWidget {
  22. final Exercise exercise;
  23. const PoseDetector({required this.exercise, Key? key}) : super(key: key);
  24. @override
  25. State<PoseDetector> createState() => _PoseDetectorState();
  26. }
  27. class _PoseDetectorState extends State<PoseDetector> {
  28. static const buffer = 5;
  29. final List<StreamSubscription> _streamSubscriptions = [];
  30. final List<StreamController> _streamControllers = [];
  31. final StreamController<Pose> _streamController = StreamController.broadcast();
  32. final StreamController<StepExercise> _stepExerciseStream = StreamController.broadcast();
  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(_streamController);
  43. _streamControllers.add(_stepExerciseStream);
  44. _startCamera = _startCameraStream();
  45. _meanFilterStream = _getMeanFilterStream(_streamController.stream);
  46. _streamSubscriptions.add(
  47. CombineLatestStream.combine2(_stepExerciseStream.stream, _meanFilterStream, (a, b) => [a, b]).listen((value) {
  48. StepExercise stepExercise = value.first as StepExercise;
  49. MeanFilteredData meanFilteredData = value.last as MeanFilteredData;
  50. final isStartOfExerciseMovement = widget.exercise.isAtStartMovement(meanFilteredData);
  51. final isEndOfExerciseMovement = widget.exercise.isAtEndMovement(meanFilteredData);
  52. if (stepExercise == StepExercise.notInPlace && isStartOfExerciseMovement) {
  53. _stepExerciseStream.add(StepExercise.ready);
  54. }
  55. if ((stepExercise == StepExercise.ready || stepExercise == StepExercise.start) && isEndOfExerciseMovement) {
  56. _stepExerciseStream.add(StepExercise.end);
  57. }
  58. if (stepExercise == StepExercise.end && isStartOfExerciseMovement) {
  59. _stepExerciseStream.add(StepExercise.start);
  60. }
  61. }));
  62. _stepExerciseStream.add(StepExercise.notInPlace);
  63. _repCounter = _stepExerciseStream.stream
  64. .where((event) => event == StepExercise.start)
  65. .scan((int accumulated, value, index) => accumulated + 1, 0);
  66. }
  67. Stream<MeanFilteredData> _getMeanFilterStream(Stream<Pose> stream) {
  68. return stream
  69. .where((pose) => pose.landmarks.isNotEmpty)
  70. .map((pose) => pose.landmarks.where((landmark) => authorizedType.contains(landmark.type)).toList())
  71. // Get last [buffer] poses
  72. .bufferCount(buffer, 1)
  73. // Swap matrix [buffer] * [authorizedType.length]
  74. .map(_swapMatrixDimensions)
  75. // For every landmarks, get meanFilter of size [buffer]
  76. .map((filteredLandmarks) => filteredLandmarks.map(_meanFilter));
  77. }
  78. List<double> _meanFilter(List<PoseLandmark> landmarks) {
  79. return landmarks
  80. .map((landmark) => landmark.position)
  81. .map((position) => [
  82. position.x / buffer,
  83. position.y / buffer,
  84. position.z / buffer,
  85. ])
  86. .reduce((value, element) => [
  87. value[0] + element[0],
  88. value[1] + element[1],
  89. value[2] + element[2],
  90. ]);
  91. }
  92. List<List<T>> _swapMatrixDimensions<T>(List<List<T>> matrix) {
  93. final height = matrix.length;
  94. final width = matrix[0].length;
  95. List<List<T>> newMatrix = [];
  96. for (int col = 0; col < width; col++) {
  97. List<T> newRow = [];
  98. for (int row = 0; row < height; row++) {
  99. newRow.add(matrix[row][col]);
  100. }
  101. newMatrix.add(newRow);
  102. }
  103. return newMatrix;
  104. }
  105. Future<void> _startCameraStream() async {
  106. await BodyDetection.startCameraStream(onFrameAvailable: _handleCameraImage, onPoseAvailable: _handlePose);
  107. await BodyDetection.enablePoseDetection();
  108. }
  109. Future<void> _stopCameraStream() async {
  110. await BodyDetection.disablePoseDetection();
  111. await BodyDetection.stopCameraStream();
  112. }
  113. void _handleCameraImage(ImageResult result) {
  114. if (!mounted) return;
  115. // To avoid a memory leak issue.
  116. // https://github.com/flutter/flutter/issues/60160
  117. PaintingBinding.instance?.imageCache?.clear();
  118. PaintingBinding.instance?.imageCache?.clearLiveImages();
  119. final image = Image.memory(
  120. result.bytes,
  121. gaplessPlayback: true,
  122. fit: BoxFit.contain,
  123. );
  124. setState(() {
  125. _cameraImage = image;
  126. _imageSize = result.size;
  127. });
  128. }
  129. void _handlePose(Pose? pose) {
  130. if (!mounted) return;
  131. if (pose != null) _streamController.add(pose);
  132. setState(() {
  133. _detectedPose = pose;
  134. });
  135. }
  136. @override
  137. void dispose() {
  138. _stopCameraStream();
  139. for (final ss in _streamSubscriptions) {
  140. ss.cancel();
  141. }
  142. for (final sc in _streamControllers) {
  143. sc.close();
  144. }
  145. super.dispose();
  146. }
  147. @override
  148. Widget build(BuildContext context) {
  149. return FutureBuilder<void>(
  150. future: _startCamera,
  151. builder: (context, snapshot) {
  152. if (snapshot.connectionState == ConnectionState.waiting) {
  153. return const Center(child: CircularProgressIndicator());
  154. }
  155. return Column(
  156. children: [
  157. Center(
  158. child: CustomPaint(
  159. child: _cameraImage,
  160. foregroundPainter: PosePainter(
  161. pose: _detectedPose,
  162. imageSize: _imageSize,
  163. ),
  164. ),
  165. ),
  166. StreamBuilder<StepExercise>(
  167. stream: _stepExerciseStream.stream,
  168. builder: (context, snapshot) {
  169. Color color;
  170. if (!snapshot.hasData) {
  171. color = Colors.black;
  172. } else {
  173. switch (snapshot.data!) {
  174. case StepExercise.notInPlace:
  175. color = Colors.black;
  176. break;
  177. case StepExercise.ready:
  178. color = Colors.green;
  179. break;
  180. case StepExercise.start:
  181. color = Colors.blue;
  182. break;
  183. case StepExercise.end:
  184. color = Colors.red;
  185. break;
  186. }
  187. }
  188. return Container(
  189. height: 100,
  190. width: 100,
  191. color: color,
  192. );
  193. },
  194. ),
  195. StreamBuilder<int>(
  196. stream: _repCounter,
  197. builder: (context, snapshot) {
  198. var repCounter = 0;
  199. if (snapshot.hasData) {
  200. repCounter = snapshot.data!;
  201. }
  202. return Column(
  203. children: [
  204. Text(
  205. "${repCounter % widget.exercise.reps}/${widget.exercise.reps}",
  206. style: TextStyle(fontSize: 40),
  207. ),
  208. Text(
  209. "${repCounter ~/ widget.exercise.reps}/${widget.exercise.series}",
  210. style: TextStyle(fontSize: 40),
  211. ),
  212. ],
  213. );
  214. },
  215. )
  216. ],
  217. );
  218. },
  219. );
  220. }
  221. static const authorizedType = [
  222. PoseLandmarkType.nose,
  223. PoseLandmarkType.leftShoulder,
  224. PoseLandmarkType.rightShoulder,
  225. PoseLandmarkType.leftElbow,
  226. PoseLandmarkType.rightElbow,
  227. PoseLandmarkType.leftWrist,
  228. PoseLandmarkType.rightWrist,
  229. PoseLandmarkType.leftHip,
  230. PoseLandmarkType.rightHip,
  231. PoseLandmarkType.leftKnee,
  232. PoseLandmarkType.rightKnee,
  233. PoseLandmarkType.leftAnkle,
  234. PoseLandmarkType.rightAnkle,
  235. ];
  236. }
  237. /*
  238. GETTING IN POSITION:
  239. CHECK IF EVERY NECESSARY JOINT ARE ON SCREEN (reliability > 0.8)
  240. CHECK IF START POSITION IS OKAY (for squat, if knee, hip, shoulder aligned)
  241. COUTING REPETITION:
  242. FROM BEGINNING TO END:
  243. - BEGINNING: defined by start position, get position of interesting joint
  244. - END: defined by positions/distance interesting joints (knee and hip same level for squat,
  245. elbow and should same level for push up)
  246. */