pose_detector.dart 9.5 KB

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