Résultat de Recherche pour squelette

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.

Présentation du SDK Kinect – Partie 3

Ceci est la troisième partie d’une série d’articles, si vous avez raté la partie 2, c’est ici !

Mise en place de la récupération des distances

Maintenant que nous avons vu comment récupérer les images capturées par le capteur couleur du Kinect, il est temps de passer à des choses un peu plus sympas ! De façon globale, la récupération des informations de profondeur est quasi identique à la récupération de la vidéo couleur. La chose principale qui change est le type de donnée que l’on récupère où en tout cas ce qu’elle représente. En effet, pour l’image couleur, on a vu que Kinect renvoie gentiment un tableau de Byte qui représente clairement une image. Dans le cas de la profondeur (récupérée via les capteurs infrarouges, je le rappelle), on va recevoir à nouveau un tableau de Byte (du binaire donc) mais celui-ci ne va pas représenter une image mais un tableau de distances. Concrètement, chaque case du tableau représente à quel distance le point représenté par le pixel est par rapport au capteur.
Pourquoi ne récupère-t-on pas directement un tableau de Byte qui représente l’image de profondeur ? Parce que cela va nous permettre de construire notre propre image telle qu’on la souhaite, nous allons voir ça juste après.

Bien, après ce petit interlude théorique, passons à la pratique. La première chose à faire est de modifier l’initialisation du Runtime pour que Kinect prévienne lorsque l’on peut récupérer des informations de profondeur. Deux choix s’offrent à nous:

_nui.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseDepth);

ou

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

Les noms sont, je pense, assez clairs, RunTimeOptions.UseDepth permet de récupérer uniquement l’information sur la distance d’un pixel alors que RunTimeOptions.UseDepthAndPlayerIndex permet également de savoir si ce pixel fait partie d’un joueur ou non et le cas échéant de savoir lequel. A noter qu’il faut que Kinect trouve votre squelette pour savoir que vous êtes un joueur et donc fournir correctement cette information (la reconnaissance de squelette est quasi immédiate pour peu que votre corps entier soit présent dans le champs de la camera, plus d’informations dans la partie suivante). C’est pour cela que j’ai aussi ajouté RuntimeOptions.UseSkeletalTracking.
Nous allons ici utiliser la profondeur avec information sur le joueur.
Maintenant que ceci est fait, il suffit de faire grosso modo la même chose que pour la caméra couleur en commençant par ouvrir le flux pour la profondeur.

_nui.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex);

Ensuite on s’abonne à l’événement DepthFrameReady du Runtime qui sera lui aussi déclenché environ 30 fois pas seconde.

_nui.DepthFrameReady += nui_DepthFrameReady;

On va alors pouvoir récupérer l’objet PlanarImage de l’argument de type ImageFrameReadyEventArgs.

void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
    PlanarImage profondeurImage = e.ImageFrame.Image;
}

Récupérer les distances à partir du tableau d’octets

C’est donc ici que la subtilité commence ! Comme je l’ai dit plus haut PlanarImage n’est pas une image en soit, il s’agit plutôt d’un tableau de Byte qui va nous permettre de connaitre à quelle distance du capteur se situe le point représenté par un pixel. Chaque pixel contient une distance qui est enregistrée sur 2 octets. Le schéma ci-dessous représente le contenu de ces deux octets en fonction du type de donnée de profondeur choisie dans l’initialisation (Depth tout seul ou Depth & PlayerIndex).

Schéma d'oganisation des octets

Détail de composition des octets pour la distance

A noter que les données entrées dans ce schéma ne sont ABSOLUMENT pas représentatives de ce qui pourrait arriver.
Maintenant que ceci est dit, on va pouvoir faire notre méthode qui va nous retourner la distance pour un pixel donné ! Il faut tout de même avant ça savoir que la distance est la combinaison des deux octets et que l’octet ayant l’indice le plus grand dans le tableau du PlanarImage est l’octet qui a le poids le plus fort (donc qui doit être le plus à gauche).
Il va donc être nécessaire de faire un « déplacement de bit », bitshift en anglais qui va nous permettre de replacer le tout à un endroit cohérent et récupérer la distance. Voici la méthode qui permet de retourner la distance en millimètre d’un pixel sans le player index:

private int GetDistance(byte byte1, byte byte2)
{
    int distance = (int)(byte1 | byte2 << 8);
    return distance; 
}

