FR – Gestures & Kinect

20110616 - Microsoft - Kinect-windows-sdk-300x250

Vous ne l’avez certainement pas manqué en tant que lecteur assidu de ce blog : Le Kinect pour Windows SDK beta est disponible!

Pour le moment toutefois il n’y a pas de services de reconnaissance de gestes (gestures). Nous allons donc essayer tout au long de cet article de créer notre propre librairie qui se chargera de détecter des mouvements simples tels que le swipe (c’est à dire le déplacement latéral de gauche à droite ou de droite à gauche) mais également des mouvements plus complexes comme le tracé d’un cercle avec la main.

imageimage

Cette détection de gestures nous permettra par exemple de piloter Powerpoint à la mode Jedi (comme dans la démonstration Kinect Keyboard Simulator).

Si vous n’êtes pas familier avec le Kinect pour Windows SDK, je vous invite à lire ce premier article qui débroussaille le sujet: http://blogs.msdn.com/b/eternalcoding/archive/2011/06/14/fr-prenez-le-contr-244-le-avec-kinect-pour-windows-sdk.aspx

Comment détecter des gestures ?

Il existe une infinité de solutions pour détecter une gesture. Nous allons dans cet article en retenir deux:

  • La recherche algorithmique
  • La recherche à base de templates

Il faut noter également que ces deux techniques disposent d’un grand nombre de variantes et de raffinements. Le but ici est de donner des pistes et une proposition de solutions qui fonctionnent.

Vous pourrez d’ailleurs trouver le code de cet article juste ici : http://kinecttoolbox.codeplex.com/

image

Dans tous les cas, il s’agira d’enregistrer les différentes positions dans le temps d’un joint d’un squelette pour les analyser par la suite. Les techniques se différencieront dans leur manière de traiter ces positions.

La classe GestureDetector

Pour uniformiser l’usage de notre système de gestures, nous allons donc proposer une classe abstraite GestureDetector dont hériteront toutes les implémentations de nos techniques de recherches:

image

Cette classe fournit la méthode Add pour enregistrer les différentes positions du joint du squelette que nous suivons.

Elle fournit également la méthode abstraite LookForGesture qui sera à la charge de ses filles.

Elle stocke dans la propriété Entries une liste d’Entry dont le rôle est de sauvegarder les coordonnées et le timing de chaque position enregistrée.

Dessin des positions enregistrées

La classe Entry stocke également une ellipse WPF qui sera utilisée pour dessiner la position enregistrée:

image

Via la méthode TraceTo de la classe GestureDetector, nous allons indiquer sur quel canvas WPF l’utilisateur veut dessiner les positions stockées.

Au final, l’ensemble du travail est fait dans la méthode Add qui se compose ainsi :

  1. public virtual void Add(Vector position, SkeletonEngine engine)
  2. {
  3.     Entry newEntry = new Entry {Position = position.ToVector3(), Time = DateTime.Now};
  4.     Entries.Add(newEntry);
  5.  
  6.     if (displayCanvas != null)
  7.     {
  8.         newEntry.DisplayEllipse = new Ellipse
  9.         {
  10.             Width = 4,
  11.             Height = 4,
  12.             HorizontalAlignment = HorizontalAlignment.Left,
  13.             VerticalAlignment = VerticalAlignment.Top,
  14.             StrokeThickness = 2.0,
  15.             Stroke = new SolidColorBrush(displayColor),
  16.             StrokeLineJoin = PenLineJoin.Round
  17.         };
  18.  
  19.  
  20.         float x, y;
  21.  
  22.         engine.SkeletonToDepthImage(position, out x, out y);
  23.  
  24.         x = (float)(x * displayCanvas.ActualWidth);
  25.         y = (float)(y * displayCanvas.ActualHeight);
  26.  
  27.         Canvas.SetLeft(newEntry.DisplayEllipse, x – newEntry.DisplayEllipse.Width / 2);
  28.         Canvas.SetTop(newEntry.DisplayEllipse, y – newEntry.DisplayEllipse.Height / 2);
  29.  
  30.         displayCanvas.Children.Add(newEntry.DisplayEllipse);
  31.     }
  32.  
  33.     if (Entries.Count > WindowSize)
  34.     {
  35.         Entry entryToRemove = Entries[0];
  36.         
  37.         if (displayCanvas != null)
  38.         {
  39.             displayCanvas.Children.Remove(entryToRemove.DisplayEllipse);
  40.         }
  41.  
  42.         Entries.Remove(entryToRemove);
  43.     }
  44.  
  45.     LookForGesture();
  46. }

