Présentation du SDK Kinect – Partie 4

Ceci est la quatrième partie d’une série d’articles sur l’utilisation du SDK Kinect, pour voir la troisième partie c’est par ici.

Préparer la récupération du squelette

Maintenant que nous avons vu comment récupérer l’image RGB de la camera du Kinect et comment utiliser les données de profondeur fournies également par le Kinect, il est temps de passer au skeletal tracking. Tout d’abord, qu’est-ce que c’est ? Il s’agit tout simplement et comme son nom l’indique du fait que Kinect va repérer votre squelette et va commencer à le suivre afin de savoir à tout moment où se trouve votre main droite ou votre genou gauche par exemple. Pour que ce tracking soit possible, il faut que votre corps soit visible en quasi totalité par le capteur. Dès lors que Kinect a repéré un joueur et son squelette il va alors pouvoir être possible de récupérer les informations sur chaque os de chaque joueur. A noter que Kinect ne peut observer et suivre le squelette de 2 personnes à la fois mais pas plus, contrairement à ce que l’on a pu voir avec les données de profondeur.

Pour ce qui est des os que peut détecter le capteur, il est possible de récupérer les informations d’un total de 20 os, voici le schéma de la documentation du SDK qui montre quels sont les os qui sont identifiés sur un corps humain.

Les joints repérés par Kinect

Les joints du corps humains détéctés pas Kinect

On remarque donc que l’on va pouvoir accéder à pas mal d’informations sur la posture des joueurs. Maintenant que la petite partie théorique est faite, passons au code.
A l’image de ce qui a été fait avec la caméra couleur ou les données de profondeur, il va falloir se préparer dans le code à recevoir les informations du squelette. La première chose à faire est de signaler au runtime Kinect que l’on va souhaiter recevoir ces informations, si vous avez suivi les articles précédents, c’est déjà fait avec les arguments de la méthode Initialize lorsque l’on souhaitait récupérer le PlayerIndex.

_nui.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseDepthAndPlayerIndex | RuntimeOptions.UseSkeletalTracking);

Vous aurez compris ici que la partie importante est le RuntimeOptions.UseSkeletalTracking, le reste est juste là pour que ce que l’on a fait avant fonctionne toujours.

Il faut ensuite s’abonner à l’évènement SkeletonFrameReady qui sera appelé dès lors qu’un squelette (ou deux) sont visibles par le capteur.

_nui.SkeletonFrameReady += nui_SkeletonFrameReady;

Il ne nous reste donc plus qu’à créer la méthode appelée par l’évènement et l’implémenter pour traiter les données fournies par Kinect.

void nui_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
}

Traitement des informations du squelette

On peut remarquer que la méthode appelée par l’événement SkeletalFrameReady fournie en argument un objet de type SkeletonFrameReadyEventArgs, c’est donc lui qui contient toutes les informations relatives aux squelettes visibles par Kinect.
Bien que Kinect ne puisse suivre que 2 squelettes, cet objet nous fournit un tableau de 6 squelettes. Pourquoi ça ? Parce que tout comme ce que l’on a vu dans l’article précédent, les informations de profondeur ont un PlayerIndex codé sur 3 bits, Kinect peut donc « voir » jusqu’à 6 joueurs mais pour des questions de fluidité ne peut calculer et suivre le squelette que de 2 d’entre eux.
Pour bien voir cela, on va commencer par récupérer l’information sur la frame actuelle dans notre méthode nui_SkeletonFrameReady.

void nui_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
    SkeletonFrame frame = e.SkeletonFrame;
}

Une fois ceci fait, vous pouvez mettre un breakpoint à la sortie de la méthode puis lancer le programme en debug afin d’observer et comprendre ce qu’il se passe. Déjà on peut voir que l’événement ne se déclenche que lorsque vous êtes en quasi totalité dans le champs de vision du Kinect. Ensuite, la propriété qui nous intéresse pour l’instant est Skeletons.

Etat du programme au breakpoint

La propriété Skeletons au niveau du breakpoint


On peut aussi observer que le squelette qui représente le joueur suivi (ici le 6ème) a sa propriété TrackingState à Tracked alors que tous les autres sont à NotTracked. Il va donc falloir à chaque fois parcourir ce tableau de Skeleton et ne traiter que ceux qui ont le bon TrackingState.

foreach (SkeletonData skeleton in frame.Skeletons)
{
    if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
    {
        // On traite le squelette
    }
}

Il est temps de passer au dessin de notre squelette dans l’application ! Pour cela, on va créer un canvas qui servira de conteneur à des ellipses qui représenteront chacune un des os fournis par Kinect. Voici le code XAML correspondant.

<Canvas Name="skeletonCanvas" Width="320" Height="240" HorizontalAlignment="Left" Margin="700,0,0,0">
    <Ellipse Name="headEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="rightShoulderEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="rightWristEllipse" Width="20" Height="20" Fill="Red" />
    <Ellipse Name="leftShoulderEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="leftWristEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="spineEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="rightHipEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="rightAnkleEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="leftHipEllipse" Width="20" Height="20" Fill="Red"/>
    <Ellipse Name="leftAnkleEllipse" Width="20" Height="20" Fill="Red"/>
</Canvas>

Ca fait beaucoup d’ellipse, et encore je n’ai pas mis tous les os possibles. On aura ici la tête, les deux épaules, les deux poignets, les deux hanches et les deux chevilles.
Retournons maintenant dans notre méthode nui_SkeletonFrameReady pour bouger nos ellipses. On a vu donc que l’on ne faisait le traitement que sur les squelettes qui sont réellement suivies. Ces squelettes nous fournissent une propriété Joints qui est une collection de tous les joints accessibles. Pour accéder aux informations d’un os, on utilise une énumération incluses dans le SDK qui est JointID. Ainsi, s’il on veut par exemple récupérer les informations sur la tête, on va utiliser:

Joint headJoint = skeleton.Joints[JointID.Head];

Rien de compliqué donc ! Ce joint nous donne accès une propriété Position qui donne la position en 3D sous forme de vecteur, cependant nous on souhaite projeter ceci dans un plan 2D (notre Canvas). On va donc utiliser une méthode fournie par le SDK qui permet de passer d’un vecteur 3D à 2 valeurs représentant le X et le Y de notre plan 2D.
La méthode à utilisée est _nui.SkeletonEngine.SkeletonToDepthImage, voici l’exemple pour placer correctement l’ellipse de la tête en fonction des données récupérées via le Kinect.

if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
{
    // On récupère le joint qui représente la tête
    Joint headJoint = skeleton.Joints[JointID.Head];

    // On créé les variables qui contiendront les coordonnées projetées
    float x = 0;
    float y = 0;

    // On projette la position de la tête dans un espace 2D
    _nui.SkeletonEngine.SkeletonToDepthImage(headJoint.Position, out x, out y);

    // On place l'ellipse de la tête au bon endroit dans le canvas
    Canvas.SetLeft(headEllipse, x * skeletonCanvas.Width);
    Canvas.SetTop(headEllipse, y * skeletonCanvas.Height);
}

A noter que l’on multiplie les valeurs trouvées x et y car la projection renvoie des valeurs comprises entre 0 et 1 pour indiquer de façon relative si l’on est plutôt d’un coté ou de l’autre du champs de vision.
Étant donné que pas mal de code est nécessaire pour faire fonctionner tout ceci et surtout qu’il va falloir le répéter autant de fois qu’il y a d’os, nous allons créer une méthode qui se charge de faire la totalité des actions nécessaires.

private void ProjectAndMoveEllipse(Microsoft.Research.Kinect.Nui.Vector vecteurPosition, UIElement ellipse)
{
    // On créé les variables qui contiendront les coordonnées projetées
    float x = 0;
    float y = 0;

    // On projette le vecteur en 2D
    _nui.SkeletonEngine.SkeletonToDepthImage(vecteurPosition, out x, out y);

    // On place l'ellipse avec les coordonnées projetées trouvées
    Canvas.SetLeft(ellipse, x * skeletonCanvas.Width);
    Canvas.SetTop(ellipse, y * skeletonCanvas.Height);
}

Ce qui nous donne donc plus que ceci dans notre méthode nui_SkeletonFrameReady.

void nui_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
    // On récupère la frame à traiter
    SkeletonFrame frame = e.SkeletonFrame;

    foreach (SkeletonData skeleton in frame.Skeletons)
    {
        if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
        {
            // On utilise notre méthode pour bouger chaque ellipse en fonction de la position de son os associé
            ProjectAndMoveEllipse(skeleton.Joints[JointID.Head].Position, headEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.ShoulderRight].Position, rightShoulderEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.WristRight].Position, rightWristEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.ShoulderLeft].Position, leftShoulderEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.WristLeft].Position, leftWristEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.Spine].Position, spineEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.HipRight].Position, rightHipEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.AnkleRight].Position, rightAnkleEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.HipLeft].Position, leftHipEllipse);
            ProjectAndMoveEllipse(skeleton.Joints[JointID.AnkleLeft].Position, leftAnkleEllipse);
        }
    }
}

Et voila, vous pouvez lancer votre application et admirer votre squelette se déplacer en même temps que vous.

Pour aller plus loin

Vous avez pu remarquer que votre squelette est toujours un peu en retard sur vos mouvements réelles et que parfois ce dernier tremble légèrement mais rapidement s’il perd une partie de votre corps. Afin d’affiner la précision et améliorer la latence du tracking, le SDK fournit des options paramétrables en fonction des besoins de votre application.
Ces options se trouvent dans _nui.SkeletonEngine.SmoothParameters et doivent être modifiées après l’initialisation du Runtime. Voici donc le code qui permet de modifier ces paramètres.

_nui.SkeletonEngine.TransformSmooth = true;
_nui.SkeletonEngine.SmoothParameters = new TransformSmoothParameters
{
    Smoothing = 0.75f,
    Correction = 0.1f,
    Prediction = 0.05f,
    JitterRadius = 0.15f,
    MaxDeviationRadius = 0.04f
};

Le mieux étant évidemment de modifier ces valeurs et de les tester pour assimiler leur fonction respective. A savoir qu’il n’y a pas de réglage parfait, celui-ci dépend de votre application et de quel usage vous voulez faire du Kinect.

Vous aurez remarqué que le SDK n’inclut pas par défaut de système de détection de geste, il va donc falloir s’en charger nous même s’il l’on souhaite détecter des gestes au sein de notre application. Je ne vais pas faire d’exemple dans cet article mais je vous conseil de consulter ce blog (en anglais) si vous êtes intéressés par de telles fonctionnalités.

Ceci clôture cet article, nous n’avons évidemment pas tout vu sur le skeletal tracking mais cela fait de bonne bases pour commencer à vraiment utiliser au mieux les capteurs de Kinect dans ses applications. Il reste un dernier article qui traite la reconnaissance vocale avec Kinect.

Publié le 10/07/2011, dans Kinect, WPF, et tagué , , , , . Bookmarquez ce permalien. Poster un commentaire.

Laisser un commentaire