On voit bien ici que l’on décale de 8 bits vers la gauche (donc d’un octet entier) le deuxième octet avant de combiner les deux de façon à retrouver un entier codé sur 2 octets cohérents.
Voici la même méthode mais cette fois-ci en prenant en compte le cas où l’on a le player index d’indiqué dans les octets:

private int GetDistanceWithPlayerIndex(byte byte1, byte byte2)
{
    int distance = (int)(byte1 >> 3 | byte2 << 5);
    return distance; 
}

On décale le premier octet de 3 bits vers la droite pour faire sortir le PlayerIndex de l’octet puis on lui colle devant le deuxième octet (un décalage de 5 vers la gauche suffit puisque 3 bits ont été enlevé sur le premier).
Et voila ! nous sommes prêt à créer une image personnelle qui représentera ces distances récupérées par Kinect.

Construire une depth map (image de profondeur)

Pour cela, il faut savoir que Kinect peut récupérer les distances dans un spectre allant de 850mm à 4000mm, toute distance de 0 signifie que le capteur ne connait pas l’information (peut être causé par une distance trop proche, une réflectivité trop importante, …).
Je vous donne la méthode qui créé l’image en binaire à partir du tableau de byte des distances :

private byte[] CreateImageFromDistances(PlanarImage distanceImage)
{
    // On récupère la largeur et la hauteur de "l'image" des distances (ici normalement 320x240)
    int width = distanceImage.Width;
    int height = distanceImage.Height;

    // On créé le tableau de byte qui contiendra notre image réelle en binaire que l'on affichera plus tard
    byte[] imageCreeeBinaire = new byte[width*height];
    // On créé l'index qui permettra de savoir où l'on est de la création.
    int positionCreation = 0;

    // On définit les bornes de visibilité de notre capteur pour le calcul de la couleur plus tard
    const int minDistance = 850;
    const int maxDistance = 4000;

    // On parcourt tout le tableau d'octet des distances par saut de 2
    for (int i = 0; i < distanceImage.Bits.Length - 1; i += 2)
    {
        // On récupère la distance du pixel en cours de traitement
        int distance = GetDistanceWithPlayerIndex(distanceImage.Bits[i], distanceImage.Bits[i + 1]);

        // On calcule le poids de la couleur (de 0 à 255) en fonction de la distance
        imageCreeeBinaire[positionCreation] = (byte)(255 * Math.Max(distance - minDistance, 0) / maxDistance);

        // On incrémente la position de la création de l'image
        positionCreation++;
     }

     // Enfin, on retourne l'image binaire créée
     return imageCreeeBinaire;

J’ai pas mal commenté ce code pour qu’il soit plus facile à comprendre. Globalement on parcourt tout le tableau d’octets que nous fournit le capteur par pas de deux (puisque une distance est sur 2 octets) et en fonction de la distance du pixel traité, on ajoute un octet à notre image finale. A noter que notre image finale sera un dégradé de gris, donc un seul octet suffit par pixel (plus la valeur est élevée plus c’est blanc, inversement plus elle est faible plus c’est noir). Dans notre cas donc, plus le pixel de distance représentera quelque chose de prêt, plus le pixel de notre image finale sera noir.

Et voila ! Il ne reste plus qu’à appliquer le tableau d’octets donné par cette méthode en tant que source d’un contrôle WPF Image que je vous laisse le soin de créer.

void nui_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
    // On récupère "l'image" prise par le capteur infrarouge
    PlanarImage imageProfondeur = e.ImageFrame.Image;

    // On créé l'image binaire en échelle de gris à partir des informations de distance
    byte[] imageBinaire = CreateImageFromDistances(e.ImageFrame.Image);

    // Puis tout comme avec la couleur, on définit la source pour le contrôle WPF
    imageDepth.Source = BitmapSource.Create(
        imageProfondeur.Width, imageProfondeur.Height, 96, 96, PixelFormats.Gray8,
        null, imageBinaire, imageProfondeur.Width);
}

Et voici le résultat au lancement de l’application.

Screenshot du projet après depth map

Le projet avec la depth map

Comme vous pouvez le voir plus les objets sont loin du capteur plus ils sont blancs et c’est ce que l’on voulait !

Pour aller plus loin