Notez l’utilisation de la méthode SkeletonToDepthImage qui sait convertir une coordonnée 3D (celle d’un joint) vers une coordonnées 2D entre 0 et 1 sur chaque axe.

Ainsi en plus de sauvegarder les informations de positions des joints, la classe GestureDetector permet de dessiner ces dernières pour donner un retour visuel qui facilite grandement la mise en place de nos techniques de recherches:

image

Comme nous le voyons ci-dessus, les positions en cours d’analyses apparaissent en rouge par-dessus l’image de Kinect. Pour activer ce service, le développeur doit juste mettre un canvas au dessus de l’image qui affiche le retour de la caméra Kinect et donner ce canvas à la méthode GestureDetector.TraceTo:

  1. <Viewbox Margin="5" Grid.RowSpan="5">
  2.     <Grid Width="640" Height="480" ClipToBounds="True">
  3.         <Image x:Name="kinectDisplay"></Image>
  4.         <Canvas x:Name="kinectCanvas"></Canvas>
  5.         <Canvas x:Name="gesturesCanvas"></Canvas>
  6.         <Rectangle Stroke="Black" StrokeThickness="1"/>
  7.     </Grid>
  8. </Viewbox>

La Viewbox permet ici de garder tout ce petit monde aligné tout en autorisant de retailler l’ensemble en fonction de l’interface.

Le second canvas permet quand à lui de dessiner le squelette (grâce à une autre classe du sample : SkeletonDisplayManager)

Approche événementielle

La classe GestureDetector fournit un dernier service à ses filles via la méthode RaiseGestureDetected qui permet de signaler la détection d’une nouvelle gesture (au travers d’un évènement OnGestureDetected). L’énumération SupportedGesture contient ainsi les valeurs suivantes:

  • SwipeToLeft
  • SwipeToRight
  • Circle

Bien évidemment la solution est enrichissable sans problème et je vous encourage même à rajouter de nouvelles gestures au système.

La méthode RaiseGestureDetected permet également via la propriété MinimalPeriodBetweenGestures de garantir un certain délai entre deux signalement de gestures pour éviter les faux positifs (comme par exemple un SwipeToLeft juste après un SwipeToRight correspondant en fait au retour naturel du bras).

Maintenant que nos bases sont posées, nous allons pouvoir nous attaquer aux classes filles qui vont donc fournir la détection proprement dite.

Recherche algorithmique

La recherche algorithmique consiste à parcourir la liste des positions en vérifiant que les contraintes prédéfinies sont constamment valides.

C’est la classe SwipeGestureDetector qui se charge de cette recherche:

image

Pour la gesture SwipeToRight, elle se base sur les contraintes suivantes:

  • Chaque nouvelle position doit être à la droite de la précédente
  • Chaque position ne doit pas dépasser en hauteur la première d’une certaine distance (20 cm)
  • Le temps passé entre la première et la dernière position de la gesture doit être entre 250ms et 1500ms
  • La gesture doit au moins faire 40cm de long (de la première à la dernière gesture donc)

La gesture SwipeToLeft se base sur les même contraintes sauf pour le sens de progression évidemment.

Pour gérer ces deux gestures efficacement, nous allons donc partir sur un algorithme générique qui se chargera de vérifier les quatre contraintes citées précédemment:

  1. bool ScanPositions(Func<Vector3, Vector3, bool> heightFunction, Func<Vector3, Vector3, bool> directionFunction, Func<Vector3, Vector3, bool> lengthFunction, int minTime, int maxTime)
  2. {
  3.     int start = 0;
  4.  
  5.     for (int index = 1; index < Entries.Count – 1; index++)
  6.     {
  7.         if (!heightFunction(Entries[0].Position, Entries[index].Position) || !directionFunction(Entries[index].Position, Entries[index + 1].Position))
  8.         {
  9.             start = index;
  10.         }
  11.  
  12.         if (lengthFunction(Entries[index].Position, Entries[start].Position))
  13.         {
  14.             double totalMilliseconds = (Entries[index].Time – Entries[start].Time).TotalMilliseconds;
  15.             if (totalMilliseconds >= minTime && totalMilliseconds <= maxTime)
  16.             {
  17.                 return true;
  18.             }
  19.         }
  20.     }
  21.  
  22.     return false;
  23. }

Pour se servir de cette méthode, il faut donc fournir 3 fonctions et une période à vérifier.

