diff --git a/assets/images/fabio_fognini.jpeg b/assets/images/fabio_fognini.jpeg new file mode 100644 index 0000000..88d602a Binary files /dev/null and b/assets/images/fabio_fognini.jpeg differ diff --git a/assets/images/rafael_nadal.jpeg b/assets/images/rafael_nadal.jpeg new file mode 100644 index 0000000..73f6a4e Binary files /dev/null and b/assets/images/rafael_nadal.jpeg differ diff --git a/assets/images/roger_federer.jpeg b/assets/images/roger_federer.jpeg new file mode 100644 index 0000000..d12d56d Binary files /dev/null and b/assets/images/roger_federer.jpeg differ diff --git a/lib/finals.dart b/lib/finals.dart index a1b9a0a..1700054 100644 --- a/lib/finals.dart +++ b/lib/finals.dart @@ -10,6 +10,7 @@ final ServeResult fabioFognini = ServeResult.fromCompleteInferenceList( "Fabio Fognini", 178, false, + "assets/images/fabio_fognini.jpeg", [ [ [260, 304, 0.23688101768493652], @@ -1176,6 +1177,7 @@ final ServeResult rogerFederer = ServeResult.fromCompleteInferenceList( "Roger Federer", 185, false, + "assets/images/roger_federer.jpeg", [ [ [264, 304, 0.6883679628372192], @@ -2342,6 +2344,7 @@ final ServeResult rafaelNadal = ServeResult.fromCompleteInferenceList( "Rafael Nadal", 185, true, + "assets/images/rafael_nadal.jpeg", [ [ [238, 281, 0.497579962015152], diff --git a/lib/models/serve_result.dart b/lib/models/serve_result.dart index 4fd5f04..4af0a0e 100644 --- a/lib/models/serve_result.dart +++ b/lib/models/serve_result.dart @@ -22,10 +22,14 @@ class InferencePoint { class ServeResult { final String playerName; - final String? playerPhotoUrl; + final String? playerPhotoAssetPath; final int height; //in cm final bool isLeftHanded; + double minWidth = 2000; + double minHeight = 2000; + double maxHeight = 0; + final List leftShoulderAngles = []; final List leftKneeAngles = []; final List leftElbowAngles = []; @@ -52,7 +56,7 @@ class ServeResult { final List rightAnklePoints = []; ServeResult(this.playerName, this.height, this.isLeftHanded, - {this.playerPhotoUrl}); + {this.playerPhotoAssetPath}); ServeResult copyWith({String? playerName, int? height, bool? isLeftHanded}) { return ServeResult(playerName ?? this.playerName, height ?? this.height, @@ -180,6 +184,55 @@ class ServeResult { rightShoulderPoint.point)); rightShoulderAngles.add(getAngle( rightElbowPoint.point, rightShoulderPoint.point, rightHipPoint.point)); + + List pointHeights = [ + nosePoint.point.dy, + leftEyePoint.point.dy, + rightEyePoint.point.dy, + leftEarPoint.point.dy, + rightEarPoint.point.dy, + leftShoulderPoint.point.dy, + rightShoulderPoint.point.dy, + leftElbowPoint.point.dy, + rightElbowPoint.point.dy, + leftWristPoint.point.dy, + rightWristPoint.point.dy, + leftHipPoint.point.dy, + rightHipPoint.point.dy, + leftKneePoint.point.dy, + rightKneePoint.point.dy, + leftAnklePoint.point.dy, + rightAnklePoint.point.dy, + ]; + List pointWidths = [ + nosePoint.point.dx, + leftEyePoint.point.dx, + rightEyePoint.point.dx, + leftEarPoint.point.dx, + rightEarPoint.point.dx, + leftShoulderPoint.point.dx, + rightShoulderPoint.point.dx, + leftElbowPoint.point.dx, + rightElbowPoint.point.dx, + leftWristPoint.point.dx, + rightWristPoint.point.dx, + leftHipPoint.point.dx, + rightHipPoint.point.dx, + leftKneePoint.point.dx, + rightKneePoint.point.dx, + leftAnklePoint.point.dx, + rightAnklePoint.point.dx, + ]; + + if (pointWidths.min < minWidth) { + minWidth = pointWidths.min; + } + if (pointHeights.min < minHeight) { + minHeight = pointHeights.min; + } + if (pointHeights.max > maxHeight) { + maxHeight = pointHeights.max; + } } String heightInFeetAndInches() { @@ -188,9 +241,14 @@ class ServeResult { return "${heightInFeet.floor()}ft ${heightInRemainingInches.round()}in"; } - factory ServeResult.fromCompleteInferenceList(String playerName, int height, - bool isLeftHanded, List completeExtractedInferenceList) { - ServeResult newServeResult = ServeResult(playerName, height, isLeftHanded); + factory ServeResult.fromCompleteInferenceList( + String playerName, + int height, + bool isLeftHanded, + String imageAssetPath, + List completeExtractedInferenceList) { + ServeResult newServeResult = ServeResult(playerName, height, isLeftHanded, + playerPhotoAssetPath: imageAssetPath); for (var inferenceList in completeExtractedInferenceList) { newServeResult.addInferenceFromFrame(inferenceList); } diff --git a/lib/screens/change_reference_player.dart b/lib/screens/change_reference_player.dart new file mode 100644 index 0000000..283b609 --- /dev/null +++ b/lib/screens/change_reference_player.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:tennis_serve_analysis/finals.dart'; +import 'package:tennis_serve_analysis/widgets/reference_player_card.dart'; + +class ChangeReferencePlayerScreen extends StatefulWidget { + final int selectedPlayerIndex; + const ChangeReferencePlayerScreen( + {Key? key, required this.selectedPlayerIndex}) + : super(key: key); + + @override + State createState() => + _ChangeReferencePlayerScreenState(); +} + +class _ChangeReferencePlayerScreenState + extends State { + late int selectedIndex; + + @override + void initState() { + selectedIndex = widget.selectedPlayerIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Change Reference Player"), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + Navigator.pop(context, selectedIndex); + }, + child: const Icon(Icons.done), + ), + body: ListView.builder( + itemCount: availableReferencePlayers.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: GestureDetector( + onTap: () { + setState(() { + selectedIndex = index; + }); + }, + child: ReferencePlayerCard( + referencePlayerResult: availableReferencePlayers[index], + isSelected: index == selectedIndex, + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/results_screen.dart b/lib/screens/results_screen.dart index f2b3e25..f6ac08b 100644 --- a/lib/screens/results_screen.dart +++ b/lib/screens/results_screen.dart @@ -11,10 +11,13 @@ import 'package:image_picker/image_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:image/image.dart' as image_lib; import 'package:tennis_serve_analysis/controllers/user_controller.dart'; +import 'package:tennis_serve_analysis/finals.dart'; import 'package:tennis_serve_analysis/models/serve_result.dart'; +import 'package:tennis_serve_analysis/screens/change_reference_player.dart'; import 'package:tennis_serve_analysis/utility/classifier.dart'; import 'package:tennis_serve_analysis/utility/isolate_utils.dart'; import 'package:tennis_serve_analysis/widgets/analyzing_loading.dart'; +import 'package:tennis_serve_analysis/widgets/reference_player_card.dart'; import 'package:tennis_serve_analysis/widgets/stat_tile.dart'; import 'package:tennis_serve_analysis/widgets/serve_visualizer.dart'; @@ -153,59 +156,25 @@ class _ResultsScreenState extends ConsumerState { ref.watch(selectedPlayerProvider(selectedPlayerIndex)); return Scaffold( appBar: AppBar( - title: const Text("Serve Analysis Result"), + title: const Text("Analysis Result"), actions: [ if (!isLoading) IconButton( onPressed: () async { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Change Reference Player"), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioListTile( - title: const Text("Fabio Fognini"), - value: 0, - groupValue: selectedPlayerIndex, - onChanged: (val) { - selectedPlayerIndex = val ?? 2; - setState(() {}); - Navigator.pop(context); - }, - ), - RadioListTile( - title: const Text("Roger Federer"), - value: 1, - groupValue: selectedPlayerIndex, - onChanged: (val) { - selectedPlayerIndex = val ?? 0; - setState(() {}); - Navigator.pop(context); - }, - ), - RadioListTile( - title: const Text("Rafael Nadel"), - value: 2, - groupValue: selectedPlayerIndex, - onChanged: (val) { - selectedPlayerIndex = val ?? 1; - setState(() {}); - Navigator.pop(context); - }, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Ok"), - ), - ], - ), - ); + selectedPlayerIndex = availableReferencePlayers.indexWhere( + (element) => + element.playerName == + selectedPlayerServeResult.playerName); + selectedPlayerIndex = + (selectedPlayerIndex == -1) ? null : selectedPlayerIndex; + selectedPlayerIndex = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChangeReferencePlayerScreen( + selectedPlayerIndex: selectedPlayerIndex ?? 0))); + setState(() {}); }, + tooltip: "Change Reference Player", icon: const Icon(Icons.change_circle), ) ], @@ -217,21 +186,24 @@ class _ResultsScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + ReferencePlayerCard( + referencePlayerResult: selectedPlayerServeResult, + ), Card( child: Column( children: [ - const SizedBox(height: 10), + const SizedBox(height: 5), Row( children: [ const SizedBox(width: 20), const SizedBox( - height: 20, - width: 20, + height: 15, + width: 15, child: ColoredBox(color: Colors.red), ), const SizedBox(width: 10), Text( - "User Serve", + "User's Serve", style: Theme.of(context).textTheme.titleSmall, ), ], @@ -241,13 +213,13 @@ class _ResultsScreenState extends ConsumerState { children: [ const SizedBox(width: 20), const SizedBox( - height: 20, - width: 20, + height: 15, + width: 15, child: ColoredBox(color: Colors.green), ), const SizedBox(width: 10), Text( - "${selectedPlayerServeResult.playerName} Serve", + "${selectedPlayerServeResult.playerName}'s Serve", style: Theme.of(context).textTheme.titleSmall, ), ], @@ -256,11 +228,25 @@ class _ResultsScreenState extends ConsumerState { children: [ UserServeVisualizer( points: serveResult.completeInferenceList, + minSize: Size( + serveResult.minWidth, serveResult.minHeight), + maxSize: Size( + 500, + serveResult.maxHeight, + ), ), UserServeVisualizer( + key: ValueKey( + selectedPlayerServeResult.playerName), points: selectedPlayerServeResult .completeInferenceList, isReference: true, + minSize: Size(selectedPlayerServeResult.minWidth, + selectedPlayerServeResult.minHeight), + maxSize: Size( + 500, + selectedPlayerServeResult.maxHeight, + ), ), ], ), @@ -268,7 +254,7 @@ class _ResultsScreenState extends ConsumerState { ), ), Padding( - padding: const EdgeInsets.fromLTRB(10, 15, 20, 5), + padding: const EdgeInsets.fromLTRB(10, 5, 20, 5), child: Text( "Average Angles", style: Theme.of(context).textTheme.titleLarge, diff --git a/lib/widgets/reference_player_card.dart b/lib/widgets/reference_player_card.dart new file mode 100644 index 0000000..2691878 --- /dev/null +++ b/lib/widgets/reference_player_card.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:tennis_serve_analysis/models/serve_result.dart'; + +class ReferencePlayerCard extends StatelessWidget { + final ServeResult referencePlayerResult; + final bool isSelected; + const ReferencePlayerCard( + {Key? key, required this.referencePlayerResult, this.isSelected = false}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + color: (isSelected) ? Colors.green.shade100 : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.asset( + referencePlayerResult.playerPhotoAssetPath!, + height: 70, + width: 70, + fit: BoxFit.scaleDown, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + referencePlayerResult.playerName, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 10), + Row( + children: [ + const Icon(Icons.straighten), + const SizedBox(width: 4), + Text(referencePlayerResult.heightInFeetAndInches()), + const Spacer(), + const Icon(Icons.sports_tennis), + const SizedBox(width: 4), + Text(referencePlayerResult.isLeftHanded + ? "Left Handed" + : "Right Handed"), + const SizedBox(width: 4), + ], + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/serve_visualizer.dart b/lib/widgets/serve_visualizer.dart index 4e695e6..d67030c 100644 --- a/lib/widgets/serve_visualizer.dart +++ b/lib/widgets/serve_visualizer.dart @@ -4,10 +4,14 @@ import 'package:flutter/material.dart'; class UserServeVisualizer extends StatefulWidget { final List points; final bool isReference; + final Size minSize; + final Size maxSize; const UserServeVisualizer({ Key? key, required this.points, this.isReference = false, + this.minSize = Size.zero, + this.maxSize = Size.infinite, }) : super(key: key); @override @@ -17,17 +21,17 @@ class UserServeVisualizer extends StatefulWidget { class _UserServeVisualizerState extends State with SingleTickerProviderStateMixin { int userIndex = 0; - late final AnimationController _animationController; + late AnimationController _animationController; @override void initState() { + super.initState(); //50ms because 20frames per second (Each image coordinate persists for 50ms) _animationController = AnimationController( vsync: this, duration: Duration(milliseconds: widget.points.length * 50), upperBound: widget.points.length.toDouble()) ..repeat(); - super.initState(); } @override @@ -44,10 +48,12 @@ class _UserServeVisualizerState extends State return CustomPaint( willChange: true, isComplex: true, - size: const Size(500, 500), + size: Size(500, widget.maxSize.height - widget.minSize.height + 10), painter: RenderLandmarks( - widget.points[_animationController.value.toInt()], - widget.isReference), + widget.points[_animationController.value.toInt()], + widget.isReference, + widget.minSize, + ), ); }, ); @@ -57,8 +63,9 @@ class _UserServeVisualizerState extends State class RenderLandmarks extends CustomPainter { final List inferenceList; final bool isReference; + final Size minSize; - RenderLandmarks(this.inferenceList, this.isReference); + RenderLandmarks(this.inferenceList, this.isReference, this.minSize); final greenPoint = Paint() ..color = Colors.green @@ -118,20 +125,20 @@ class RenderLandmarks extends CustomPainter { for (List point in inferenceList) { if ((point[2] > 0.20)) { if (isReference) { - pointsGreen - .add(Offset(point[0].toDouble() - 70, point[1].toDouble() - 130)); + pointsGreen.add(Offset(point[0].toDouble() - minSize.width + 40, + point[1].toDouble() - minSize.height)); } else { - pointsRed - .add(Offset(point[0].toDouble() - 70, point[1].toDouble() - 130)); + pointsRed.add(Offset(point[0].toDouble() - minSize.width + 40, + point[1].toDouble() - minSize.height)); } } } for (List edge in edges) { - double vertex1X = inferenceList[edge[0]][0] - 70; - double vertex1Y = inferenceList[edge[0]][1] - 130; - double vertex2X = inferenceList[edge[1]][0] - 70; - double vertex2Y = inferenceList[edge[1]][1] - 130; + double vertex1X = inferenceList[edge[0]][0] - minSize.width + 40; + double vertex1Y = inferenceList[edge[0]][1] - minSize.height; + double vertex2X = inferenceList[edge[1]][0] - minSize.width + 40; + double vertex2Y = inferenceList[edge[1]][1] - minSize.height; canvas.drawLine(Offset(vertex1X, vertex1Y), Offset(vertex2X, vertex2Y), (isReference) ? greenEdge : redEdge); }