Add overlay to ScanningView. (#51)
authorPiotr Czaja/Tizen Services & IoT (PLT) /SRPOL/Engineer/Samsung Electronics <p.czaja@samsung.com>
Wed, 26 May 2021 09:55:37 +0000 (11:55 +0200)
committerPiotr Czaja <p.czaja@samsung.com>
Tue, 14 Sep 2021 11:01:34 +0000 (13:01 +0200)
* Add overlay to ScanningView.
* Apply fixes after review.

Fitness/Controls/BodyMarker.cs [new file with mode: 0644]
Fitness/Controls/MarkersConnection.cs [new file with mode: 0644]
Fitness/Controls/Overlay.cs [new file with mode: 0644]
Fitness/Controls/PositionMarker.cs [new file with mode: 0644]
Fitness/ViewModels/ScanningViewModel.cs
Fitness/res/layout/ScanningView.xaml

diff --git a/Fitness/Controls/BodyMarker.cs b/Fitness/Controls/BodyMarker.cs
new file mode 100644 (file)
index 0000000..e6782cf
--- /dev/null
@@ -0,0 +1,227 @@
+using System.Collections.Generic;
+using System.Linq;
+using Tizen.Multimedia.Vision;
+using Tizen.NUI;
+using Tizen.NUI.BaseComponents.VectorGraphics;
+
+namespace Fitness.Controls
+{
+    /// <summary>
+    /// Body parts for display.
+    /// </summary>
+    internal enum BodyPart
+    {
+        /// <summary>
+        /// Head.
+        /// </summary>
+        Head,
+
+        /// <summary>
+        /// Neck.
+        /// </summary>
+        Neck,
+
+        /// <summary>
+        /// Upper part of the right arm.
+        /// </summary>
+        RightUpArm,
+
+        /// <summary>
+        /// Lower part of the right arm.
+        /// </summary>
+        RightLowArm,
+
+        /// <summary>
+        /// Right hand.
+        /// </summary>
+        RightHand,
+
+        /// <summary>
+        /// Upper part of the left arm.
+        /// </summary>
+        LeftUpArm,
+
+        /// <summary>
+        /// Lower part of the left arm.
+        /// </summary>
+        LeftLowArm,
+
+        /// <summary>
+        /// Left hand.
+        /// </summary>
+        LeftHand,
+
+        /// <summary>
+        /// Upper part of the right leg.
+        /// </summary>
+        RightUpLeg,
+
+        /// <summary>
+        /// Lower part of the right leg.
+        /// </summary>
+        RightLowLeg,
+
+        /// <summary>
+        /// Right foot.
+        /// </summary>
+        RightFoot,
+
+        /// <summary>
+        /// Upper part of the left leg.
+        /// </summary>
+        LeftUpLeg,
+
+        /// <summary>
+        /// Lower part of the left leg.
+        /// </summary>
+        LeftLowLeg,
+
+        /// <summary>
+        /// Left foot.
+        /// </summary>
+        LeftFoot,
+    }
+
+    /// <summary>
+    /// Class that determines the position of the body.
+    /// </summary>
+    public class BodyMarker
+    {
+        private static List<(BodyPart From, BodyPart To)> bodyPartConnections;
+        private List<PositionMarker> markers;
+        private List<MarkersConnection> markersConnections;
+        private int numberOfBodyParts;
+        private Color color;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BodyMarker"/> class.
+        /// </summary>
+        /// <param name="numberOfBodyParts">The number of body parts.</param>
+        /// <param name="color">The color of marker.</param>
+        public BodyMarker(int numberOfBodyParts, Color color)
+        {
+            this.numberOfBodyParts = numberOfBodyParts;
+            this.color = color;
+
+            Initialize();
+        }
+
+        /// <summary>
+        /// Gets all shapes in the marker.
+        /// </summary>
+        public IEnumerable<Shape> AllShapes { get; private set; }
+
+        /// <summary>
+        /// Hides the marker.
+        /// </summary>
+        internal void Hide()
+        {
+            foreach (var marker in markers)
+            {
+                if (marker != null)
+                {
+                    marker.Opacity = 0.0f;
+                }
+            }
+
+            foreach (var connection in markersConnections)
+            {
+                if (connection != null)
+                {
+                    connection.Opacity = 0.0f;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Updates the marker's position.
+        /// </summary>
+        /// <param name="landmark">Position of body elements.</param>
+        /// <param name="overlaySize">Size of the view.</param>
+        internal void Update(Landmark[] landmark, Size overlaySize)
+        {
+            UpdateMarkersPosition(landmark, overlaySize);
+            UpdateConnections();
+        }
+
+        private static void DefineBodyPartConnections()
+        {
+            if (bodyPartConnections == null)
+            {
+                bodyPartConnections = new List<(BodyPart From, BodyPart To)>()
+                {
+                    (BodyPart.Head, BodyPart.Neck),
+                    (BodyPart.Neck, BodyPart.RightUpArm),
+                    (BodyPart.RightUpArm, BodyPart.RightLowArm),
+                    (BodyPart.RightLowArm, BodyPart.RightHand),
+                    (BodyPart.Neck, BodyPart.LeftUpArm),
+                    (BodyPart.LeftUpArm, BodyPart.LeftLowArm),
+                    (BodyPart.LeftLowArm, BodyPart.LeftHand),
+                    (BodyPart.Neck, BodyPart.RightUpLeg),
+                    (BodyPart.RightUpLeg, BodyPart.RightLowLeg),
+                    (BodyPart.RightLowLeg, BodyPart.RightFoot),
+                    (BodyPart.Neck, BodyPart.LeftUpLeg),
+                    (BodyPart.LeftUpLeg, BodyPart.LeftLowLeg),
+                    (BodyPart.LeftLowLeg, BodyPart.LeftFoot),
+                };
+            }
+        }
+
+        private void UpdateMarkersPosition(Landmark[] landmark, Size overlaySize)
+        {
+            for (int j = 0; j < numberOfBodyParts; j++)
+            {
+                int x = landmark[j].Location.X * (int)overlaySize.Width / 640;
+                int y = landmark[j].Location.Y * (int)overlaySize.Height / 480;
+
+                markers[j].Position = new Tizen.Multimedia.Point(x, y);
+                markers[j].Opacity = 1.0f;
+            }
+        }
+
+        private void UpdateConnections()
+        {
+            for (int i = 0; i < bodyPartConnections.Count; i++)
+            {
+                int from = (int)bodyPartConnections[i].From;
+                int to = (int)bodyPartConnections[i].To;
+                markersConnections[i].Update(markers[from].Position, markers[to].Position);
+                markersConnections[i].Opacity = 1.0f;
+            }
+        }
+
+        private void Initialize()
+        {
+            InitializeMarkers();
+            InitializeConnections();
+
+            var allShapes = new List<Shape>(markers.Count + markersConnections.Count);
+            allShapes.AddRange(markers);
+            allShapes.AddRange(markersConnections);
+            AllShapes = allShapes;
+        }
+
+        private void InitializeMarkers()
+        {
+            var fillColor = new Color(color.R, color.G, color.B, 0.5f);
+
+            markers = new List<PositionMarker>();
+            for (int j = 0; j < numberOfBodyParts; j++)
+            {
+                var marker = new PositionMarker(color, fillColor);
+                markers.Add(marker);
+            }
+        }
+
+        private void InitializeConnections()
+        {
+            DefineBodyPartConnections();
+            markersConnections = new List<MarkersConnection>();
+            for (int i = 0; i < bodyPartConnections.Count; i++)
+            {
+                var markersConnection = new MarkersConnection(color);
+                markersConnections.Add(markersConnection);
+            }
+        }
+    }
+}
diff --git a/Fitness/Controls/MarkersConnection.cs b/Fitness/Controls/MarkersConnection.cs
new file mode 100644 (file)
index 0000000..6f5aa8b
--- /dev/null
@@ -0,0 +1,42 @@
+using System;
+using Tizen.Multimedia;
+using Tizen.NUI;
+using Tizen.NUI.BaseComponents.VectorGraphics;
+
+namespace Fitness.Controls
+{
+    /// <summary>
+    /// The body part position markers connection.
+    /// </summary>
+    public class MarkersConnection : Shape
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MarkersConnection"/> class.
+        /// </summary>
+        /// <param name="color">The color to use for stroking the connection.</param>
+        public MarkersConnection(Color color)
+        {
+            Opacity = 1.0f;
+            StrokeColor = color;
+            StrokeWidth = 3.0f;
+        }
+
+        /// <summary>
+        /// Updates the connection position.
+        /// </summary>
+        /// <param name="position1">Start of connection.</param>
+        /// <param name="position2">End of connection.</param>
+        internal void Update(Point position1, Point position2)
+        {
+            var dX = position2.X - position1.X;
+            var dY = position2.Y - position1.Y;
+            var distance = (float)System.Math.Sqrt((dX * dX) + (dY * dY));
+            var shiftX = dX * PositionMarker.Radius / distance;
+            var shiftY = dY * PositionMarker.Radius / distance;
+            ResetPath();
+            Translate(position1.X, position1.Y);
+            AddMoveTo(shiftX, shiftY);
+            AddLineTo(position2.X - position1.X - shiftX, position2.Y - position1.Y - shiftY);
+        }
+    }
+}
diff --git a/Fitness/Controls/Overlay.cs b/Fitness/Controls/Overlay.cs
new file mode 100644 (file)
index 0000000..bff22c2
--- /dev/null
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Tizen.Multimedia;
+using Tizen.Multimedia.Vision;
+using Tizen.NUI;
+using Tizen.NUI.BaseComponents.VectorGraphics;
+using Tizen.NUI.Binding;
+
+namespace Fitness.Controls
+{
+    /// <summary>
+    /// Class that displays body position markers.
+    /// </summary>
+    public class Overlay : CanvasView
+    {
+        /// <summary>
+        /// PoseLandmark property.
+        /// </summary>
+        public static readonly BindableProperty PoseLandmarksProperty = BindableProperty.Create(
+            nameof(PoseLandmarks),
+            typeof(Landmark[,]),
+            typeof(Overlay),
+            null,
+            propertyChanged: OnPoseLandmarksChanged);
+
+        private const int MaxNumberOfPeople = 3;
+
+        private static readonly Color[] MarkersColors = new Color[]
+        {
+            new Color(1.0f, 0.0f, 0.0f, 1.0f),
+            new Color(0.145f, 0.93f, 0.898f, 1.0f),
+            Color.Blue,
+            Color.Green,
+            Color.Yellow,
+        };
+
+        private List<BodyMarker> bodyMarkers;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Overlay"/> class.
+        /// </summary>
+        public Overlay()
+            : this(NUIApplication.GetDefaultWindow().Size)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Overlay"/> class.
+        /// </summary>
+        /// <param name="viewBox">The size of viewbox.</param>
+        public Overlay(Size2D viewBox)
+            : base(viewBox)
+        {
+            bodyMarkers = new List<BodyMarker>();
+        }
+
+        /// <summary>
+        /// Gets or sets the landmarks of body pose.
+        /// </summary>
+        public Landmark[,] PoseLandmarks { get; set; }
+
+        private static void OnPoseLandmarksChanged(BindableObject bindable, object oldValue, object newValue)
+        {
+            if (bindable is Overlay overlay && newValue is Landmark[,] landmark)
+            {
+                int numberOfPeople = landmark.GetLength(1);
+                int numberOfBodyParts = landmark.GetLength(0);
+                int numberOfPeopleDisplayed = System.Math.Min(numberOfPeople, MaxNumberOfPeople);
+
+                for (int i = 0; i < numberOfPeopleDisplayed; i++)
+                {
+                    if (overlay.bodyMarkers.Count <= i)
+                    {
+                        var bodyMarker = new BodyMarker(numberOfBodyParts, MarkersColors[i]);
+                        overlay.bodyMarkers.Add(bodyMarker);
+                        overlay.AddDrawables(bodyMarker);
+                    }
+
+                    var landmarksRow = Enumerable.Range(0, landmark.GetLength(0))
+                        .Select(x => landmark[x, i])
+                        .ToArray();
+                    overlay.bodyMarkers[i].Update(landmarksRow, overlay.Size);
+                }
+
+                for (int i = numberOfPeopleDisplayed; i < overlay.bodyMarkers.Count; ++i)
+                {
+                    overlay.bodyMarkers[i].Hide();
+                }
+            }
+        }
+
+        private void AddDrawables(BodyMarker bodyMarker)
+        {
+            foreach (var drawableShape in bodyMarker.AllShapes)
+            {
+                AddDrawable(drawableShape);
+            }
+        }
+    }
+}
diff --git a/Fitness/Controls/PositionMarker.cs b/Fitness/Controls/PositionMarker.cs
new file mode 100644 (file)
index 0000000..0f110bf
--- /dev/null
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Tizen.NUI;
+using Tizen.NUI.BaseComponents.VectorGraphics;
+
+namespace Fitness.Controls
+{
+    /// <summary>
+    /// The body part position marker.
+    /// </summary>
+    public class PositionMarker : Shape
+    {
+        private Tizen.Multimedia.Point position;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PositionMarker"/> class.
+        /// </summary>
+        /// <param name="color">The color to use for stroking the marker.</param>
+        /// <param name="fillcolor">The color to use for filling the marker.</param>
+        public PositionMarker(Color color, Color fillcolor)
+        {
+            Opacity = 1.0f;
+            FillColor = fillcolor;
+            StrokeColor = color;
+            StrokeWidth = 2.0f;
+            AddCircle(0.0f, 0.0f, Radius, Radius);
+        }
+
+        /// <summary>
+        /// Gets the radius of the marker.
+        /// </summary>
+        public static float Radius => 8.0f;
+
+        /// <summary>
+        /// Gets or sets the position of the marker.
+        /// </summary>
+        public Tizen.Multimedia.Point Position
+        {
+            get
+            {
+                return position;
+            }
+
+            set
+            {
+                position = value;
+                Translate(value.X, value.Y);
+            }
+        }
+    }
+}
index 1dcda814e909660e2a230226e793213b0d979133..cc93f48356ca598cfd30075228988152da609bb9 100644 (file)
@@ -2,15 +2,25 @@ using System;
 using System.Windows.Input;
 using Fitness.Models;
 using Fitness.Services;
+using Tizen.Multimedia.Vision;
+using Tizen.NUI;
 using Tizen.NUI.Binding;
 
 namespace Fitness.ViewModels
 {
     public class ScanningViewModel : BaseViewModel
     {
+        private Landmark[,] poseLandmarks;
+        private Timer timer;
+
         public ScanningViewModel()
         {
-            CloseScanningView = new Command(() => { NavigationService.Instance.Pop(); });
+            CloseScanningView = new Command(() =>
+            {
+                NavigationService.Instance.Pop();
+                timer.Stop();
+            });
+            AddTimer();
         }
 
         /// <summary>
@@ -18,9 +28,53 @@ namespace Fitness.ViewModels
         /// </summary>
         public string Title { get; private set; } = "ScanningView";
 
+        public Landmark[,] PoseLandmarks
+        {
+            get => poseLandmarks;
+            set
+            {
+                if (value != poseLandmarks)
+                {
+                    poseLandmarks = value;
+                    RaisePropertyChanged();
+                }
+            }
+        }
+
         /// <summary>
         /// Gets EndWorkout Command.
         /// </summary>
         public ICommand CloseScanningView { get; private set; }
+
+        private void AddTimer()
+        {
+            bool OnTick(object o, Timer.TickEventArgs e)
+            {
+                Random rnd = new Random();
+                int numberOfBodyParts = 14;
+                int numberOfPeople = rnd.Next(5);
+                Landmark[,] array = new Landmark[numberOfBodyParts, numberOfPeople];
+
+                for (int i = 0; i < numberOfBodyParts; i++)
+                {
+                    for (int j = 0; j < numberOfPeople; j++)
+                    {
+                        Tizen.Multimedia.Point point1 = new Tizen.Multimedia.Point(rnd.Next(640), rnd.Next(480));
+                        Landmark landmark = new Landmark
+                        {
+                            Location = point1,
+                        };
+                        array[i, j] = landmark;
+                    }
+                }
+
+                PoseLandmarks = array;
+                return true;
+            }
+
+            timer = new Timer(1000 / 2);
+            timer.Tick += OnTick;
+            timer.Start();
+        }
     }
 }
index 7ae4b899978ba813a92ff51ade488235458f0ac9..f5d73b1b007908bf064d4471673724ed741636cf 100644 (file)
         <vm:ScanningViewModel/>
     </View.BindingContext>
 
-    <ctrl:Camera x:Name="cameraView"
-                 PreviewFps="Fps30"
-                 PositionX="640" PositionY="300" SizeWidth="640" SizeHeight="480"/>
+    <View Position="320, 100"
+          Size="1280, 960">
+        <ctrl:Camera x:Name="cameraView"
+                     WidthResizePolicy="FillToParent"
+                     HeightResizePolicy="FillToParent"
+                     PreviewFps="Fps30" />
+        <ctrl:Overlay WidthResizePolicy="FillToParent"
+                      HeightResizePolicy="FillToParent"
+                      BindingContext="{Binding Source={x:Reference Root}, Path=BindingContext}"
+                      PoseLandmarks="{Binding PoseLandmarks}" >
+          <x:Arguments>
+            <Size2D>1280, 960</Size2D>
+          </x:Arguments>
+        </ctrl:Overlay>
+    </View>
 
     <View Size="{views:SizeInUnits Height=20}"
                   WidthResizePolicy="FillToParent">