Donc pour gérer nos deux gestures, il suffit d’appeler le code suivant:

  1. protected override void LookForGesture()
  2. {
  3.     // Swipe to right
  4.     if (ScanPositions((p1, p2) => Math.Abs(p2.Y – p1.Y) < SwipeMaximalHeight, // Height
  5.         (p1, p2) => p2.X – p1.X > -0.01f, // Progression to right
  6.         (p1, p2) => Math.Abs(p2.X – p1.X) > SwipeMinimalLength, // Length
  7.         SwipeMininalDuration, SwipeMaximalDuration)) // Duration
  8.     {
  9.         RaiseGestureDetected(SupportedGesture.SwipeToRight);
  10.         return;
  11.     }
  12.  
  13.     // Swipe to left
  14.     if (ScanPositions((p1, p2) => Math.Abs(p2.Y – p1.Y) < SwipeMaximalHeight,  // Height
  15.         (p1, p2) => p2.X – p1.X < 0.01f, // Progression to right
  16.         (p1, p2) => Math.Abs(p2.X – p1.X) > SwipeMinimalLength, // Length
  17.         SwipeMininalDuration, SwipeMaximalDuration))// Duration
  18.     {
  19.         RaiseGestureDetected(SupportedGesture.SwipeToLeft);
  20.         return;
  21.     }
  22. }

Grâce à cette classe il est donc possible de rajouter des gestures simplement détectables via des contraintes.

Stabilité de la position du squelette

Pour bien garantir le fonctionnement de notre détection, nous devons valider que le squelette est effectivement statique pour ne pas générer de faux positifs (par exemple la détection d’un swipe lié au déplacement du corps entier et non pas juste à celui de la main).

Pour effectuer cette vérification, nous allons recourir à la classe BarycenterHelper :

  1. public class BarycenterHelper
  2. {
  3.     readonly Dictionary<int, List<Vector3>> positions = new Dictionary<int, List<Vector3>>();
  4.     readonly int windowSize;
  5.  
  6.     public float Threshold { get; set; }
  7.  
  8.     public BarycenterHelper(int windowSize = 20, float threshold = 0.05f)
  9.     {
  10.         this.windowSize = windowSize;
  11.         Threshold = threshold;
  12.     }
  13.  
  14.     public bool IsStable(int trackingID)
  15.     {
  16.         List<Vector3> currentPositions = positions[trackingID];
  17.         if (currentPositions.Count != windowSize)
  18.             return false;
  19.  
  20.         Vector3 current = currentPositions[currentPositions.Count – 1];
  21.  
  22.         for (int index = 0; index < currentPositions.Count – 2; index++)
  23.         {
  24.             Debug.WriteLine((currentPositions[index] – current).Length());
  25.  
  26.             if ((currentPositions[index] – current).Length() > Threshold)
  27.                 return false;
  28.         }
  29.  
  30.         return true;
  31.     }
  32.  
  33.     public void Add(Vector3 position, int trackingID)
  34.     {
  35.   ��     if (!positions.ContainsKey(trackingID))
  36.             positions.Add(trackingID, new List<Vector3>());
  37.  
  38.         positions[trackingID].Add(position);
  39.  
  40.         if (positions[trackingID].Count > windowSize)
  41.             positions[trackingID].RemoveAt(0);
  42.     }
  43. }

En fournissant à cette classe les positions successives du squelette, elle nous indiquera via la méthode IsStable si le squelette est en mouvement ou statique.

Ainsi, nous pouvons nous servir de cette information pour n’envoyer les positions des joints aux systèmes de détection que lorsque le squelette n’est pas en mouvement:

  1. void ProcessFrame(ReplaySkeletonFrame frame)
  2. {
  3.     Dictionary<int, string> stabilities = new Dictionary<int, string>();
  4.     foreach (var skeleton in frame.Skeletons)
  5.     {
  6.         if (skeleton.TrackingState != SkeletonTrackingState.Tracked)
  7.             continue;
  8.  
  9.         barycenterHelper.Add(skeleton.Position.ToVector3(), skeleton.TrackingID);
  10.  
  11.         stabilities.Add(skeleton.TrackingID, barycenterHelper.IsStable(skeleton.TrackingID) ? "Stable" : "Unstable");
  12.         if (!barycenterHelper.IsStable(skeleton.TrackingID))
  13.             continue;
  14.  
  15.         foreach (Joint joint in skeleton.Joints)
  16.         {
  17.             if (joint.Position.W < 0.8f || joint.TrackingState != JointTrackingState.Tracked)
  18.                 continue;
  19.  
  20.             if (joint.ID == JointID.HandRight)
  21.             {
  22.                 swipeGestureRecognizer.Add(joint.Position, kinectRuntime.SkeletonEngine);
  23.                 circleGestureRecognizer.Add(joint.Position, kinectRuntime.SkeletonEngine);
  24.             }
  25.         }
  26.  
  27.         postureRecognizer.TrackPostures(skeleton);
  28.     }
  29.  
  30.     skeletonDisplayManager.Draw(frame);
  31.  
  32.     stabilitiesList.ItemsSource = stabilities;
  33.  
  34.     currentPosture.Text = "Current posture: " + postureRecognizer.CurrentPosture.ToString();
  35. }

