Squat service implementation.
authorPiotr Czaja/Advanced Frameworks (PLT) /SRPOL/Engineer/Samsung Electronics <p.czaja@samsung.com>
Mon, 19 Jul 2021 09:52:15 +0000 (11:52 +0200)
committerPiotr Czaja <p.czaja@samsung.com>
Tue, 14 Sep 2021 11:01:34 +0000 (13:01 +0200)
Fitness/Services/SquatService.cs [new file with mode: 0644]
Fitness/ViewModels/ScanningViewModel.cs
Fitness/Views/ScanningView.cs
Fitness/res/layout/ScanningView.xaml

diff --git a/Fitness/Services/SquatService.cs b/Fitness/Services/SquatService.cs
new file mode 100644 (file)
index 0000000..62e78ca
--- /dev/null
@@ -0,0 +1,239 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Timers;
+using Tizen.Multimedia;
+using Tizen.Multimedia.Vision;
+
+namespace Fitness.Services
+{
+    /// <summary>
+    /// Class handling the squat exercise detection.
+    /// </summary>
+    public class SquatService : INotifyPropertyChanged
+    {
+        private const float SquatDetectedThreshold = 0.92f;
+        private const int HoldCount = 5;
+        private readonly PoseDetector poseDetector = new PoseDetector();
+        private int hold;
+        private int count;
+        private int score;
+        private int newHold;
+        private int newCount;
+        private int newScore;
+        private float fscore = 0;
+        private Stopwatch stopwatch = new Stopwatch();
+        private System.Timers.Timer holdTimer;
+        private long holdTimeThreshold;
+        private int isInferencing = 0;
+        private Landmark[,] poseLandmarks;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SquatService"/> class.
+        /// </summary>
+        public SquatService()
+        {
+        }
+
+        /// <summary>
+        /// Occurs when a property value changes.
+        /// </summary>
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        /// <summary>
+        /// Gets or sets the pose landmark property.
+        /// </summary>
+        public Landmark[,] PoseLandmarks
+        {
+            get => poseLandmarks;
+            set
+            {
+                if (value != poseLandmarks)
+                {
+                    poseLandmarks = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the hold property specifying the time of the exercise performed - values ranging from 0 to 5.
+        /// </summary>
+        public int Hold
+        {
+            get => hold;
+            set
+            {
+                if (value != hold)
+                {
+                    hold = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the count property specifying the number of repetitions of the exercise.
+        /// </summary>
+        public int Count
+        {
+            get => count;
+            set
+            {
+                if (value != count)
+                {
+                    count = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the score property specifying the correctness of the exercise - values ranging from 0 to 100.
+        /// </summary>
+        public int Score
+        {
+            get => score;
+            set
+            {
+                if (value != score)
+                {
+                    score = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the hold time threshold specifying the time of a single exercise.
+        /// </summary>
+        public long HoldTimeThreshold
+        {
+            get => holdTimeThreshold;
+            set
+            {
+                if (value != holdTimeThreshold)
+                {
+                    holdTimeThreshold = value;
+                    InitializeTimer();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Detects squat exercise based on given preview image data.
+        /// </summary>
+        /// <param name="previewFrame">Preview image data.</param>
+        /// <returns>A task that represents the squat detection.</returns>
+        public async Task DetectSquat(Tizen.Multimedia.PreviewFrame previewFrame)
+        {
+            var stopwatch = new System.Diagnostics.Stopwatch();
+            stopwatch.Start();
+
+            try
+            {
+                if (Interlocked.Exchange(ref isInferencing, 1) == 0)
+                {
+                    var plane = previewFrame.Plane as TriplePlane;
+                    if (plane == null)
+                    {
+                        throw new System.Exception($"Unsupported plane type: {previewFrame.Plane.GetType()}");
+                    }
+
+                    uint width = (uint)previewFrame.Resolution.Width;
+                    uint height = (uint)previewFrame.Resolution.Height;
+
+                    var landmarks = await poseDetector.Detect(plane, height, width);
+
+                    int numberOfBodyParts = landmarks.GetLength(1);
+                    var range = Enumerable.Range(0, numberOfBodyParts);
+
+                    var locations = range.Select(x => landmarks[0, x].Location).ToList();
+                    var squatSimilarity = SquatDetector.Similarity(locations);
+                    if (float.IsNaN(squatSimilarity))
+                    {
+                        squatSimilarity = 0.0f;
+                    }
+
+                    DetectSquat(squatSimilarity);
+
+                    NUIContext.InvokeOnMainThread(() =>
+                    {
+                        PoseLandmarks = landmarks;
+                        Hold = newHold;
+                        Count = newCount;
+                        Score = newScore;
+                    });
+                }
+            }
+            catch (System.Exception exception)
+            {
+                Services.Logger.Error(exception.Message + System.Environment.NewLine + exception.StackTrace);
+            }
+            finally
+            {
+                stopwatch.Stop();
+                Services.Logger.Debug($"total time {stopwatch.ElapsedMilliseconds} msec");
+
+                Interlocked.Exchange(ref isInferencing, 0);
+            }
+        }
+
+        private void InitializeTimer()
+        {
+            long interval = holdTimeThreshold / HoldCount;
+            holdTimer = new System.Timers.Timer(interval);
+            holdTimer.Elapsed += HoldTimer_Elapsed;
+            holdTimer.AutoReset = true;
+            holdTimer.Enabled = true;
+        }
+
+        private void HoldTimer_Elapsed(object sender, ElapsedEventArgs e)
+        {
+            if ((Hold < HoldCount) && (fscore >= SquatDetectedThreshold))
+            {
+                newHold++;
+            }
+        }
+
+        private void DetectSquat(float squatSimilarity)
+        {
+            var previousScore = fscore;
+            fscore = squatSimilarity;
+            newScore = (int)System.Math.Round((float)(100 * fscore));
+
+            if (previousScore < SquatDetectedThreshold)
+            {
+                if (fscore >= SquatDetectedThreshold)
+                {
+                    stopwatch.Start();
+                    holdTimer.Start();
+                }
+            }
+            else
+            {
+                if (fscore < SquatDetectedThreshold)
+                {
+                    stopwatch.Stop();
+                    holdTimer.Stop();
+
+                    if (stopwatch.ElapsedMilliseconds >= holdTimeThreshold)
+                    {
+                        newCount++;
+                    }
+
+                    newHold = 0;
+                    stopwatch.Reset();
+                }
+            }
+        }
+
+        private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+        }
+    }
+}
index 6cd3afbd55233aefb955883fce1dcab8cd3f00cb..1885210c903108047f5edea79c360f2b5c28560f 100644 (file)
@@ -1,31 +1,29 @@
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
 using System.Windows.Input;
 using Fitness.Services;
-using Tizen.Multimedia;
-using Tizen.Multimedia.Vision;
 using Tizen.NUI.Binding;
 
 namespace Fitness.ViewModels
 {
+    /// <summary>
+    /// Class handling the scanning view.
+    /// </summary>
     public class ScanningViewModel : BaseViewModel
     {
-        private const float SquatDetectedThreshold = 0.92f;
-
-        private readonly PoseDetector poseDetector = new PoseDetector();
-        private int isInferencing = 0;
-
-        private Landmark[,] poseLandmarks;
-        private string poseScores;
-        private string squatSimilarity;
+        private SquatService squatService;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ScanningViewModel"/> class.
+        /// </summary>
         public ScanningViewModel()
         {
             CloseScanningView = new Command(() =>
             {
                 NavigationService.Instance.Pop();
             });
+            squatService = new SquatService()
+            {
+                HoldTimeThreshold = 1200,
+            };
         }
 
         /// <summary>
@@ -33,40 +31,17 @@ namespace Fitness.ViewModels
         /// </summary>
         public string Title { get; private set; } = "ScanningView";
 
-        public Landmark[,] PoseLandmarks
-        {
-            get => poseLandmarks;
-            set
-            {
-                if (value != poseLandmarks)
-                {
-                    poseLandmarks = value;
-                    RaisePropertyChanged();
-                }
-            }
-        }
-
-        public string PoseScores
-        {
-            get => poseScores;
-            set
-            {
-                if (value != poseScores)
-                {
-                    poseScores = value;
-                    RaisePropertyChanged();
-                }
-            }
-        }
-
-        public string SquatSimilarity
+        /// <summary>
+        /// Gets or sets the <see cref="SquatService"/> property.
+        /// </summary>
+        public SquatService SquatService
         {
-            get => squatSimilarity;
+            get => squatService;
             set
             {
-                if (value != squatSimilarity)
+                if (value != squatService)
                 {
-                    squatSimilarity = value;
+                    squatService = value;
                     RaisePropertyChanged();
                 }
             }
@@ -76,62 +51,5 @@ namespace Fitness.ViewModels
         /// Gets EndWorkout Command.
         /// </summary>
         public ICommand CloseScanningView { get; private set; }
-
-        public async Task DetectPose(Tizen.Multimedia.PreviewFrame previewFrame)
-        {
-            var stopwatch = new System.Diagnostics.Stopwatch();
-            stopwatch.Start();
-
-            try
-            {
-                if (Interlocked.Exchange(ref isInferencing, 1) == 0)
-                {
-                    var plane = previewFrame.Plane as TriplePlane;
-                    if (plane == null)
-                    {
-                        throw new System.Exception($"Unsupported plane type: {previewFrame.Plane.GetType()}");
-                    }
-
-                    uint width = (uint)previewFrame.Resolution.Width;
-                    uint height = (uint)previewFrame.Resolution.Height;
-
-                    var landmarks = await poseDetector.Detect(plane, height, width);
-
-                    int numberOfBodyParts = landmarks.GetLength(1);
-                    var range = Enumerable.Range(0, numberOfBodyParts);
-
-                    var scores = range.Select(x => landmarks[0, x].Score);
-                    var trimmedScores = scores.Select(score => score.ToString(".000"));
-                    var averageScore = scores.Average().ToString("0.000");
-
-                    var locations = range.Select(x => landmarks[0, x].Location).ToList();
-                    var squatSimilarity = SquatDetector.Similarity(locations);
-                    if (float.IsNaN(squatSimilarity))
-                    {
-                        squatSimilarity = 0.0f;
-                    }
-
-                    var squatDetection = squatSimilarity >= SquatDetectedThreshold ? "SQUAT DETECTED" : "no squat";
-
-                    NUIContext.InvokeOnMainThread(() =>
-                    {
-                        PoseLandmarks = landmarks;
-                        PoseScores = $"scores {string.Join(", ", trimmedScores)} average {averageScore}";
-                        SquatSimilarity = $"{squatSimilarity.ToString("0.000")} {squatDetection}";
-                    });
-                }
-            }
-            catch (System.Exception exception)
-            {
-                Services.Logger.Error(exception.Message + System.Environment.NewLine + exception.StackTrace);
-            }
-            finally
-            {
-                stopwatch.Stop();
-                Services.Logger.Debug($"total time {stopwatch.ElapsedMilliseconds} msec");
-
-                Interlocked.Exchange(ref isInferencing, 0);
-            }
-        }
     }
 }
index 97c5a7786f1d056a373a787480b5a0211a59cb41..181e5e7a2002331ed39a46541f34b77dabdbcf8f 100644 (file)
@@ -32,7 +32,7 @@ namespace Fitness.Views
         {
             if (BindingContext is ViewModels.ScanningViewModel viewModel)
             {
-                _ = viewModel.DetectPose(e.Preview);
+                _ = viewModel.SquatService.DetectSquat(e.Preview);
             }
         }
     }
index 2c89491a66c201de36beec7a0bdde5626ad4c1db..a55009c2f5a9d2bd1a1d316eac3e10d5cc138aad 100644 (file)
@@ -29,7 +29,7 @@
         <ctrl:Overlay WidthSpecification="{Static LayoutParamPolicies.MatchParent}"
                       HeightSpecification="{Static LayoutParamPolicies.MatchParent}"
                       BindingContext="{Binding Source={x:Reference Root}, Path=BindingContext}"
-                      PoseLandmarks="{Binding PoseLandmarks}">
+                      PoseLandmarks="{Binding SquatService.PoseLandmarks}">
             <x:Arguments>
                 <Size2D>1280, 960</Size2D>
             </x:Arguments>
             </View.Layout>
             
             <TextLabel BindingContext="{Binding Source={x:Reference Root}, Path=BindingContext}"
-                       Text="{Binding PoseScores}"
-                       PixelSize="30"
-                       TextColor="Black"/>
-            
+                       Text="{Binding SquatService.Hold, StringFormat='Hold: {0}'}"
+                       PixelSize="40"
+                       TextColor="Black" />
+            <TextLabel BindingContext="{Binding Source={x:Reference Root}, Path=BindingContext}"
+                       Text="{Binding SquatService.Count, StringFormat='Count: {0}'}"
+                       PixelSize="40"
+                       TextColor="Black" />
             <TextLabel BindingContext="{Binding Source={x:Reference Root}, Path=BindingContext}"
-                       Text="{Binding SquatSimilarity}"
+                       Text="{Binding SquatService.Score, StringFormat='Score: {0}'}"
                        PixelSize="40"
                        TextColor="Black"/>