Kinect SDK et détection de posture
L’auteur: | Jason Dejaégère |
Technologies utilisées: | C#, Kinect SDK 1.0 |
Niveau de difficulté: | 200 |
Introduction
Cet article a pour but de montrer l’une des façons d’utiliser la Kinect pour contrôler une application. On va en fait analyser la position de différents points du corps de l’utilisateur, et se baser sur ces positions pour déterminer si l’utilisateur se trouve dans une posture qui indique sa volonté de déclencher une action.
L’application en elle-même est basée sur un contrôle de type Carousel. L’utilisateur peut utiliser la Kinect pour déplacer le Carousel vers la gauche ou la droite, et pour démarrer ou fermer une application.
Initialisation
MainWindow.xaml.cs
<my:KinectSensorChooser Name="kinectSensorChooser" Width="400" Padding="0,0,155,0" />
Le KinectSensorChooser quant à lui, sert à déclarer qu’une Kinect peut être utilisée ou du moins un de ses capteurs. Ici on l’utilisera pour la détection des mouvements.
Au préalable, il faut ajouter des références aux librairies Kinect.Toolbox, Microsoft.Kinect (du SDK) et Microsoft.Samples.Kinect.WpfViewers (dans le Kinect SDK sample browser).
MainWindow.xaml.cs
C’est ici que l’on va déclarer les méthodes gérant les actions de la Kinect.
Pour commencer, il est quand même nécessaire de déclarer quelques variables.
/*****Kinect*****/ private const int SkeletonCount = 6; Skeleton[] _allSkeletons = new Skeleton[SkeletonCount]; private KinectSensor _myKinect; private static Vector3 RightHand { get; set; } private static Vector3 LeftHand { get; set; } private static Vector3 Spine { get; set; } private static Vector3 RightElbow { get; set; } private static Vector3 LeftElbow { get; set; } private static Vector3 RightHip { get; set; } private static Vector3 LeftHip { get; set; } private bool GoRight { get; set; } private bool GoLeft { get; set; } private bool EnterApplication { get; set; } private bool QuitApplication { get; set; } private bool CheckerTime { get; set; } private readonly DispatcherTimer _timer;
Ces variables sont utilisées pour les événements de la Kinect. Il faut en premier lieu établir un tableau de squelettes pouvant être détectées par la Kinect. Ensuite, il faut préciser que l’on va utiliser une Kinect. Les variables de type Vector3 sont utilisées pour les coordonnées des parties du corps utiles au programme. Quatre booléens indiquent les actions en cours. On verra par la suite à quoi servent les deux derniers champs.
/// <summary> /// Initializes a new instance of the <see cref="MainWindow"/> class. /// </summary> public MainWindow() { try { InitializeComponent(); Loaded += WindowLoaded; _timer = new DispatcherTimer {Interval = TimeSpan.FromMilliseconds(2250)}; _timer.Tick += ResetingBoolTimer; _timer.Start(); CheckerTime = true; GoRight = false; GoLeft = false; EnterApplication = false; QuitApplication = false; } catch (Exception e) { Logger.Log(LogLevel.Fatal,e.InnerException.Message); } }
C’est dans cette méthode que l’on initialise les différents éléments au lancement de la fenêtre.
On va notamment initialiser les booléens permettant de représenter l’état des différentes actions.
Il y a 4 actions possibles :
- GoRight : aller vers la droite.
- GoLeft : aller vers la gauche.
- EnterApplication : démarrer une application.
- QuitApplication : sortir d’une application et revenir au menu principal.
Toutes ces variables sont initialisées à false. Lorsque l’une de ces variables passera à true, cela signifiera que l’on veut déclencher l’action correspondante. La variable CheckerTime indique si assez de temps s’est écoulé depuis la dernière action pour que l’on puisse en déclencher une nouvelle.
Toutes ces variables sont réinitialisées régulièrement afin de pouvoir les répéter sans obtenir une réponse trop rapide des mouvements. Ceci est fait par le biais de la méthode « ResetingBoolTimer ».
void ResetingBoolTimer(Object sender, EventArgs e) { CheckerTime = true; GoRight = false; GoLeft = false; EnterApplication = false; QuitApplication = false; } /// <summary> /// Windows the loaded. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param> private void WindowLoaded(object sender, RoutedEventArgs e) { try { kinectSensorChooser.KinectSensorChanged += KinectSensorChooserKinectSensorChanged; } catch (DirectoryNotFoundException dnfe) { Logger.Log(LogLevel.Fatal, dnfe.Message); } }
Nous précisons dans l’eventhandler WindowLoaded qu’au chargement de la page, on appelle la méthode gérant le changement des senseurs de la Kinect, cette méthode est principalement utilisée pour fixer les paramètres du périphérique.
void KinectSensorChooserKinectSensorChanged(object sender, DependencyPropertyChangedEventArgs e) { KinectSensor oldKinect = (KinectSensor)e.OldValue; StopKinect(oldKinect); _myKinect = (KinectSensor)e.NewValue; if (_myKinect == null) { return; } var parameters = new TransformSmoothParameters { Smoothing = 0.1f, Correction = 0.0f, Prediction = 0.0f, JitterRadius = 1.0f, MaxDeviationRadius = 0.5f }; _myKinect.SkeletonStream.Enable(parameters); _myKinect.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30); _myKinect.AllFramesReady += MyKinectAllFramesReady; try { _myKinect.Start(); } catch (IOException) { kinectSensorChooser.AppConflictOccurred(); } }
Il faut en premier lieu supprimer l’ancien senseur de la Kinect puis en déclarer un nouveau pour être certain d’utiliser le dernier connecté. Il est possible de paramétrer plusieurs éléments comme la fluidité de l’image ou des mouvements à gérer. Pour ce faire, ces modifications doivent être assignées aux éléments ayant besoin de ces derniers. Par exemple, nous les plaçons ici pour la détection des mouvements du squelette afin que la Kinect puisse mieux interpréter les mouvements et surtout pour évider des saccades au niveau de l’image. Il est également possible de déterminer quelle résolution doit être appliquée pour un affichage vidéo. Dans le programme, ce paramètre est pris en compte pour la visualisation de la profondeur de champ.
Ensuite, la partie la plus importante : on s’abonne à l’event AllFramesReady pour la récupération des flux de données :
_myKinect.AllFramesReady += MyKinectAllFramesReady;
Récupération des positions des points du corps
Dans l’eventhandler, il est possible de gérer l’affichage vidéo, l’affichage du squelette des personnes suivies, la profondeur du champ mais également récupérer les coordonnées des différents joints du corps.
Voici cette méthode si utile et qui est assez grande. C’est pourquoi, elle sera divisée pour un maximum de compréhension et de lisibilité.
/// <summary> /// Check if all frames for the Kinects are ready. /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The <see cref="Microsoft.Kinect.AllFramesReadyEventArgs"/> instance containing the event data.</param> void MyKinectAllFramesReady(object sender, AllFramesReadyEventArgs e) { /*Get the first skeleton*/ Skeleton first = GetFirstSkeleton(e); if (first == null) { return; } // On verra ce qui suit juste après… }
Tout d’abord, il faut se dire que les actions ne peuvent pas être actionnées par plusieurs personnes à la fois pour qu’il n’y ait pas de confusion au niveau de son utilisation. C’est pour cela, qu’il faut que le programme sache à quel squelette il doit se référer pour effectuer les manœuvres. On place ainsi dans un objet de type Skeleton, le premier squelette détecté par le biais de la méthode « GetFirstSkeleton », développée ci-dessous.
Skeleton GetFirstSkeleton(AllFramesReadyEventArgs e) { Skeleton first = null; try { // Récupération de toutes les informations sur les Skeletons pour la frame en cours using (SkeletonFrame skeletonFrameData = e.OpenSkeletonFrame()) { if (skeletonFrameData == null) { return null; } // On copie les données dans notre tableaux de Skeletons skeletonFrameData.CopySkeletonDataTo(_allSkeletons); // On récupère le premier Tracked first = (from s in _allSkeletons where s.TrackingState == SkeletonTrackingState.Tracked select s).FirstOrDefault(); } } catch (NullReferenceException nre) { Logger.Log(LogLevel.Fatal, nre.Message); } return first; }
Rien de bien compliqué ici, on récupère simplement l’ensemble des squelettes trackés, et ensuite on cherche celui qui a été suivi en premier parmi toutes les personnes présentent devant l’objectif de la Kinect.
Nous pouvons constater la présence de deux blocs « using ». Ces parties de code permettent, pour le premier, d’accéder aux fonctions des squelettes établies par la Kinect, et pour le second, de gérer les notions de profondeur.
L’instruction using permet dans ce cas-ci de déclarer que l’on utilise des objets de type IDisposable, et qui font appel à des ressources non-managées. En les instanciant dans cette instruction, on s’assure que ces objets seront correctement disposés une fois sorti de la clause using.
Si on revient dans MyKinectAllFramesReady, on a donc une variable first contenant le premier skeleton tracké. Si la variable contient bien quelque chose, on va analyser son contenu, en parcourant l’ensemble des points du squelette (appelés Joints) :
foreach (Joint joint in first.Joints) { // Faire quelque chose avec chaque joint }
- Obtenir les coordonnées des mains
/*Hands*/ if (joint.JointType == JointType.HandRight) { RightHand = new Vector3(joint.Position.X, joint.Position.Y, joint.Position.Z); } if (joint.JointType == JointType.HandLeft) { LeftHand = new Vector3(joint.Position.X, joint.Position.Y, joint.Position.Z); }
- Obtenir celles des coudes
/*Elbows*/ if(joint.JointType==JointType.ElbowRight) { RightElbow = new Vector3(joint.Position.X, joint.Position.Y, joint.Position.Z); } if (joint.JointType == JointType.ElbowLeft) { LeftElbow = new Vector3(joint.Position.X, joint.Position.Y, joint.Position.Z); }
- Obtenir celles du centre de la colonne vertébrale, qui sert à remplacer la position du nombril
/*Spine*/ if(joint.JointType==JointType.Spine) { Spine=new Vector3(joint.Position.X,joint.Position.Y,joint.Position.Z); }
Ces coordonnées sont ainsi stockées dans des variables spécialisées de type Vector3.
Analyse des positions et détection de posture
Une fois ces données récupérées, il est possible de déterminer quelle action doit être effectuée.
Aller vers la gauche et la droite
/*Right & left hands to swipe*/ if(RightHand.Y>RightElbow.Y) { GoRight = true; } if (LeftHand.Y > LeftElbow.Y) { GoLeft = true; } if (GoLeft && CheckerTime) { if (_control.Content == null) { //ACTION } CheckerTime = false; } else if (GoRight && CheckerTime) { if (_control.Content == null) { //ACTION } CheckerTime = false; }
Pour chaque main, le même procédé est appliqué, c’est pourquoi seul le traitement de celle de droite sera expliqué ci-dessous.
Avec les différentes coordonnées récupérées, on vérifie que celles de la main dépassent celles du coude et dans ce cas, on passe le booléen en vrai. Ensuite, si cette variable est true et que CheckerTime l’est également, le mouvement peut s’appliquer. On passe ensuite CheckerTime à false pour indiquer qu’une action vient d’être effectuée, et qu’il n’est pas question d’en faire une autre immédiatement. Le timer initialisé en début de programme remettra cette variable à true après un bref laps de temps, pour permettre de déclencher une nouvelle action.
Ouvrir et fermer une application
On peut utiliser le même procédé pour établir des zones d’action mais il faut dans ce cas prend en compte l’axe des X. Les coordonnées des mains doivent pénétrer dans cette zone pour effectuer l’action voulue.
Voici le déroulement concret :
/*Right hand over the spine to enter into application*/ if (RightHand.X <= Spine.X + 0.1 && RightHand.X >= Spine.X - 0.1 && RightHand.Y <= Spine.Y+0.1 && RightHand.Y >= Spine.Y-0.1) { EnterApplication = true; } if(EnterApplication && CheckerTime) { //ACTION CheckerTime = false; } /*Left hand over the spine to quit an application*/ if (LeftHand.X <= Spine.X + 0.1 && LeftHand.X >= Spine.X - 0.1 && LeftHand.Y <= Spine.Y + 0.1 && LeftHand.Y >= Spine.Y - 0.1) { QuitApplication = true; } if (QuitApplication && CheckerTime) { //ACTION CheckerTime = false; }
Dans ce cas-ci, on vérifie si l’une des deux mains se trouve dans un carré de 20 cm de côté, centré au niveau du nombril. Si c’est le cas pour la main droite, cela indique que l’utilisateur veut ouvrir l’application ciblée. Si c’est le cas pour la main gauche, c’est qu’il veut quitter l’application.
À propos de l’auteur
Jason Dejaégère, actuellement étudiant à la HELHa à Mons en dernière et stagiaire au Microsoft Innovation Center. Je suis donc programmeur de base mais également designer à mes heures perdues. Vous pouvez me contacter par email: dej.jason@gmail.com |
/JasonDejaegere |