Outil d’enregistrements et de replay

Un autre point important lorsque l’on développe avec le Kinect pour Windows SDK, c’est la possibilité de tester. Cela peut paraitre idiot mais pour tester il faut se lever, venir devant le capteur et effectuer la gestuelle adéquate. Et à moins d’avoir un assistant, cela peut vite devenir pénible.

Nous allons donc nous doter d’un service d’enregistrements et de replay des informations des squelettes envoyés par Kinect.

Enregistrement

La partie enregistrement est assez simple en ce sens qu’il suffit de prendre une SkeletonFrame et de parcourir chaque squelette pour sérialiser son contenu:

  1. public void Record(SkeletonFrame frame)
  2. {
  3.     if (writer == null)
  4.         throw new Exception("You must call Start before calling Record");
  5.  
  6.     TimeSpan timeSpan = DateTime.Now.Subtract(referenceTime);
  7.     referenceTime = DateTime.Now;
  8.     writer.Write((long)timeSpan.TotalMilliseconds);
  9.     writer.Write(frame.FloorClipPlane);
  10.     writer.Write((int)frame.Quality);
  11.     writer.Write(frame.NormalToGravity);
  12.  
  13.     writer.Write(frame.Skeletons.Length);
  14.  
  15.     foreach (SkeletonData skeleton in frame.Skeletons)
  16.     {
  17.         writer.Write((int)skeleton.TrackingState);
  18.         writer.Write(skeleton.Position);
  19.         writer.Write(skeleton.TrackingID);
  20.         writer.Write(skeleton.EnrollmentIndex);
  21.         writer.Write(skeleton.UserIndex);
  22.         writer.Write((int)skeleton.Quality);
  23.  
  24.         writer.Write(skeleton.Joints.Count);
  25.         foreach (Joint joint in skeleton.Joints)
  26.         {
  27.             writer.Write((int)joint.ID);
  28.             writer.Write((int)joint.TrackingState);
  29.             writer.Write(joint.Position);
  30.         }
  31.     }
  32. }

Replay

La problématique au niveau du replay se situe au niveau de la reconstruction. En effet, les classes de Kinect sont scellées et n’exposent pas leur constructeur. Pour contourner ce problème, nous allons reproduire la hiérarchie de classe en rajoutant des opérateurs de cast implicites vers les classes de Kinect:

image

Grâce à ces opérateurs de cast, nos méthodes vont travailler avec les classes de replay tout en acceptant également les classes de bases de Kinect (qui seront alors automatiquement converties).

La classe en charge du replay est la classe SkeletonReplay dont la méthode Start permet de lancer l’exécution:

  1. public void Start()
  2. {
  3.     context = SynchronizationContext.Current;
  4.  
  5.     CancellationToken token = cancellationTokenSource.Token;
  6.  
  7.     Task.Factory.StartNew(() =>
  8.     {
  9.         foreach (ReplaySkeletonFrame frame in frames)
  10.         {
  11.             Thread.Sleep(TimeSpan.FromMilliseconds(frame.TimeStamp));
  12.  
  13.             if (token.IsCancellationRequested)
  14.                 return;
  15.                                       
  16.             ReplaySkeletonFrame closure = frame;
  17.             context.Send(state =>
  18.                             {
  19.                                 if (SkeletonFrameReady != null)
  20.                                     SkeletonFrameReady(this, new ReplaySkeletonFrameReadyEventArgs {SkeletonFrame = closure});
  21.                             }, null);
  22.         }
  23.     }, token);
  24. }

Au final, nous pouvons donc enregistrer des gestuelles et les rejouer à l’infini pour débugger ou développer notre application:

image