On a vu comment afficher une image qui représente la distance des objets par rapport au capteur Kinect mais on n’a fait aucune utilisation du PlayerIndex qui est aussi disponible pour peu que l’initialisation du Runtime et l’ouverture du flux soient bien faites.
Le PlayerIndex (qui se récupère, je le rappelle, sur le premier octet des données de distance pour un pixel) peut prendre 6 valeurs différentes :

  • 0 signifie que le pixel concerné ne correspond pas à un joueur
  • 1 signifie que le pixel concerné correspond au squelette 0
  • 2 signifie que le pixel concerné correspond au squelette 1
  • etc..

En théorie il est possible de détecter 5 joueurs différents avec les données de profondeur (par contre seulement 2 squelettes), je n’ai personnellement pu tester qu’avec 3.
En faisant référence au schéma des données pour la distance et le PlayerIndex, on peut en déduire cette méthode pour retrouver le PlayerIndex:

private int GetPlayerIndex(byte octet)
{
    return (int)(octet & 7);
}

En effet, la représentation de 7 en binaire est 00000111, ce qui signifie que l’opération & mettra à 0 tous les bits qui ne concernent pas le PlayerIndex et nous retournera ainsi la bonne information.
Pour finir voici la méthode CreateImageFromDistances modifiée de façon à ce que les joueurs apparaissent en couleur lorsqu’ils sont détectés. La plus grosse différence est le type de données que l’on retourne, notre image finale n’aura plus besoin d’un octet par pixel mais de trois qui définissent chacun la quantité d’une couleur de base (rouge, vert et bleu).

private byte[] CreateImageFromDistances(PlanarImage distanceImage)
{
    // On récupère la largeur et la hauteur de "l'image" des distances (ici normalement 320x240)
    int width = distanceImage.Width;
    int height = distanceImage.Height;

    // On créé le tableau de byte qui contiendra notre image réelle en binaire que l'on affichera plus tard
    byte[] imageCreeeBinaire = new byte[width*height*3];
    // On créé l'index qui permettra de savoir où l'on est de la création.
    int positionCreation = 0;

    // On définit les bornes de visibilité de notre capteur pour le calcul de la couleur plus tard
    const int minDistance = 850;
    const int maxDistance = 4000;

    // On parcourt tout le tableau d'octet des distances par saut de 2
    for (int i = 0; i < distanceImage.Bits.Length - 1; i += 2)
    {
        // On regarde si le pixel appartient à un joueur
        if (GetPlayerIndex(distanceImage.Bits[i]) != 0)
        {
            // Si c'est le cas, on colorie le joueur en fonction de son numero
            switch (GetPlayerIndex(distanceImage.Bits[i]))
            {
                // Si c'est le squelette 0
                case 1:
                    // On met le rouge au max, les autres couleurs à 0
                    imageCreeeBinaire[positionCreation] = 255;
                    imageCreeeBinaire[positionCreation + 1] = 0;
                    imageCreeeBinaire[positionCreation + 2] = 0;
                    break;
                // Si c'est le squelette 1
                case 2:
                    // On met le vert au max, les autres couleurs à 0
                    imageCreeeBinaire[positionCreation] = 0;
                    imageCreeeBinaire[positionCreation + 1] = 255;
                    imageCreeeBinaire[positionCreation + 2] = 0;
                    break;
                // Si c'est le squelette 2
                case 3:
                    // On met le bleu au max, les autres couleurs à 0
                    imageCreeeBinaire[positionCreation] = 0;
                    imageCreeeBinaire[positionCreation + 1] = 0;
                    imageCreeeBinaire[positionCreation + 2] = 255;
                    break;
                default:
                    imageCreeeBinaire[positionCreation] = 255;
                    imageCreeeBinaire[positionCreation + 1] = 0;
                    imageCreeeBinaire[positionCreation + 2] = 255;
                    break;
            }
        }
        // Sinon, on colorie en echelle de gris en fonction de la distance
        else
        {
            // On récupère la distance du pixel en cours de traitement
            int distance = GetDistanceWithPlayerIndex(distanceImage.Bits[i], distanceImage.Bits[i + 1]);

            // On calcule le poids de la couleur (de 0 à 255) en fonction de la distance et on l'applique aux 3 couleurs pour obtenir du gris
            byte grayIntensity = (byte)(255 * Math.Max(distance - minDistance, 0) / maxDistance);
            imageCreeeBinaire[positionCreation] = grayIntensity;
            imageCreeeBinaire[positionCreation + 1] = grayIntensity;
            imageCreeeBinaire[positionCreation + 2] = grayIntensity;
        }
        // On incrémente de 4 la position de la création de l'image (les 3 couleurs + un octet vide pour respecter le format de pixel)
        positionCreation+=3;
    }

    // Enfin, on retourne l'image binaire créée
    return imageCreeeBinaire;
}