Vous pouvez télécharger ici un exemple de fichier de replay: http://www.catuhe.com/msdn/davca.replay.zip

Savoir quand commencer

Le dernier problème qu’il nous reste à résoudre est de savoir quand commencer l’analyse des gestures. Nous savons déjà que nous ne devons le faire que quand le corps est stable mais cela n’est pas suffisant.

En effet, même si je reste statique, lorsque je parle en tant que bon latin, je m’exprime beaucoup avec les mains et je peux déclencher des gestures malencontreusement.

Pour nous protéger de cela, il est possible de compléter la détection en ajoutant une condition sur une posture. Ainsi, nous pourrions définir que l’on passe au slide suivant dans Powerpoint si l’on détecte SwipeToRight ET que la main gauche fait “Hello”.

C’est donc ici que nous allons faire appel à la classe PostureDetector qui va fournir les algorithmes adéquats:

  1. public class PostureDetector
  2. {
  3.     const float Epsilon = 0.1f;
  4.     const float MaxRange = 0.25f;
  5.     const int AccumulatorTarget = 10;
  6.  
  7.     Posture previousPosture = Posture.None;
  8.     public event Action<Posture> PostureDetected;
  9.     int accumulator;
  10.     Posture accumulatedPosture = Posture.None;
  11.  
  12.     public Posture CurrentPosture
  13.     {
  14.         get { return previousPosture; }
  15.     }
  16.  
  17.     public void TrackPostures(ReplaySkeletonData skeleton)
  18.     {
  19.         if (skeleton.TrackingState != SkeletonTrackingState.Tracked)
  20.             return;
  21.  
  22.         Vector3? headPosition = null;
  23.         Vector3? leftHandPosition = null;
  24.         Vector3? rightHandPosition = null;
  25.  
  26.         foreach (Joint joint in skeleton.Joints)
  27.         {
  28.             if (joint.Position.W < 0.8f || joint.TrackingState != JointTrackingState.Tracked)
  29.                 continue;
  30.  
  31.             switch (joint.ID)
  32.             {
  33.                 case JointID.Head:
  34.                     headPosition = joint.Position.ToVector3();
  35.                     break;
  36.                 case JointID.HandLeft:
  37.                     leftHandPosition = joint.Position.ToVector3();
  38.                     break;
  39.                 case JointID.HandRight:
  40.                     rightHandPosition = joint.Position.ToVector3();
  41.                     break;
  42.             }
  43.         }
  44.  
  45.         // HandsJoined
  46.         if (CheckHandsJoined(rightHandPosition, leftHandPosition))
  47.             return;
  48.  
  49.         // LeftHandOverHead
  50.         if (CheckHandOverHead(headPosition, leftHandPosition))
  51.         {
  52.             RaisePostureDetected(Posture.LeftHandOverHead);
  53.             return;
  54.         }
  55.  
  56.         // RightHandOverHead
  57.         if (CheckHandOverHead(headPosition, rightHandPosition))
  58.         {
  59.             RaisePostureDetected(Posture.RightHandOverHead);
  60.             return;
  61.         }
  62.  
  63.         // LeftHello
  64.         if (CheckHello(headPosition, leftHandPosition))
  65.         {
  66.             RaisePostureDetected(Posture.LeftHello);
  67.             return;
  68.         }
  69.  
  70.         // RightHello
  71.         if (CheckHello(headPosition, rightHandPosition))
  72.         {
  73.             RaisePostureDetected(Posture.RightHello);
  74.             return;
  75.         }
  76.  
  77.         previousPosture = Posture.None;
  78.         accumulator = 0;
  79.     }
  80.  
  81.     bool CheckHandOverHead(Vector3? headPosition, Vector3? handPosition)
  82.     {
  83.         if (!handPosition.HasValue || !headPosition.HasValue)
  84.             return false;
  85.  
  86.         if (handPosition.Value.Y < headPosition.Value.Y)
  87.             return false;
  88.  
  89.         if (Math.Abs(handPosition.Value.X – headPosition.Value.X) > MaxRange)
  90.             return false;
  91.  
  92.         if (Math.Abs(handPosition.Value.Z – headPosition.Value.Z) > MaxRange)
  93.             return false;
  94.  
  95.         return true;
  96.     }
  97.  
  98.  
  99.     bool CheckHello(Vector3? headPosition, Vector3? handPosition)
  100.     {
  101.         if (!handPosition.HasValue || !headPosition.HasValue)
  102.             return false;
  103.  
  104.         if (Math.Abs(handPosition.Value.X – headPosition.Value.X) < MaxRange)
  105.             return false;
  106.  
  107.         if (Math.Abs(handPosition.Value.Y – headPosition.Value.Y) > MaxRange)
  108.             return false;
  109.  
  110.         if (Math.Abs(handPosition.Value.Z – headPosition.Value.Z) > MaxRange)
  111.             return false;
  112.  
  113.         return true;
  114.     }
  115.  
  116.     bool CheckHandsJoined(Vector3? leftHandPosition, Vector3? rightHandPosition)
  117.     {
  118.         if (!leftHandPosition.HasValue || !rightHandPosition.HasValue)
  119.             return false;
  120.  
  121.         float distance = (leftHandPosition.Value – rightHandPosition.Value).Length();
  122.  
  123.         if (distance > Epsilon)
  124.             return false;
  125.  
  126.         RaisePostureDetected(Posture.HandsJoined);
  127.         return true;
  128.     }
  129.  
  130.     void RaisePostureDetected(Posture posture)
  131.     {
  132.         if (accumulator < AccumulatorTarget)
  133.         {
  134.             if (accumulatedPosture != posture)
  135.             {
  136.                 accumulator = 0;
  137.                 accumulatedPosture = posture;
  138.             }
  139.             accumulator++;
  140.             return;
  141.         }
  142.  
  143.         if (previousPosture == posture)
  144.             return;
  145.  
  146.         previousPosture = posture;
  147.         if (PostureDetected != null)
  148.             PostureDetected(posture);
  149.  
  150.         accumulator = 0;
  151.     }
  152. }

Le fonctionnement de la classe PostureDetector s’appuie sur l’analyse comparée des positions des éléments qui nous intéressent. Par exemple pour la posture Hello, nous cherchons à savoir si la main est à la même hauteur que la tête et si elle est au moins à 25cm sur le coté.

De plus, la détection d’une posture s’appuie sur un accumulateur qui permet de ne lever l’événément de détection que lorsque la posture est stable (sur un certain nombre d’images).

Encore une fois cette classe est largement extensible.

Recherche à base de templates

Le principal défaut de l’approche à base d’algorithmes est que toutes les gestures ne sont pas facilement descriptibles avec des contraintes. Nous allons donc nous pencher sur une autre approche plus générale.

Nous allons partir du principe qu’une gesture peut être enregistrée et que par la suite, le système se chargera de savoir si la gesture courante correspond à celle qui est déjà connue : on parle ici de système de recherches à base de templates (modèles).

Notre objectif va donc être finalement de savoir comparer deux gestures.

Comparer le comparable

Avant de nous lancer dans un algorithme de comparaison, nous allons nous faciliter la tâche en “uniformisant” nos données.

En effet, une gesture est une suite de points (pour cet article nous allons nous contenter de comparer des gestures 2D comme le cercle). Toutefois les coordonnées de ces points sont des distances vers le capteur et il va nous falloir les rassembler vers un repère commun. Pour ce faire nous allons effectuer les opérations suivantes;

  1. Ramener les gestures à un nombre défini de points en calculant la distance moyenne entre chaque point
  2. Amener le point 0 sur l’angle 0 via une rotation
  3. Amener les points dans un repère de 1×1 via un zoom d’un ratio (1/largeur, 1/hauteur)
  4. Centrer la gesture sur l’origine (0,0) via une translation

Ce qui nous donne le schéma suivant:

image

Grâce à ces transformations, nous allons pouvoir comparer des tableaux de points de même taille centrés sur un repère commun avec une orientation commune.

Le code qui permet d’effectuer tout cela se situe dans la classe GoldenSection :

  1. public static List<Vector2> Pack(List<Vector2> positions, int samplesCount)
  2. {
  3.     List<Vector2> locals = ProjectListToDefinedCount(positions, samplesCount);
  4.  
  5.     float angle = GetAngleBetween(locals.Center(), positions[0]);
  6.     locals = locals.Rotate(-angle);
  7.  
  8.     locals.ScaleToReferenceWorld();
  9.     locals.CenterToOrigin();
  10.  
  11.     return locals;
  12. }