Ne pas oublier également de modifier le BitmapSource.Create du contrôle WPF.

imageDepth.Source = BitmapSource.Create(
    imageProfondeur.Width, imageProfondeur.Height, 96, 96, PixelFormats.Bgr24,
    null, imageBinaire, imageProfondeur.Width * 3);

Et voila, le tour est joué lorsque vous êtes entièrement visible par le capteur et que votre squelette est repéré, votre silhouette apparait colorée !
Ceci montre bien qu’il est très pratique de récupérer les données de distance plutôt qu’une image toute prête à l’emploi puisque l’on peut assez facilement créer sa propre depth map en fonction de ses besoins.

Ce long article est terminé ! Rendez-vous dans le 4ème article de la série pour découvrir le skeleton tracking et tout ce que qui en découle !

Présentation du SDK Kinect – Partie 2

Ceci est la deuxième partie de la série d’article d’introduction au SDK Kinect. Si vous avez raté la première partie, c’est par là.

Mise en place du projet pour Kinect

Dans la première partie, nous avons vu comment installer tous les outils nécessaires au développement avec Kinect, il est donc grand temps de nous y mettre pour de vrai dès maintenant.
Pour les démonstrations du développement, j’utiliserai un projet WPF, je vous laisse le soin de le créer par vous même. Voici donc la vue de mon espace de travail juste après la création du projet.

Projet wpf à l'origine

Juste après création du projet

Vous vous en doutez, nous allons avoir besoin d’ajouter des références à notre projet pour pouvoir utiliser les fonctionnalités du SDK, la référence à ajouter est Microsoft.Research.Kinect. Cette librairie est normalement présente dans le GAC après l’installation donc vous la trouverez directement dans l’onglet .NET de l’ajout de référence. Une fois que ceci est fait, vous remarquerez que 2 namespaces sont ajoutables depuis cette librairie:

using Microsoft.Research.Kinect.Nui;

et

using Microsoft.Research.Kinect.Audio;

Pour cette article, nous n’aurons besoin que du namespace nui, qui signifie Natural User Interface et va nous permettre de récupérer les flux vidéos depuis Kinect.

La toute première chose à faire lorsque l’on va utiliser les fonctionnalités du Kinect est d’initialiser le runtime. Qui dit initialisation dit aussi dés-initialisation afin de libérer Kinect pour éviter tout conflit lors d’une utilisation ultérieure.
Je vous conseil de vous abonner à l’évènement Loaded de la fenêtre principale et de faire l’initialisation du runtime dans la méthode associée plutôt que de le faire directement dans le constructeur de la fenêtre. A l’inverse, la dés-initialisation devrait être faite lorsque la fenêtre se ferme, donc à l’appel de l’événement Closed.
Ainsi la structure de base de tout projet utilisant Kinect devrait ressembler à ça:

public partial class MainWindow : Window
{
    // L'attribut de type Runtime qui sera le point d'entrée à tout interaction avec Kinect
    private Runtime _nui = new Runtime();

    public MainWindow()
    {
        InitializeComponent();

        // On s'abonne à l'évènement de fin de chargement pour aller initialiser le runtime
        Loaded += MainWindow_Loaded;

        // Même chose avec l'évènement de fermeture et pour la dés-initialisation
        Closed += MainWindow_Closed;
    }

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        try
        {
            // On essaye d'initialiser le runtime en spécifiant les informations qui nous intéressent
            _nui.Initialize(RuntimeOptions.UseColor);
        }
        catch (InvalidOperationException)
        {
            // Si on ne peut initialiser le Kinect, on affiche un message d'erreur
            MessageBox.Show("Erreur lors de l'initialisation du runtime, vérifier que le Kinect est bien branché.");
            return;
        }
    }

    void MainWindow_Closed(object sender, EventArgs e)
    {
        _nui.Uninitialize(RuntimeOptions.UseColor);
    }
}

Je pense que le code n’est pas très compliqué à comprendre, on créé un attribut de type Runtime qui va nous permettre toute interaction avec Kinect, puis on l’initialise en spécifiant quelles informations on va souhaiter récupérer par la suite (image couleur, données de profondeur, tracking de squelette, etc..). Les informations passées en argument de Initialize peuvent être cumulées avec des |, par exemple:

_nui.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseDepth);

Enfin, on arrête l’utilisation du Kinect lorsque la fenêtre est fermée avec la dés-initialisation du runtime.
Vous pouvez essayer de lancer dès maintenant l’application et si vous n’avez aucune MessageBox qui apparait, alors vous êtes prêt à récupérer les informations du Kinect !
Nous allons d’ailleurs commencer tout de suite par la récupération de l’image RGB fournie par le capteur.

Récupération de l’image couleur du capteur Kinect

Maintenant que que nous avons initialisé le runtime du Kinect, nous allons pouvoir récupérer les informations fournies par celui-ci. Pour cela, le SDK nous fournit un flux que l’on peut récupérer et lire à notre guise (à condition de l’avoir spécifié en argument de Initialize).

try
{
    // On ouvre le flux vidéo pour pouvoir récupérer les images de la caméra RGB
    _nui.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color);
}
catch (InvalidOperationException)
{
    // Si le flux n'a pu s'ouvrir, on le signal
    MessageBox.Show("Impossible d'ouvrir les flux, vérifiez que vous tentez d'ouvrir un flux à un format et une taille prise en charge");
    return;
}

On tente ici d’ouvrir le flux vidéo en passant en argument les informations du flux que l’on souhaite récupérer. Le premier argument correspond au type de flux que l’on veut avoir, ici le flux vidéo, le deuxième argument est le nombre de tampon qui va être utilisé (la valeur 2 est celle par défaut et permet d’avoir l’image actuelle ainsi qu’une image d’avance). Ensuite on spécifie la résolution de l’image que l’on souhaite récupérer, il est possible d’obtenir toutes les résolutions utilisables à l’aide de la méthode ImageStream.GetValidResolutions(ImageType). Enfin, le dernier argument permet de dire sous quelle format on souhaite récupérer le flux, ici sous forme d’image couleur.

Très bien ! Maintenant que le flux est ouvert, un événement est déclenché environ 30 fois par seconde permettant de récupérer le flux produit, celui-ci s’appelle VideoFrameReady. Il va donc falloir s’y abonner pour récupérer son objet de type ImageFrameReadyEventArgs qui lui même contient une PlanarImage représentant l’image récupérée. Le SDK n’étant pas associé à une plateforme particulière(WPF / Winforms/ …), il contient sa propre classe de définition d’une image avec justement cette classe PlanarImage.
Voici donc en code ce qui est dit juste avant:

// On s'abonne à l'événement déclenché dès qu'une image est prête à être traitée (environ 30 fois pas seconde)
_nui.VideoFrameReady += nui_VideoFrameReady;
void nui_VideoFrameReady(object sender, ImageFrameReadyEventArgs e)
{

}

Puisque PlanarImage ne représente pas une image en WPF, nous allons créer un contrôle WPF de type Image qui sera notre hôte pour l’image couleur du Kinect.

<Window x:Class="IntroductionSDKKinect.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="400" Width="1000">
    <Grid>
        <Image Name="imageRGB" Width="320" Height="240" HorizontalAlignment="Left" Margin="20,0,0,0"/>
    </Grid>
</Window>

Il ne nous reste plus qu’à transformer l’image récupérée par Kinect en une source de données affichable par le contrôle WPF.

void nui_VideoFrameReady(object sender, ImageFrameReadyEventArgs e)
{
   // On récupère l'image fournie par Kinect
   PlanarImage imageFromKinect = e.ImageFrame.Image;

   // On définit la source du contrôle Image WPF en utilisant l'image récupérée
   imageRGB.Source = BitmapSource.Create(imageFromKinect.Width, imageFromKinect.Height, 96, 96, PixelFormats.Bgr32,
                                         null, imageFromKinect.Bits, imageFromKinect.Width * imageFromKinect.BytesPerPixel);
}

Je ne vais pas m’étaler sur les paramètres de la méthode BitmapSource.Create mais si vous souhaitez plus d’informations, vous pouvez consulter la page msdn.
Vous pouvez sans plus attendre lancer votre application et vous admirer à travers la camera du Kinect 😀 .

La scene vue en couleur par Kinect

La vue de la Kinect en couleur.

Ceci clôture cette deuxième partie, rendez-vous donc dans la troisième pour traiter la camera de profondeur (qui utilise l’infrarouge).

Présentation du SDK Kinect – Partie 1

Le SDK enfin disponible !

Ca y est ! Microsoft l’avait annoncé pour ce printemps et c’est maintenant chose faite, le SDK pour Kinect est disponible en téléchargement libre depuis le 16 juin dernier.
A l’aide de ce kit de développement, il est maintenant possible et facile d’utiliser les fonctionnalités du Kinect (en évitant par la même occasion d’utiliser des moyens un peu moins officiels). Les fonctionnalités contrôlables via du code incluent:

  • Inclinaison du capteur via activation de la motorisation
  • Capture de l’image couleur
  • Capture de l’image de profondeur (récupérée via infrarouge)
  • Tracking de squelette : le SDK permet de suivre le squelette de 2 joueurs en même temps
  • Récupération des données audio 3D
  • Reconnaissance Vocale : elle n’est pas incluse directement dans le SDK Kinect puisqu’il faut utiliser l’API Speech Recognition déjà existante (et utilisée sur Windows 7).

De quoi faut-il se munir ?

Maintenant que les présentations sont faites, il est temps de parler de comment utiliser ce fameux SDK. Premièrement, vous aurez besoin d’un Windows 7 (x86 ou x64) et d’un Visual Studio 2010 Express au minimum.
Au niveau des capacités de l’ordinateur, il est nécessaire de posséder au minimum un Dual Core à 2.66 GHz, une carte graphique compatible DirectX 9.0c et 2Go de RAM.
Et pour finir, vous aurez évidemment besoin de…un Kinect ! A savoir qu’il va vous falloir l’adaptateur qui permet depuis le connecteur Xbox (qui n’est pas un USB malgré les apparences) d’obtenir un câble pour brancher Kinect sur secteur et un USB. Si vous avez acheté Kinect seul, l’adaptateur est dans la boite ! Par contre si vous avez acheté le pack Xbox360 + Kinect, il est fort probable que vous ne l’ayez pas, il faudra alors vous le procurer sur la boutique en ligne de Microsoft.

Pour ce qui est des logiciels / drivers / SDK à installer, voici la liste avec les détails :

  • Le framework .NET 4.0 : La brique de base pour toute application, si vous avez fait déjà du .NET (ce qui est indispensable pour comprendre cette suite d’article), vous devriez savoir ce que c’est 🙂 .
  • Le SDK Kinect lui-même : c’est lui qui va vous apporter les drivers Kinect et les DLLs qui seront utilisées dans les projets. A noter que si vous avez au préalable utilisé les drivers non officiels, il faut les désinstaller pour éviter tout problème.
  • Microsoft Speech Plateforme Runtime x86 : Installe le runtime pour la reconnaissance vocale. Vous n’en avez besoin que dans le cas où vous souhaiter faire de la reconnaissance vocale via Kinect (inutile pour faire du tracking de squelette uniquement). Attention tout de même à bien prendre la version x86 (même sur un ordinateur x64) car le SDK Kinect ne peut être utilisé QUE sur des applications x86.
  • Microsoft Speech Platform SDK : Installe le SDK de la reconnaissance vocale. Idem que précédemment, attention à bien récupérer la version x86
  • Kinect for Windows Runtime Language Pack : C’est un modèle utilisé par la reconnaissance vocale qui est optimisé pour Kinect. Malheureusement celui-ci n’est pour l’instant disponible qu’en anglais. Il sera tout de même possible de faire de la reconnaissance en français mais en utilisant quelque chose de non optimisé pour l’utilisation du Kinect en tant que micro.

Et voila ! Il ne vous reste plus qu’à installer tous ces éléments (en ayant le Kinect débranché) puis branché votre Kinect pour les activer.
Pour être sur que les drivers se sont bien installés, vous devriez voir dans le gestionnaire de périphériques un nœud Microsoft Kinect avec à l’intérieur 3 éléments :

  • Microsoft Kinect Audio Array Control
  • Microsoft Kinect Camera
  • Microsoft Kinect Device

Vous devriez également avoir dans la section « Contrôleurs audio, vidéo et jeu » un nœud appelé « Kinect USB Audio ».

Lorsque tout ceci est fait, vous êtes prêt à utiliser Kinect dans vos applications !
Rendez-vous dans la deuxième partie de cette série d’articles pour découvrir l’utilisation du SDK avec la récupération des informations de la caméra couleur.