Les différentes méthodes et méthodes d’extension utilisées sont présentes dans la classe statique GoldenSectionExtensions :

  1. public static class GoldenSectionExtensions
  2. {
  3.     // Get length of path
  4.     public static float Length(this List<Vector2> points)
  5.     {
  6.         float length = 0;
  7.  
  8.         for (int i = 1; i < points.Count; i++)
  9.         {
  10.             length += (points[i – 1] – points[i]).Length();
  11.         }
  12.  
  13.         return length;
  14.     }
  15.  
  16.     // Get center of path
  17.     public static Vector2 Center(this List<Vector2> points)
  18.     {
  19.         Vector2 result = points.Aggregate(Vector2.Zero, (current, point) => current + point);
  20.  
  21.         result /= points.Count;
  22.  
  23.         return result;
  24.     }
  25.  
  26.     // Rotate path by given angle
  27.     public static List<Vector2> Rotate(this List<Vector2> positions, float angle)
  28.     {
  29.         List<Vector2> result = new List<Vector2>(positions.Count);
  30.         Vector2 c = positions.Center();
  31.  
  32.         float cos = (float)Math.Cos(angle);
  33.         float sin = (float)Math.Sin(angle);
  34.  
  35.         foreach (Vector2 p in positions)
  36.         {
  37.             float dx = p.X – c.X;
  38.             float dy = p.Y – c.Y;
  39.  
  40.             Vector2 rotatePoint = Vector2.Zero;
  41.             rotatePoint.X = dx * cos – dy * sin + c.X;
  42.             rotatePoint.Y = dx * sin + dy * cos + c.Y;
  43.  
  44.             result.Add(rotatePoint);
  45.         }
  46.         return result;
  47.     }
  48.  
  49.     // Average distance betweens paths
  50.     public static float DistanceTo(this List<Vector2> path1, List<Vector2> path2)
  51.     {
  52.         return path1.Select((t, i) => (t – path2[i]).Length()).Average();
  53.     }
  54.  
  55.     // Compute bounding rectangle
  56.     public static Rectangle BoundingRectangle(this List<Vector2> points)
  57.     {
  58.         float minX = points.Min(p => p.X);
  59.         float maxX = points.Max(p => p.X);
  60.         float minY = points.Min(p => p.Y);
  61.         float maxY = points.Max(p => p.Y);
  62.  
  63.         return new Rectangle(minX, minY, maxX – minX, maxY – minY);
  64.     }
  65.  
  66.     // Check bounding rectangle size
  67.     public static bool IsLargeEnough(this List<Vector2> positions, float minSize)
  68.     {
  69.         Rectangle boundingRectangle = positions.BoundingRectangle();
  70.  
  71.         return boundingRectangle.Width > minSize && boundingRectangle.Height > minSize;
  72.     }
  73.  
  74.     // Scale path to 1×1
  75.     public static void ScaleToReferenceWorld(this List<Vector2> positions)
  76.     {
  77.         Rectangle boundingRectangle = positions.BoundingRectangle();
  78.         for (int i = 0; i < positions.Count; i++)
  79.         {
  80.             Vector2 position = positions[i];
  81.  
  82.             position.X *= (1.0f / boundingRectangle.Width);
  83.             position.Y *= (1.0f / boundingRectangle.Height);
  84.  
  85.             positions[i] = position;
  86.         }
  87.     }
  88.  
  89.     // Translate path to origin (0, 0)
  90.     public static void CenterToOrigin(this List<Vector2> positions)
  91.     {
  92.         Vector2 center = positions.Center();
  93.         for (int i = 0; i < positions.Count; i++)
  94.         {
  95.             positions[i] -= center;
  96.         }
  97.     }
  98. }

Golden Section

La comparaison entre nos données pourraient se faire via une simple fonction de distance moyenne entre chaque point. Toutefois, cette solution n’est pas assez précise et laisse passer de nombreux cas.

J’ai donc utilisé un algorithme beaucoup plus puissant appelé Golden Section Search. Il est issu d’un papier disponible ici: http://www.math.uic.edu/~jan/mcs471/Lec9/gss.pdf

Une implémentation Javascript est disponible ici : http://depts.washington.edu/aimgroup/proj/dollar/

La mise en œuvre en C# donne :

  1. public static float Search(List<Vector2> current, List<Vector2> target, float a, float b, float epsilon)
  2. {
  3.     float x1 = ReductionFactor * a + (1 – ReductionFactor) * b;
  4.     List<Vector2> rotatedList = current.Rotate(x1);
  5.     float fx1 = rotatedList.DistanceTo(target);
  6.  
  7.     float x2 = (1 – ReductionFactor) * a + ReductionFactor * b;
  8.     rotatedList = current.Rotate(x2);
  9.     float fx2 = rotatedList.DistanceTo(target);
  10.  
  11.     do
  12.     {
  13.         if (fx1 < fx2)
  14.         {
  15.             b = x2;
  16.             x2 = x1;
  17.             fx2 = fx1;
  18.             x1 = ReductionFactor * a + (1 – ReductionFactor) * b;
  19.             rotatedList = current.Rotate(x1);
  20.             fx1 = rotatedList.DistanceTo(target);
  21.         }
  22.         else
  23.         {
  24.             a = x1;
  25.             x1 = x2;
  26.             fx1 = fx2;
  27.             x2 = (1 – ReductionFactor) * a + ReductionFactor * b;
  28.             rotatedList = current.Rotate(x2);
  29.             fx2 = rotatedList.DistanceTo(target);
  30.         }
  31.     }
  32.     while (Math.Abs(b – a) > epsilon);
  33.  
  34.     float min = Math.Min(fx1, fx2);
  35.  
  36.     return 1.0f – 2.0f * min / Diagonal;
  37. }

Grâce à cet algorithme, nous pouvons simplement comparer un template avec la gesture courante et obtenir un score (entre 0 et 1). Charge à nous de définir le score minimal pour considérer qu’il y a une égalité.

Learning Machine

Pour améliorer notre taux de réussite, il nous suffit de disposer de plusieurs templates. Pour cela, nous allons travailler avec la classe LearningMachine dont le rôle est précisément de stocker nos templates (i.e. d’apprendre de nouveaux modèles) et de les confronter avec la gesture courante. Elle sait bien entendu se sérialiser et se désérialiser et via sa méthode Match elle parcourt ses RecordedPath(qui sont donc des listes de points normalisés) pour les comparer avec la liste courante:

  1. public class LearningMachine
  2. {
  3.     readonly List<RecordedPath> paths;
  4.  
  5.     public LearningMachine(Stream kbStream)
  6.     {
  7.         if (kbStream == null || kbStream.Length == 0)
  8.         {
  9.             paths = new List<RecordedPath>();
  10.             return;
  11.         }
  12.  
  13.         BinaryFormatter formatter = new BinaryFormatter();
  14.  
  15.         paths = (List<RecordedPath>)formatter.Deserialize(kbStream);
  16.     }
  17.  
  18.     public List<RecordedPath> Paths
  19.     {
  20.         get { return paths; }
  21.     }
  22.  
  23.     public bool Match(List<Vector2> entries, float threshold, float minimalScore, float minSize)
  24.     {
  25.         return Paths.Any(path => path.Match(entries, threshold, minimalScore, minSize));
  26.     }
  27.  
  28.     public void Persist(Stream kbStream)
  29.     {
  30.         BinaryFormatter formatter = new BinaryFormatter();
  31.  
  32.         formatter.Serialize(kbStream, Paths);
  33.     }
  34.  
  35.     public void AddPath(RecordedPath path)
  36.     {
  37.         path.CloseAndPrepare();
  38.         Paths.Add(path);
  39.     }
  40. }

Chaque RecordedPath implémente la méthode Match qui fait donc appel à la Golden Section Search:

  1. public bool Match(List<Vector2> positions, float threshold, float minimalScore, float minSize)
  2. {
  3.     if (positions.Count < samplesCount)
  4.         return false;
  5.  
  6.     if (!positions.IsLargeEnough(minSize))
  7.         return false;
  8.  
  9.     List<Vector2> locals = GoldenSection.Pack(positions, samplesCount);
  10.  
  11.     float score = GoldenSection.Search(locals, points, –MathHelper.PiOver4, MathHelper.PiOver4, threshold);
  12.  
  13.     return score > minimalScore;
  14. }

Si un RecordedPath trouve une correspondance nous pouvons alors indiquer avoir trouvé une gesture connue.

Ainsi, grâce à notre algorithme de détection et à la learning machine, nous sommes en présence d’un système à la fois fiable et très peu dépendant de la qualité des informations transmises par Kinect.

Vous trouverez ci-après un exemple de base de connaissances pour reconnaitre un cercle : http://www.catuhe.com/msdn/circleKB.zip.

Conclusion

Nous avons donc à notre disposition un ensemble d’outils permettant de travailler simplement avec Kinect. De plus nous disposons de deux systèmes permettant de détecter de très nombreuses gestures.

A vous de jouer maintenant pour utiliser ces services dans vos applications Kinect !

Pour aller plus loin


Posted

in

,

by

Tags: