Kinect SDK 1.0 - Hands-on! (fr)

27. April 2012 23:04 by Renaud in   //  Tags:   //   Comments (0)

Je vous propose de lire cette série de 5 articles consacrés à la Kinect pour Windows! En les parcourant vous apprendrez à récupérer et utiliser les informations des différents capteurs, pour vous en servir dans vos applications.

Sommaire

 1. Introduction à l'API
 2. Utilisation du ColorImageStream
 3. Tracker le squelette avec le SkeletonStream
 4. Kinect en profondeur avec le DepthStream
 5. Reconnaissance vocale

Le MIC proposera également un training d'une demi-journée basé sur le contenu de ces articles. Pour cette occasion, nous mettrons à votre disposition des Kinect pour Windows afin que vous puissiez tester vos applications!

L'inscription est gratuite et la formation aura lieu dans les locaux du MIC ! Deux dates sont disponibles: le 29 mai ou le 6 juin, de 13h à 17h.

 

 

Kinect SDK 1.0 - Hands-on!

27. April 2012 15:56 by Renaud in   //  Tags:   //   Comments (0)

I'm pleased to propose you a set of 5 blogposts introducing the Kinect for Windows programming. If you want to know how to use the Microsoft Kinect sensor to empower your applications, take five minutes to read those posts!

What's on the table?

 1. Introducing the Kinect SDK API
 2. Deal with the ColorImageStream
 3. Track users with the SkeletonStream
 4. Kinect in depth! (with the DepthStream)
 5. Speech recognition

The MIC also organizes an half-day training based on the content of those posts. Come and learn how to start programming for Kinect for Windows! Registration is free and the event will take place at the Microsoft Innovation Center in Mons. Two dates are available:  May 29 or June 6, from 1pm to 5pm. 

 

Kinect SDK 1.0 - 5 - Speech Recognition

22. April 2012 21:04 by Renaud in   //  Tags:   //   Comments (6)
 1. Introduction to the API
 2. Use the ColorImageStream
 3. Track the users with the SkeletonStream
 4. Kinect in depth!
 5. Speech recognition

Here is the last post of this serie talking about the Kinect SDK 1.0. In the previous ones, we saw how to display the ColorImageStream video, how to track the Skeleton, and how to produce a 3D video with the DepthImageStream.

In this final post, we will see how to add speech recognition capabilities to your application!

Think "user experience"

When you develop an application using Kinect, you should also try to think as if you were a user. In which way are you going to use your Kinect? Do you want the user to take a specific posture or to accomplish a gesture in order to trigger an action? Is it a repetitive task?

Keep in mind that gestures aren't easy to recognize with a lot of confidence because everybody do it in a different way. Think also that the range of gestures is limited! You can easily discern a right-hand wave from a left-hand wave. But some others gestures aren't that easy. For example, to distinguish a fist and an open hand, you need to process the image stream by yourself!

Also, how would you trigger several actions at the same time?

If at the end, your gestures are too complicated, difficult to execute, your users will quickly be weary of it! And this result in bad UX.

SDK 1.0 and speech recognition

By installing the SDK 1.0, you also installed the Microsoft Kinect Speech Recognition Language Pack (en-Us). For the moment, this pack is only available in English, but more languages have been announced with the release 1.5, such as French, Italian, Japanese and Spanish.

You have to know that the speech recognition doesn't require a Kinect! You could do it with any microphone.

Hands-on

As usual, here is a simple project that you can use to follow this lab:

[caption id="attachment_126" align="aligncenter" width="92" caption="WpfKinect - 5 - Speech Recognition"][/caption]

For this example, you can use any of you existing project. I choose to used a previous project that draws the Skeleton a the two tracked users.  For each tracked user, we draw spheres for the hands and for the head.

To that basic application, we will add speech recognition capabilities to change the shape used to draw the skeletons (circles or squares), and the color of each user.

The goal is to be able to command the application with a phrase such as "Use red squares for player one!" to see an update of the player one.

1/ Initialize the Recognizer

First of all, you'll have to add a reference to the assembly: Microsoft.Speech.dll. Be careful to use that one and not System.Speech.dll. The second one doesn't give you access to the installed Kinect recognizer.

The SDK documentation gives us an helper method to retrieve the Kinect recognizer. We need that method because you probably have more than one recognizer installed and that we want to use the Kinect one.

This method creates a function which takes a RecognizerInfo as parameter, and which returns a Boolean. The RecognizerInfo is a object describing a speech recognition engine installed on your machine. This object has a Dictionary property which contains information about the recognizer, such as the supported cultures and languages, the name, the version number, ...

This function will return true if value of Kinect in the AdditionInfo dictionary is "True", and if the culture is "en-US" (which is the only one available for now).

Then, we will use that function in a Linq request so that for each installed recognizer, we will check if it fits the criteria, and return the first one that matches.

        private static RecognizerInfo GetKinectRecognizer()
        {
            Func<RecognizerInfo, bool> matchingFunc = r =>
            {
                string value;
                r.AdditionalInfo.TryGetValue("Kinect", out value);
                return "True".Equals(value, StringComparison.InvariantCultureIgnoreCase)
                    && "en-US".Equals(r.Culture.Name, StringComparison.InvariantCultureIgnoreCase);
            };
            return SpeechRecognitionEngine.InstalledRecognizers().Where(matchingFunc).FirstOrDefault();
        }

Here, we add a method to instantiate the SpeechRecognizerEngine. If an error occurs, we display a message box (this code also comes from the SDK documentation).

        private void InitializeSpeechRecognition()
        {
            RecognizerInfo ri = GetKinectRecognizer();

            if (ri == null)
            {
                MessageBox.Show(
                    @"There was a problem initializing Speech Recognition.
Ensure you have the Microsoft Speech SDK installed.",
                    "Failed to load Speech SDK",
                    MessageBoxButton.OK,
                    MessageBoxImage.Error);
                return;
            }

            try
            {
                speechRecognizer = new SpeechRecognitionEngine(ri.Id);
            }
            catch
            {
                MessageBox.Show(
                    @"There was a problem initializing Speech Recognition.
Ensure you have the Microsoft Speech SDK installed and configured.",
                    "Failed to load Speech SDK",
                    MessageBoxButton.OK,
                    MessageBoxImage.Error);
            }
            if (speechRecognizer == null)
                return;

            // Ajouter la suite ici!
        }
2/ Organize your keywords

To make it clearer, we will map some words to a significant value:

        #region Phrase mapping

        private Dictionary<string, Shape> Shapes = new Dictionary<string, Shape>
        {
            { "Circle", Shape.Circle },
            { "Square", Shape.Square },
        };

        private Dictionary<string, SolidColorBrush> BgrColors = new Dictionary<string, SolidColorBrush>
        {
            { "Yellow", Brushes.Yellow },
            { "Blue", Brushes.Blue },
            { "Red", Brushes.Red },
        };

        private Dictionary<string, int> PlayerIndexes = new Dictionary<string, int>
        {
            { "One", 0 },
            { "Two", 1 },
        };

        #endregion

The Shape enumeration contains all the supported shapes in the application. If we want to add more shape possibilities, they will appear here:

    public enum Shape
    {
        Circle,
        Square
    }
3/ Build the grammar

At the end of the InitializeSpeechRecognition method, we will add some code to build the phrase that we expect the user to tell. We will build Choices object that allows to expect different possibilities at a given position in the phrase.

And finally, we build the phrase based on "static" values and Choices. For example, the sentence should always start by "Use", followed by any of the possible colors, any of the possible shapes, and so on...

            // Create choices containing values of the lists
            var shapes = new Choices();
            foreach (string value in Shapes.Keys)
                shapes.Add(value);

            var colors = new Choices();
            foreach (string value in BgrColors.Keys)
                colors.Add(value);

            var playerIndexes = new Choices();
            foreach (string value in PlayerIndexes.Keys)
                playerIndexes.Add(value);

            // Describes how the phraze should look like
            var gb = new GrammarBuilder();
            //Specify the culture to match the recognizer in case we are running in a different culture.                                 
            gb.Culture = ri.Culture;
            // It should start with "Use"
            gb.Append("Use");
            // And then we should say any of the colors value
            gb.Append(colors);
            // Then one of the two possible shapes
            gb.Append(shapes);
            // then again the words "for player"
            gb.Append("for player");
            // and finally the player that we want to update
            gb.Append(playerIndexes);

            // Create the actual Grammar instance, and then load it into the speech recognizer.
            var g = new Grammar(gb);

            speechRecognizer.LoadGrammar(g);

We can easily build complex grammar! The whole GrammarBuilder could have been added to another Choices object as an alternative to another GrammarBuilder even more complex!

4/ Start the recognition

Then, we can subscribe to the speech recognizer events:

            speechRecognizer.SpeechRecognized += speechRecognizer_SpeechRecognized;
            speechRecognizer.SpeechHypothesized += speechRecognizer_SpeechHypothesized;
            speechRecognizer.SpeechRecognitionRejected += speechRecognizer_SpeechRecognitionRejected;

Then we start the Kinect audio stream, set that stream as the input source of the speech recognizer, and finally start the recognition!

            var audioSource = this.Kinect.AudioSource;
            audioSource.BeamAngleMode = BeamAngleMode.Adaptive;
            var kinectStream = audioSource.Start();

            speechRecognizer.SetInputToAudioStream(
                    kinectStream, new SpeechAudioFormatInfo(EncodingFormat.Pcm, 16000, 16, 1, 32000, 2, null));
            speechRecognizer.RecognizeAsync(RecognizeMode.Multiple);

The RecognizeMode enumeration indicates whether you want the recognition to stop after a first recognition event (Single) or to continue until you stop it manually (Multiple).

5/ Process the results

Now the recognition has started, and we will start processing what the Kinect heard. The first two eventhandlers are fired respectively when a phrase is rejected (because the confidence is too low), and when a phrase is hypothesized which means that a phrase is recognized but is ambiguous because it matches different accepted results. In those case, we will just display the result text.

        void speechRecognizer_SpeechRecognitionRejected(object sender, SpeechRecognitionRejectedEventArgs e)
        {
            Console.WriteLine("Rejected: " + e.Result.Text);
        }

        void speechRecognizer_SpeechHypothesized(object sender, SpeechHypothesizedEventArgs e)
        {
            Console.WriteLine("Hypothesized: " + e.Result.Text);
        }

The event that we really need is the SpeechRecognized. It will give us a result with a Confidence property. The confidence indicates how likely the result is the right one compared to other possibilities. Those possibilities are stored in the Alternatives property, which contains a collection of RecognizedPhrase.

Th recognized phrase is stored as a string in the Text property or as a collection of RecognizedWordUnit in the Words property. Each one of those words has its on Confidence property.

In that last code sample, we will analyze the result and modify the settings of the corresponding user:

        void speechRecognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            // Confidence indicates the likelihood that a phrase is recognized correctly
            // compared to alternatives. 0.8 confidence doesn't mean 80% chance of being correct.
            if (e.Result.Confidence < 0.50)
            {
                Console.WriteLine("Rejected: " + e.Result.Text + ", " + e.Result.Confidence);
                if (e.Result.Alternates.Count > 0)
                {
                    // Print the alternatives
                    Console.WriteLine("Alternates available: " + e.Result.Alternates.Count);
                    foreach (RecognizedPhrase alternate in e.Result.Alternates)
                    {
                        Console.WriteLine("Alternate: " + alternate.Text + ", " + alternate.Confidence);
                    }
                }
                return;
            }
            Console.WriteLine("Recognized: " + e.Result.Text + ", " + e.Result.Confidence);

            var index = PlayerIndexes[e.Result.Words[5].Text];

            var playerConfig = playerConfigs[index];

            if (playerConfig != null)
            {
                playerConfig.Brush = BgrColors[e.Result.Words[1].Text];

                playerConfig.Shape = Shapes[e.Result.Words[2].Text];
            }
        }

Kinect SDK 1.0 - 5 - Reconnaissance vocale

22. April 2012 16:03 by Renaud in   //  Tags:   //   Comments (2)
 1. Introduction à l'API
 2. Utilisation du ColorImageStream
 3. Tracker le squelette avec le SkeletonStream
 4. Kinect en profondeur avec le DepthStream
 5. Reconnaissance vocale

Voici le dernier article de cette série consacrée au Kinect SDK 1.0. Dans les précédents articles on a vu comment afficher une vidéo avec le ColorImageStream, comment tracker les utilisateurs avec le SkeletonStream, comment faire une vidéo 3D avec le DepthImageStream.

Pour terminer et ajouter un petit plus à vos applications en termes de Natural User Interface, on va voir comment utiliser la reconnaissance vocale!

Pensez expérience utilisateur

Quand vous développez une application utilisant la Kinect, essayez de vous mettre à la place des utilisateurs. Quelle est l'utilisation que vous en faites? Est-ce que les utilisateurs devront prendre des postures spécifiques? Ou bien réaliser un certain mouvement pour déclencher une action? Est-ce que cette tâche sera répétitive?

Rappelez-vous que les gestures (mouvements) sont difficiles à détecter avec certitude dû à leur différentes interprétations par les utilisateurs. Rappelez-vous également que l'éventail de mouvement est limité! Vous pouvez facilement différencier un "salut" de la main droite, d'un de la main gauche. Une main en bas, ou en haut. Mais quid du reste? Une main fermée ou ouverte demande une analyse plus poussée des images!

Demandez-vous aussi, comment gérer le fait de devoir déclencher plusieurs actions simultanément?

Si vous deviez arriver au point de faire des mouvements trop complexes, ou difficiles à exécuter, les utilisateurs risqueraient vite d'être fatigués, ou lassés de votre application. Et dès lors, ils en auront une mauvaise expérience.

Dans ce contexte, l'utilisation de la reconnaissance vocale semble être une bonne idée!

SDK 1.0 et reconnaissance vocale

En installant le SDK 1.0, vous avez installé par la même occasion le Microsoft Kinect Speech Recognition Language Pack (en-Us). Et en effet pour le moment ce pack n'est disponible qu'en anglais. Toutefois, le français est annoncé dans la release 1.5 au côté d'autres langues telles que l'Italien, le Japonais, et l'Espagnol.

Même si le français n'est pas encore là, rien n'empêche de déjà y jeter un oeil! Ainsi vous serez prêt pour la suite.

De plus il faut savoir que la reconnaissance vocale n'est pas quelque chose de lié à la Kinect. Vous pourriez le faire avec n'importe quel autre micro!

Hands-on

Comme toujours, je vous propose de télécharger les sources directement pour suivre plus facilement:

WpfKinect - 5 - Speech Recognition

WpfKinect - 5 - Speech Recognition

Pour cet exemple, on va reprendre un projet dans lequel des sphères étaient dessinées pour représenter les joueurs: un cercle pour chaque main, et un plus grand pour la tête. On n'affiche que les deux joueurs trackés.

A ce programme de base, on va ajouter la possibilité de changer les formes affichées (des cercles ou des carrés), ainsi que la couleur des joueurs indépendamment l'un de l'autre.

Ainsi, le but c'est de pouvoir dire (en anglais bien sûr :) ) "Utiliser des carrés rouges pour le joueur 1" et de voir l'image se mettre à jour en conséquence.

1/ Initialiser le Recognizer

Pour commencer il va falloir ajouter une référence à la librairie Microsoft.Speech.dll. A ne pas confondre avec System.Speech.dll, qui ne vous donnera pas accès au recognizer de la Kinect. Les deux librairies sont très proches l'une de l'autre et sont destinées à ne faire qu'une prochainement.

La doc du SDK propose une méthode helper pour retrouver le recognizer de la Kinect. On a besoin de cette méthode parce que vous avez déjà d'autres SpeechRecognitionEngine, mais nous on veut celui du SDK Kinect.

Cette méthode, elle fait quoi: elle crée une fonction qui prends en paramètre un RecognizerInfo et qui retourne un booléen. Le RecognizerInfo c'est un objet qui va décrire un outil de reconnaissance vocale installé sur votre machine. Cet objet a notamment un attribut de type Dictionary qui contient tout un tas d'informations décrivant le recognizer comme par exemple les cultures et langues supportées, la version, le nom, ou tout autre propriété spécifique à ce recognizer.

Cette fonction va donc retourner true si la propriété Kinect du dictionnary AdditionalInfo contient true, et si la culture est "en-US" (la seule disponible pour le moment).

Et on va ensuite utiliser une requête Linq dans laquelle on va faire appel à cette fonction. Ainsi, on va récupérer la liste de tous les recognizers installés, et on prendra le premier parmi ceux répondant aux critères de la fonction.

        private static RecognizerInfo GetKinectRecognizer()
        {
            Func<RecognizerInfo, bool> matchingFunc = r =>
            {
                string value;
                r.AdditionalInfo.TryGetValue("Kinect", out value);
                return "True".Equals(value, StringComparison.InvariantCultureIgnoreCase)
                    && "en-US".Equals(r.Culture.Name, StringComparison.InvariantCultureIgnoreCase);
            };
            return SpeechRecognitionEngine.InstalledRecognizers().Where(matchingFunc).FirstOrDefault();
        }

On va ajouter une méthode pour instancier le SpeechRecognizerEngine. Si on ne trouve pas de RecognizerInfo, on affiche un message d'erreur. Pareil si on arrive pas à instancier un SpeechRecognitionEngine à partir du RecognizerInfo récupéré.

        private void InitializeSpeechRecognition()
        {
            RecognizerInfo ri = GetKinectRecognizer();

            if (ri == null)
            {
                MessageBox.Show(
                    @"There was a problem initializing Speech Recognition.
Ensure you have the Microsoft Speech SDK installed.",
                    "Failed to load Speech SDK",
                    MessageBoxButton.OK,
                    MessageBoxImage.Error);
                return;
            }

            try
            {
                speechRecognizer = new SpeechRecognitionEngine(ri.Id);
            }
            catch
            {
                MessageBox.Show(
                    @"There was a problem initializing Speech Recognition.
Ensure you have the Microsoft Speech SDK installed and configured.",
                    "Failed to load Speech SDK",
                    MessageBoxButton.OK,
                    MessageBoxImage.Error);
            }
            if (speechRecognizer == null)
                return;

            // Ajouter la suite ici!
        }
2/ Préparer les mots-clés

Pour nous aider dans la suite, on va associer un ensemble de termes sous forme de chaîne de caractère à des valeurs:

        #region Phrase mapping

        private Dictionary<string, Shape> Shapes = new Dictionary<string, Shape>
        {
            { "Circle", Shape.Circle },
            { "Square", Shape.Square },
        };

        private Dictionary<string, SolidColorBrush> BgrColors = new Dictionary<string, SolidColorBrush>
        {
            { "Yellow", Brushes.Yellow },
            { "Blue", Brushes.Blue },
            { "Red", Brushes.Red },
        };

        private Dictionary<string, int> PlayerIndexes = new Dictionary<string, int>
        {
            { "One", 0 },
            { "Two", 1 },
        };

        #endregion

Notez que l'énumération Shape a été créée pour l'occasion. Ce n'est pas nécessaire, mais ça permet d'y voir plus clair et d'ajouter facilement de nouvelles possibilités.

    public enum Shape
    {
        Circle,
        Square
    }
3/ Créer la sémantique

A la suite du code dans la méthode InitializeSpeechRecognition, on va construire la phrase telle qu'elle devrait être déclarée par l'utilisateur. On va par exemple créer des objets Choices, contenant l'ensemble des valeurs que l'on pourrait s'attendre à recevoir à une position dans la phrase.

A la fin, on construit concrètement la phrase à partir de valeurs fixes, et de choix: ainsi, la phrase commencera toujours par "Use" et pourra ensuite être suivie de n'importe laquelle des couleurs, ensuite de n'importe quelle forme parmi les possibilités données évidemment, etc...

            // Create choices containing values of the lists
            var shapes = new Choices();
            foreach (string value in Shapes.Keys)
                shapes.Add(value);

            var colors = new Choices();
            foreach (string value in BgrColors.Keys)
                colors.Add(value);

            var playerIndexes = new Choices();
            foreach (string value in PlayerIndexes.Keys)
                playerIndexes.Add(value);

            // Describes how the phraze should look like
            var gb = new GrammarBuilder();
            //Specify the culture to match the recognizer in case we are running in a different culture.                                 
            gb.Culture = ri.Culture;
            // It should start with "Use"
            gb.Append("Use");
            // And then we should say any of the colors value
            gb.Append(colors);
            // Then one of the two possible shapes
            gb.Append(shapes);
            // then again the words "for player"
            gb.Append("for player");
            // and finally the player that we want to update
            gb.Append(playerIndexes);

            // Create the actual Grammar instance, and then load it into the speech recognizer.
            var g = new Grammar(gb);

            speechRecognizer.LoadGrammar(g);

Au final, on peut construire des objets très complexes. L'ensemble de l'objet GrammarBuilder aurait pu être ajouté à un nouvelle objet Choices en face d'un second objet GrammarBuilder tout aussi complexe, ou d'un simple autre mot.

4/ Lancer la reconnaissance

Toujours à la suite du code précédent, on va s'abonner aux évènements de notre speech recognizer:

            speechRecognizer.SpeechRecognized += speechRecognizer_SpeechRecognized;
            speechRecognizer.SpeechHypothesized += speechRecognizer_SpeechHypothesized;
            speechRecognizer.SpeechRecognitionRejected += speechRecognizer_SpeechRecognitionRejected;

Et pour finir on va attribuer la source audio (celle de notre Kinect), démarrer cette source, et lancer effectivement la reconnaissance!

            var audioSource = this.Kinect.AudioSource;
            audioSource.BeamAngleMode = BeamAngleMode.Adaptive;
            var kinectStream = audioSource.Start();

            speechRecognizer.SetInputToAudioStream(
                    kinectStream, new SpeechAudioFormatInfo(EncodingFormat.Pcm, 16000, 16, 1, 32000, 2, null));
            speechRecognizer.RecognizeAsync(RecognizeMode.Multiple);

L'énumération RecognizeMode indique que la reconnaissance doit s'arrêter directement après un premier élément reconnu (Single), ou qu'elle continue jusqu'à ce qu'on l'arrête (Multiple).

5/ Traiter les résultats

Finalement, on va décortiquer ce qu'on pense avoir entendu. Ces deux premiers eventhandlers ne nous intéressent pas vraiment. Le premier, rejected, indique simplement qu'une phrase n'a pas été reconnue (dans le sens où elle ne match pas avec une phrase attendue) avec assez d'assurance. Le second, Hypothesized, indique qu'un mot ou groupe de mots a été reconnu et qu'il correspond à plusieurs phrases possibles.

        void speechRecognizer_SpeechRecognitionRejected(object sender, SpeechRecognitionRejectedEventArgs e)
        {
            Console.WriteLine("Rejected: " + e.Result.Text);
        }

        void speechRecognizer_SpeechHypothesized(object sender, SpeechHypothesizedEventArgs e)
        {
            Console.WriteLine("Hypothesized: " + e.Result.Text);
        }

L'event qui va nous intéresser est le SpeechRecognized. Il va nous donner un résultat, caractérisé par une propriété Confidence. Cette propriété indique combien ce résultat semble être la bon résultat comparés à d'autres. Les autres résultats possibles peuvent être consultés via la propriété Alternatives, qui est une collection de RecognizedPhrase.

La phrase reconnue en elle-même est stockée dans la propriété Text sous forme d'une chaîne de caractère ou bien dans la propriété Words, sous la forme d'une collection de RecognizedWordUnit. Chacun de ces mots possède également une propriété Confidence.

Dans l'exemple ci-dessous, on va analyser la phrase et modifier les settings d'un joueur en fonction de son contenu.

        void speechRecognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            // Confidence indicates the likelihood that a phrase is recognized correctly
            // compared to alternatives. 0.8 confidence doesn't mean 80% chance of being correct.
            if (e.Result.Confidence < 0.50)
            {
                Console.WriteLine("Rejected: " + e.Result.Text + ", " + e.Result.Confidence);
                if (e.Result.Alternates.Count > 0)
                {
                    // Print the alternatives
                    Console.WriteLine("Alternates available: " + e.Result.Alternates.Count);
                    foreach (RecognizedPhrase alternate in e.Result.Alternates)
                    {
                        Console.WriteLine("Alternate: " + alternate.Text + ", " + alternate.Confidence);
                    }
                }
                return;
            }
            Console.WriteLine("Recognized: " + e.Result.Text + ", " + e.Result.Confidence);

            var index = PlayerIndexes[e.Result.Words[5].Text];

            var playerConfig = playerConfigs[index];

            if (playerConfig != null)
            {
                playerConfig.Brush = BgrColors[e.Result.Words[1].Text];

                playerConfig.Shape = Shapes[e.Result.Words[2].Text];
            }
        }

Kinect SDK 1.0 – 4 – Kinect en profondeur avec le DepthStream

20. April 2012 16:08 by Renaud in   //  Tags:   //   Comments (0)
 1. Introduction à l'API
 2. Utilisation du ColorImageStream
 3. Tracker le squelette avec le SkeletonStream
 4. Kinect en profondeur avec le DepthStream
 5. Reconnaissance vocale

Ce nouvel article va encore une fois mettre en avant une particularité de la Kinect, qui est sa capacité à avoir une vue en 3 dimensions de l'espace.

Cela peut sembler anodin à l'heure où on est inondés de films 3D, mais la technologie utilisée par la Kinect n'est pas du tout la même que celle utilisée dans le cinéma: pour tourner un film en 3D, on utilise de la 3D stéréoscopique. Dans ce cas, ils n'utilisent non pas une mais deux caméras espacées pour reproduire la vision des yeux et obtenir simplement une image pour l'oeil droit et une image pour l'oeil gauche. C'est ensuite votre cerveau qui traite les informations des deux images et apporte une notion de distance.

La Kinect par contre va utiliser un émetteur et un récepteur d'infrarouges qui vont permettre de calculer la distance des points de l'environnement. La Kinect se suffit donc à elle même!

Dans cet article on va voir comment obtenir un DepthImageFrame de la Kinect, et comment l'utiliser pour créer une vidéo en 3 dimension! Au passage on va également avoir un exemple de polling (je vous en avais parlé dans le premier article).

Vous pouvez immédiatement télécharger les sources du projet ici:

Pour les exemples de code qui vont suivre, on va créer un projet XNA! Alors si vous n'êtes pas très familier avec ce framework, ce n'est pas bien grave: moi non plus!

Ce qu'il faut juste savoir ici, c'est que notre application n'est pas event-driven mais est exécutée dans une boucle. Cela veut dire qu'on ne va pas attendre qu'un évènement soit déclenché pour faire une action. Au lieu de ça, à chaque passage dans la boucle on va aller demander de nouvelles informations à la Kinect!

Scène 3D: principe du projet

Comme expliqué plus haut, la Kinect peut donner la profondeur de chaque pixel. Dès lors, il est tout à fait imaginable de représenter l'image vue par la Kinect dans un espace en 3 dimensions, et de changer virtuellement la position de la caméra! On pourrait alors en quelque sorte se déplacer au milieu d'une scène et la voir sous différents angles alors qu'elle n'est pourtant filmée que d'un seul endroit!

Initialisation de la Kinect

On va donc simplement récupérer la Kinect connectée, et activer les différents flux dans le constructeur de Game1 :

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            // Use the first Connected Kinect
            Kinect = KinectSensor.KinectSensors.FirstOrDefault(k => k.Status == KinectStatus.Connected);
            if (Kinect != null)
            {
                // Activate the Near mode
                Kinect.DepthStream.Range = DepthRange.Near;
                // Enable the DepthStream
                Kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);
                // ... and instantiate the needed arrays to store some data
                depthPixelData = new short[Kinect.DepthStream.FramePixelDataLength];
                colorCoordinates = new ColorImagePoint[depthPixelData.Length];
                vertexPositionColors = new VertexPositionColor[depthPixelData.Length * 2];

                // Enable the Color Stream
                Kinect.ColorStream.Enable(ColorImageFormat.RgbResolution1280x960Fps12);
                colorFramePixelData = new byte[Kinect.ColorStream.FramePixelDataLength];

                Kinect.Start();
            }
        }

Dans la méthode Update, on va demander explicitement des infos à la Kinect:

            if (Kinect != null)
            {
                // Ask for a DepthFrame
                using (DepthImageFrame depthFrame = Kinect.DepthStream.OpenNextFrame(0))
                {
                    if (depthFrame != null)
                    {
                        // Copy the data
                        depthFrame.CopyPixelDataTo(depthPixelData);

                        // And match each point of the depthframe to a position
                        // in the colorframe that we are about to receive
                        Kinect.MapDepthFrameToColorFrame(depthFrame.Format, depthPixelData, Kinect.ColorStream.Format, colorCoordinates);

                        using (ColorImageFrame colorFrame = Kinect.ColorStream.OpenNextFrame(0))
                        {
                            if (colorFrame != null)
                            {
                                // Copy the data
                                colorFrame.CopyPixelDataTo(colorFramePixelData);
                            }
                        }
                    }
                }
            }

Ici on va utiliser les méthodes OpenNextFrame directement sur les flux qui nous intéressent. La valeur passée en paramètre indique le temps à attendre avant un timeout. Dans ce cas-ci, si une frame n'est pas disponible immédiatement, on ne veut pas attendre.

Ensuite, dans la méthode Draw, on va mettre à jour les points à dessiner.

                // For each pixel index in the colorCoordinates array
                for (int i = 0; i < colorCoordinates.Length; i++)
                {
                    // If the coordinates are in the range of the colorstream frame size
                    if (colorCoordinates[i].X < Kinect.ColorStream.FrameWidth
                        && colorCoordinates[i].Y < Kinect.ColorStream.FrameHeight)
                    {
                        // calculate the X,Y coordinates of the pixel in the depthstream frame
                        var pixelY = (i) / Kinect.DepthStream.FrameHeight;
                        var pixelX = (i) % Kinect.DepthStream.FrameWidth;

                        // Find the corresponding value in the Skeleton referential
                        var skeletonPoint = Kinect.MapDepthToSkeletonPoint(Kinect.DepthStream.Format, pixelX, pixelY, depthPixelData[i]);

                        // Retrieve the first index of the fourth-bytes pixel in the ColorFrame.
                        var baseIndex = (colorCoordinates[i].Y * Kinect.ColorStream.FrameWidth + colorCoordinates[i].X) * 4;

                        // And finally we update the corresponding 
                        vertexPositionColors[i * 2].Color.R = colorFramePixelData[baseIndex + 2];
                        vertexPositionColors[i * 2].Color.G = colorFramePixelData[baseIndex + 1];
                        vertexPositionColors[i * 2].Color.B = colorFramePixelData[baseIndex + 0];
                        vertexPositionColors[i * 2].Position.X = skeletonPoint.X;
                        vertexPositionColors[i * 2].Position.Y = skeletonPoint.Y;
                        vertexPositionColors[i * 2].Position.Z = skeletonPoint.Z;

                        // Use another point right behind the first one
                        vertexPositionColors[i * 2 + 1] = vertexPositionColors[i * 2];
                        vertexPositionColors[i * 2 + 1].Position.Z = skeletonPoint.Z + 0.05f;
                    }
                }

Dans le code ci-dessus, pour chaque pixel que l'on veut imprimer, on récupère sa position réelle dans un référentiel en 3 dimensions grâce à la méthode MapDepthToSkeletonPoint.

Il faut savoir qu'avec XNA 4.0, il n'est pas possible de dessiner un point. Du coup on va dessiner une droite relativement petite à partir de deux points: un premier à la position exacte à laquelle il devrait être selon la Kinect, et un second 5 centimètres derrière.

                foreach (EffectPass effectPass in basicEffect.CurrentTechnique.Passes)
                {
                    effectPass.Apply();

                    graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                        PrimitiveType.LineList,
                        vertexPositionColors,
                        0,
                        vertexPositionColors.Length / 2
                    );
                }

Pour terminer, on va simplement dessiner une liste de lignes en se basant sur les points que l'on vient de définir juste avant!

Bouger la caméra

On va d'abord initialiser la caméra au lancement de l'application:

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            basicEffect = new BasicEffect(GraphicsDevice);

            // The position of the camera
            cameraPosition = new Vector3(0, 0, -1);
            // creates the view based on the camera position, the target position, and the up orientation.
            viewMatrix = Matrix.CreateLookAt(cameraPosition, new Vector3(0, 0, 2), Vector3.Up);
            basicEffect.View = viewMatrix;
            basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45f), GraphicsDevice.Viewport.AspectRatio, 1f, 1000f);
            basicEffect.VertexColorEnabled = true;
        }

Et pour faire en sorte de pouvoir bouger la position de la caméra au cours de l'application, on ajoute ceci dans la méthode Update:

           foreach (Keys key in Keyboard.GetState().GetPressedKeys())
            {
                if (key == Keys.D)
                {
                    cameraPosition.X -= 0.01f;
                }
                if (key == Keys.Q)
                {
                    cameraPosition.X += 0.01f;
                }
                if (key == Keys.Z)
                {
                    cameraPosition.Y += 0.01f;
                }
                if (key == Keys.S)
                {
                    cameraPosition.Y -= 0.01f;
                }
                if (key == Keys.E)
                {
                    cameraPosition.Z += 0.01f;
                }
                if (key == Keys.X)
                {
                    cameraPosition.Z -= 0.01f;
                }
            }
            viewMatrix = Matrix.CreateLookAt(cameraPosition, new Vector3(0, 0, 2), Vector3.Up);
            basicEffect.View = viewMatrix;

Ce code va modifier la position de la caméra si on appuie sur les touches indiquées. Par exemple, Z va faire monter la caméra sur l'axe Y, tandis que S va la faire descendre.

Voici une petite vidéo du rendu ;)

Kinect SDK 1.0 – 4 – Kinect in depth with the DepthStream

20. April 2012 12:04 by Renaud in   //  Tags:   //   Comments (15)
 1. Introduction to the API
 2. Use the ColorImageStream
 3. Track the users with the SkeletonStream
 4. Kinect in depth!
 5. Speech recognition

This article will show you how to use one of the notable properties of the Kinect, which is its ability to give you a 3-dimensional view of the space.

Maybe it seems trivial today, because you can just go to the cinema to watch a 3D movie, but the technology used by the Kinect isn't the same than for the cinema. For a 3D movie, you'll need two cameras, to capture two images from different places, just like your eyes do. Then your brain will process those images and you'll know that there is an idea of depth in there.

But the Kinect can't give you two images! However it can use its infrared sensor to calculate the depth of each pixel that it sees.

So with the small project below, we will see how to get the DepthImageFrame, and how to use it to produce a 3-dimensional video. At the same time, we will see how to use the polling instead of the event model as in the previous examples.

You can download the sources right here:

[caption id="attachment_126" align="aligncenter" width="132" caption="XnaKinect - 3D vision using DepthStream"][/caption]

For that project, we will use the XNA 4.0 Framework. If you're not familiar with it, it's not a problem: me neither ;)

What you need to know is that an XNA application is executed into a loop, so we don't have to add some eventhandlers here to receives the frame. We will ask the Kinect directly each time we go through the loop.

3D scene: the principle

So the idea as explained above is to produce a video in 3 dimensions based on the information received from the Kinect. Then, when the video will be rendered, we will be able to move the position of the camera virtually: it means that the Kinect will stay at the same place, but we will see the scene from a different point of view!

Intialize the Kinect sensor

Once again, we will simply take the first connected Kinect and activate the DepthStream and the ColorStream :

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            // Use the first Connected Kinect
            Kinect = KinectSensor.KinectSensors.FirstOrDefault(k => k.Status == KinectStatus.Connected);
            if (Kinect != null)
            {
                // Activate the Near mode
                Kinect.DepthStream.Range = DepthRange.Near;
                // Enable the DepthStream
                Kinect.DepthStream.Enable(DepthImageFormat.Resolution640x480Fps30);
                // ... and instantiate the needed arrays to store some data
                depthPixelData = new short[Kinect.DepthStream.FramePixelDataLength];
                colorCoordinates = new ColorImagePoint[depthPixelData.Length];
                vertexPositionColors = new VertexPositionColor[depthPixelData.Length * 2];

                // Enable the Color Stream
                Kinect.ColorStream.Enable(ColorImageFormat.RgbResolution1280x960Fps12);
                colorFramePixelData = new byte[Kinect.ColorStream.FramePixelDataLength];

                Kinect.Start();
            }
        }

In the Update method, we will retrieve the color and depth frames :

            if (Kinect != null)
            {
                // Ask for a DepthFrame
                using (DepthImageFrame depthFrame = Kinect.DepthStream.OpenNextFrame(0))
                {
                    if (depthFrame != null)
                    {
                        // Copy the data
                        depthFrame.CopyPixelDataTo(depthPixelData);

                        // And match each point of the depthframe to a position
                        // in the colorframe that we are about to receive
                        Kinect.MapDepthFrameToColorFrame(depthFrame.Format, depthPixelData, Kinect.ColorStream.Format, colorCoordinates);

                        using (ColorImageFrame colorFrame = Kinect.ColorStream.OpenNextFrame(0))
                        {
                            if (colorFrame != null)
                            {
                                // Copy the data
                                colorFrame.CopyPixelDataTo(colorFramePixelData);
                            }
                        }
                    }
                }
            }

Here, we are using the OpenNextFrame method directly on the streams to retrieve the frames. The value passed in parameter indicates how long in milliseconds you are willing to wait for the information. If the time is elapsed, you'll receive a null value.

Notice that after getting the depthFrame, we use the MapDepthFrameToColorFrame to have an array containing for each depth pixel the coordinates of the corresponding color pixel in a color frame with the given format!

Then, in the draw method, we will create the points we want to draw :

                // For each pixel index in the colorCoordinates array
                for (int i = 0; i < colorCoordinates.Length; i++)
                {
                    // If the coordinates are in the range of the colorstream frame size
                    if (colorCoordinates[i].X < Kinect.ColorStream.FrameWidth
                        && colorCoordinates[i].Y < Kinect.ColorStream.FrameHeight)
                    {
                        // calculate the X,Y coordinates of the pixel in the depthstream frame
                        var pixelY = (i) / Kinect.DepthStream.FrameHeight;
                        var pixelX = (i) % Kinect.DepthStream.FrameWidth;

                        // Find the corresponding value in the Skeleton referential
                        var skeletonPoint = Kinect.MapDepthToSkeletonPoint(Kinect.DepthStream.Format, pixelX, pixelY, depthPixelData[i]);

                        // Retrieve the first index of the fourth-bytes pixel in the ColorFrame.
                        var baseIndex = (colorCoordinates[i].Y * Kinect.ColorStream.FrameWidth + colorCoordinates[i].X) * 4;

                        // And finally we update the corresponding 
                        vertexPositionColors[i * 2].Color.R = colorFramePixelData[baseIndex + 2];
                        vertexPositionColors[i * 2].Color.G = colorFramePixelData[baseIndex + 1];
                        vertexPositionColors[i * 2].Color.B = colorFramePixelData[baseIndex + 0];
                        vertexPositionColors[i * 2].Position.X = skeletonPoint.X;
                        vertexPositionColors[i * 2].Position.Y = skeletonPoint.Y;
                        vertexPositionColors[i * 2].Position.Z = skeletonPoint.Z;

                        // Use another point right behind the first one
                        vertexPositionColors[i * 2 + 1] = vertexPositionColors[i * 2];
                        vertexPositionColors[i * 2 + 1].Position.Z = skeletonPoint.Z + 0.05f;
                    }
                }

In the above code, for each pixel that we want to draw, we will find its position in the Skeleton referential thanks to the MapDepthToSkeletonPoint method.

You have to know that in XNA 4.0, you can't draw a "point". So, what we are doing is to draw small lines, based on the actual pixel point, and a second point 5 cm behind the first one.

There are probably much more efficient ways to achieve what we are doing here, but I didn't find them. It's just for fun of course :)

And finally, draw a list of lines based on the points we just defined.

                foreach (EffectPass effectPass in basicEffect.CurrentTechnique.Passes)
                {
                    effectPass.Apply();

                    graphics.GraphicsDevice.DrawUserPrimitives<VertexPositionColor>(
                        PrimitiveType.LineList,
                        vertexPositionColors,
                        0,
                        vertexPositionColors.Length / 2
                    );
                }

Move the camera position

First, we initialize the position at the application startup:

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            basicEffect = new BasicEffect(GraphicsDevice);

            // The position of the camera
            cameraPosition = new Vector3(0, 0, -1);
            // creates the view based on the camera position, the target position, and the up orientation.
            viewMatrix = Matrix.CreateLookAt(cameraPosition, new Vector3(0, 0, 2), Vector3.Up);
            basicEffect.View = viewMatrix;
            basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45f), GraphicsDevice.Viewport.AspectRatio, 1f, 1000f);
            basicEffect.VertexColorEnabled = true;
        }

And to be able to move it, we add the following code in the Update method:

           foreach (Keys key in Keyboard.GetState().GetPressedKeys())
            {
                if (key == Keys.D)
                {
                    cameraPosition.X -= 0.01f;
                }
                if (key == Keys.Q)
                {
                    cameraPosition.X += 0.01f;
                }
                if (key == Keys.Z)
                {
                    cameraPosition.Y += 0.01f;
                }
                if (key == Keys.S)
                {
                    cameraPosition.Y -= 0.01f;
                }
                if (key == Keys.E)
                {
                    cameraPosition.Z += 0.01f;
                }
                if (key == Keys.X)
                {
                    cameraPosition.Z -= 0.01f;
                }
            }
            viewMatrix = Matrix.CreateLookAt(cameraPosition, new Vector3(0, 0, 2), Vector3.Up);
            basicEffect.View = viewMatrix;

Those lines will change the camera position if you press a key. For example, if you press Z, it will go up on the Y axis, and if you press S it will go down.  

Kinect SDK 1.0 - 2 - Use the ColorStream

19. April 2012 16:13 by Renaud in   //  Tags:   //   Comments (1)
 1. Introduction to the API
 2. Use the ColorImageStream
 3. Track the users with the SkeletonStream
 4. Kinect in depth!
 5. Speech recognition

In the first blog post talking about Kinect,  I introduced to you the Kinect SDK 1.0. For that second part, I'd like to talk about the ColorStream. That stream will allow us to get and display a video from the Kinect. At the end of this article, you will know how to realize a simple WPF application to:

  • Display the video from the Kinect camera
  • Allow the user to select the image format
  • Take a snapshot and save it on your hard drive
  • Transform the video image in real-time

If you want to try it, you can download this sample project :

What is the ColorStream?

This is the data stream that will allow us to retrieve what the Kinect sees. There are 3 main streams we can play with:

  • ColorStream: gives access to the color image.
  • DepthStream: gives information about the distance between the Kinect and a point in the space.
  • SkeletonStream: gives information about people standing in front of the Kinect sensor.

Basically, all those streams are used in the same way (because they all inherit from the same base classe: ImageStream). We will talk about the two others in the next blog posts, but for now, let's focus on the simplest one: the ColorStream.

Initialize the stream

To start a stream (and thus start receiving information), we have to activate it explictly. In the code below, we enable the ColorStream with a given ColorImageFormat:

        /// <summary>
        /// Opens the kinect.
        /// </summary>
        /// <param name="newKinect">The new kinect.</param>
        private void OpenKinect(KinectSensor newKinect)
        {
            // Enables the ColorStream with the default format
            newKinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
            // Listen to the ColorFrameReady event to know when data is available
            newKinect.ColorFrameReady += newKinect_ColorFrameReady;
            // Starts the kinect sensor
            newKinect.Start();
        }

Then we will have to process the data!

Data processing

We need some fields to store the useful information:

        /// <summary>
        /// The length of the pixels data
        /// </summary>
        private int pixelDataLength;

        /// <summary>
        /// this array will contain the data provided by the Kinect
        /// </summary>
        private byte[] pixelData;

        /// <summary>
        /// Indicates wether the selected format is supported or not
        /// </summary>
        private bool isFormatSupported;

        /// <summary>
        /// the zone to update in the writeableBitmap
        /// </summary>
        private Int32Rect int32Rect;

        /// <summary>
        /// The amounts of bytes for one row
        /// </summary>
        private int stride;

        /// <summary>
        /// Gets or sets the output image.
        /// </summary>
        /// <value>
        /// The output image.
        /// </value>
        public WriteableBitmap OutputImage { get; set; }

Then, we implement the ColorFrameReady event handler:

        /// <summary>
        /// Handles the ColorFrameReady event of the newKinect control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="Microsoft.Kinect.ColorImageFrameReadyEventArgs"/> instance containing the event data.</param>
        void newKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            // Add some code here
        }

First, we attempt to open a ColorImageFrame. It may happen that the frame is null, for example when it's open too much time after the event is raised, or when the Kinect is stopped.

            // Open the received frame
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                // Could be null if we opened it too late
                if (colorFrame == null)
                     return;

                // Add some code here
            }

If the frame isn't null, then we will compare the data size with the pixelDataLength private field (which should be 0 at the beginning).

If the sizes are different, it means that we are going through that method for the first time, so we will initialize some of the objects based on the ColorImageFormat used to enable the stream. We will create a byte array with the right size (to calculate the size, you can use some properties of the ColorImageFrame: the array should have a length equals to FrameWidth x FrameHeigth x FrameBytesPerPixel). Then we will also create a WriteableBitmap. This is just like a BitmapImage, except that you can update the pixels of the region you want at any time, and it will update the UI automatically (exactly what we need in this application). Moreover it keeps us from instantiating a new BitmapImage 30 times per second.

                    // Checks if the length has changed (means the format has changed)
                    if (pixelDataLength != colorFrame.PixelDataLength)
                    {
                        // creates a new buffer long enough to receives all the data of a frame
                        pixelData = new byte[colorFrame.PixelDataLength];
                        pixelDataLength = colorFrame.PixelDataLength;

                        // Use a WriteableBitmap because it's better to re-write some pixels
                        // of a WriteabeBitmap than creating a new BitmapImage for each new frame.
                        OutputImage = new WriteableBitmap(
                                             colorFrame.Width,
                                             colorFrame.Height,
                                             96, // Standard dpi
                                             96,
                             // All the formats provided by the kinect are 4-bytes per pixel 
                             // (except the RawYuv which is not supported)
                                             PixelFormats.Bgr32,
                                             null);

                        // The rectangle that we will update in the WriteableBitmap 
                        // (has same dimensions as OutputImage)
                        int32Rect = new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height);

                        // how many bytes we need to represent one pixel
                        stride = colorFrame.Width * colorFrame.BytesPerPixel;

                        // Set the new WriteableBitmap as the KinectImage source
                        KinectImage.Source = OutputImage;
                    }

KinectImage is an Image control that you have to define in the XAML part of the MainWindow class:

 <Image x:Name="KinectImage" Width="640" Height="480" />

Update the image

So now we have a WriteableBitmap ready to receive our data, and we just have to update it each time we receive a new frame.

The CopyPixelDataTo method will copy the content of the frame to the given byte array. Then we have to update the OutputImage:

                    // Copies the data from the fram to the pixelData array.
                    colorFrame.CopyPixelDataTo(pixelData);

                    // Update the writeable bitmap.
                    OutputImage.WritePixels(
                        // Zone that will be updated
                        int32Rect,
                        // new data
                        pixelData,
                        // stride = number of pixels for one line of the image
                        stride,
                        // starting index = 0
                        0);

You can run the application now, and see the result! :)

Change the format !

At any time, you can see the current ColorImageFormat using the property Format of the ColorStream. That value gives two informations:

  • How is represented each pixel
  • The amount of frames per second
We need to add a combobox to the UI.
            <ComboBox Width="200" x:Name="FormatComboBox" SelectionChanged="FormatComboBox_SelectionChanged" />

Then in the constructor of MainWindows, we initialize the combobox values :

            // Fill in the Format Combobox with the available formats
            IList<ColorImageFormat> colorImageFormat = new List<ColorImageFormat> 
                                                        { ColorImageFormat.RgbResolution640x480Fps30,
                                                        ColorImageFormat.RgbResolution1280x960Fps12,
                                                        ColorImageFormat.RawYuvResolution640x480Fps15,
                                                        ColorImageFormat.YuvResolution640x480Fps15
                                                        };

            // Populate the combobox with the new List
            FormatComboBox.ItemsSource = colorImageFormat;

And finally, we implement the FormatComboBox_SelectionChanged eventhandler. It's not possible to modify directly the Format property because it is read-only. To change the format, you do have to pass it as a parameter in the Enable(...) method.

        private void FormatComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count > 0
                && Kinect != null
                && Kinect.Status == KinectStatus.Connected)
            {
                // Disabled the current stream
                Kinect.ColorStream.Disable();
                // Retrieves the new format
                var newFormat = (ColorImageFormat)e.AddedItems[0];

                // Enable the stream with the new format
                Kinect.ColorStream.Enable(newFormat);
            }
        }

When the stream is re-enabled, the ColorFrameReady fires again, and the image is updated.

Take a picture!

A nice feature to have in your Kinect application is a the possibility to take a picture!

There are different ways to achieve this, and I'm going to show you two possibilities: an easy one, and a very easy one!

First, let's add a new button to start the snapshot task:

 <Button Content="Take a picture!" x:Name="PictureTask" Click="PictureTask_Click" />

Then, implement the button click event handler. We open a new SaveDialog to ask for where to save the picture. If a file already exists at the given path, then it will be overwritten.

        /// <summary>
        /// Handles the Click event of the PictureTask button.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void PictureTask_Click(object sender, RoutedEventArgs e)
        {
            string fileName = null;
            SaveFileDialog saveDialog = new SaveFileDialog();
            if (saveDialog.ShowDialog().Value)
            {
                fileName = saveDialog.FileName;
            }

            if (string.IsNullOrWhiteSpace(fileName))
                return;

            if (File.Exists(fileName))
            {
                File.Delete(fileName);
            }

            // Add some logic to take a picture here..

        }

Now you need to add the logic to take a picture and save it to your hard drive. Here are the two solutions that I propose you:

The easy way
            using (FileStream savedSnapshot = new FileStream(fileName, FileMode.CreateNew))
            {
                BitmapSource image = (BitmapSource)KinectImage.Source;
                JpegBitmapEncoder jpgEncoder = new JpegBitmapEncoder();
                jpgEncoder.QualityLevel = 70;
                jpgEncoder.Frames.Add(BitmapFrame.Create(image));
                jpgEncoder.Save(savedSnapshot);
                savedSnapshot.Flush();
                savedSnapshot.Close();
                savedSnapshot.Dispose();
            }
The even easier way (Coding4Fun Toolkit)

Add to your project a reference to the Coding4Fun's Kinect Toolkit:

[caption id="attachment_1709" align="aligncenter" width="635" caption="Add a reference to the toolkit using NuGet"][/caption]

Add a new using instruction to be able to use the extension methods:

using Coding4Fun.Kinect.Wpf;

And finally, you just need those two lines:

            // Coding4Fun helper's method:
            BitmapSource image = (BitmapSource)KinectImage.Source;
            image.Save(fileName, ImageFormat.Jpeg);

The Kinect Toolkit allows you to call a Save method on a BitmapSource and passing two parameters: the file path, and the outpout format.

Transform the output image

The final part of this article will talk about how you can manipulate the color image frame before you display it. As you know, the color frame is nothing more than a bunch of bytes stored in an array called pixelData.

Let's take, say, the ColorImageFormat.RgbResolution640x480Fps30:

pixelData contains 640 x 480 x 4 = 1228800 bytes. It means 4 bytes for each pixel in the  color frame. The first three bytes correspond to the colors Blue, Green, Red. The fourth one isn't used but could store the alpha (transparency) channel in a Bgra format.

If we simply try to invert the bytes values before we update the WriteableBitmap, it will result in a "negative" effect!

                    for (int i = 0; i < pixelData.Length - 3; i +=4)
                    {
                        // Transformation 1:
                        // ======================================= //
                        // ( ~ ) inverts the bits 
                        pixelData[i] = (byte)~pixelData[i];
                        pixelData[i + 1] = (byte)~pixelData[i + 1];
                        pixelData[i + 2] = (byte)~pixelData[i + 2];
                    }
The tild  (~) operator is used to invert each bit in a numeric value.

You can now easily imagine a lot of transformations. You could set the Blue and Green values to 0 to display a Red image. To create a gray scale, each byte (B, G, and R) must have the same value.

Thank you for reading! In the next post, we will talk about the SkeletonStream!

 

Kinect SDK 1.0 - 3 - Track bodies with the SkeletonStream

19. April 2012 16:10 by Renaud in   //  Tags:   //   Comments (0)
 1. Introduction to the API
 2. Use the ColorImageStream
 3. Track the users with the SkeletonStream
 4. Kinect in depth!
 5. Speech recognition

In the previous posts, we saw how to connect and use your Kinect within your app, and how to use the ColorStream. That was just enough to use your Kinect as a webcam, but not much more.

In this post, we are going to empower your application with the Skeleton tracking provided by the Kinect !

What is the SkeletonStream ?

This is a data stream provided by the Kinect which allows you to know the position of a user in front of the Kinect, and even to know the positions of up to 20 Joints on the user's body.

 

The following figure shows you the Joints (yellow points) tracked by the Kinect in the 3-dimensional referential.

The skeleton of a user and all the tracked points are stored in an object called Skeleton (MSDN).

Skeleton in details

The Skeleton doesn't expose a lot of properties, but they are very useful :

  • TrackingState : The status tells you whether the Skeleton is tracked (Tracked) or not (NotTracked), or if we only have the global position of the user (PositionOnly). The Kinect is able to follow up to 6 users at the same time: 2 in Tracked mode, and 4 in PositionOnly mode. The Tracked mode gives you access to the full skeleton and the 20 Joints.
  • Joints : A Joint collection representing the body as shown on the above image (only available if Tracked).
  • TrackingId :  Identifies a Skeleton, so that you can track a particular user over the time.
  • Position : The global position of a user (available if Tracked or PositionOnly).
  • ClippedEdges : Indicates whether one part of the user's body is clipped by the edges (not in the field of view).

It's important to know that all the positions are represented by a SkeletonPosition object which has 3 properties: X, Y, Z. The distances are expressed in meters and the referential is as shown in the above schema: X goes from left to right, Y goes from up to down and Z is the depth. So basically, the Kinect is the point (0, 0, 0).

[caption id="attachment_126" align="aligncenter" width="92" caption="WpfKinect 3 - SkeletonStream"][/caption]

Track and draw the skeleton !

What we do want now is to use that SkeletonStream. We will first activate it, as we did for the ColorStream in the previous post :

/// <summary>
/// This array will store the Skeletons
/// received with each SkeletonFrame
/// </summary>
private Skeleton[] skeletonData = new Skeleton[6];

/// <summary>
/// Starts the Kinect sensor.
/// </summary>
/// <param name="newKinect">Le nouveau capteur.</param>
private void OpenKinect(KinectSensor newKinect)
{
    Kinect = newKinect;
    // Activate the SkeletonStream with default smoothing.
    newKinect.SkeletonStream.Enable();
    // Subscribe to the SkeletonFrameReady event to know when data is available
    newKinect.SkeletonFrameReady += Kinect_SkeletonFrameReady;
    // Starts the sensor
    newKinect.Start();
}

Then you implement the eventhandler :

        void Kinect_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            // Opens the received SkeletonFrame
            using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame())
            {
                // Skeleton frame might be null if we are too late or if the Kinect has been stopped
                if (skeletonFrame == null)
                    return;

                // Copies the data in a Skeleton array (6 items) 
                skeletonFrame.CopySkeletonDataTo(skeletonData);

                // TODO : Do something with the skeletons

            }
        }

Now you can start processing the information using the received Skeletons. We will start by drawing the skeletons of the two tracked users (if any).

To make it properly, I created a SkeletonDrawing helper class, which will allow me to draw a skeleton in a canvas based on the Joints. You can see the source code here: SkeletonDrawing.cs

After the todo in the Kinect_SkeletonFrameReady eventhandler, we will add a a few lines of code to retrieve the two tracked skeletons and update the corresponding SkeletonDrawing object.

First, add a dictionary to keep track of the drawings:

        // The Skeletons drawing on the scene
        private Dictionary<int, SkeletonDrawing> drawnSkeletons;

Then, add the logic to look for the tracked skeletons and update them:

                // Retrieves Skeleton objects with Tracked state
                var trackedSkeletons = skeletonData.Where(s => s.TrackingState == SkeletonTrackingState.Tracked);

                // By default, assume all the drawn skeletons are inactive
                foreach (SkeletonDrawing skeleton in drawnSkeletons.Values)
                    skeleton.Status = ActivityState.Inactive;

                foreach (Skeleton trackedSkeleton in trackedSkeletons)
                {
                    SkeletonDrawing skeletonDrawing;
                    // Checks if the tracked skeleton is already drawn.
                    if (!drawnSkeletons.TryGetValue(trackedSkeleton.TrackingId, out skeletonDrawing))
                    {
                        // If not, create a new drawing on our canvas
                        skeletonDrawing = new SkeletonDrawing(this.SkeletonCanvas);
                        drawnSkeletons.Add(trackedSkeleton.TrackingId, skeletonDrawing);
                    }

                    // Update the drawing
                    Update(trackedSkeleton, skeletonDrawing);
                    skeletonDrawing.Status = ActivityState.Active;
                }

                foreach (SkeletonDrawing skeleton in drawnSkeletons.Values)
                {
                    // Erase all the still inactive drawings. It means they are not tracked anymore.
                    if (skeleton.Status == ActivityState.Inactive)
                        skeleton.Erase();
                }

Then, the update method, which is going to make some transformations on the SkeletonPoint positions and update the drawn skeleton :

        /// <summary>
        /// Updates the specified drawn skeleton with the new positions
        /// </summary>
        /// <param name="skeleton">The skeleton source.</param>
        /// <param name="drawing">The target drawing.</param>
        private void Update(Skeleton skeleton, SkeletonDrawing drawing)
        {
            foreach (Joint joint in skeleton.Joints)
            {
                // Transforms a SkeletonPoint to a ColorImagePoint
                var colorPoint = Kinect.MapSkeletonPointToColor(joint.Position, Kinect.ColorStream.Format);
                // Scale the ColorImagePoint position to the current window size
                var point = new Point((int)colorPoint.X / 640.0 * this.ActualWidth, (int)colorPoint.Y / 480.0 * this.ActualHeight);
                // update the position of that joint
                drawing.Update(joint.JointType, point);
            }
        }

Notice the MapSkeletonPointToColor method!  The KinectSensor class provides some helper methods to map a point from a stream to another.

Here are all the available methods:

The reason why those methods exist is simple:

The SkeletonStream gives information in a different referential than the ColorStream and the DepthStream. For the last two, you have a 2D position for each pixel of the frame, with the top-left corner as the origin (0,0).

The SkeletonPoints are placed in a 3D referential. Now if you want to display the ColorStream, and overlay with the Skeleton, you'll have to convert the skeleton positions to make sure they fit with the ColorStream image. The MapSkeletonPointToColor does the job for you and give you a ColorImagePoint based on a SkeletonPoint!

The case of the depth and color points is a little bit different. The depth sensor is not at the exact same place than the color sensor. It means that there is a small gap between the two frames (we will see it in the next post!).

Until now, we didn't use the Z value but we will see how we can add it to scale the size of the shapes, and show the proximity.

Postures and Gestures

Let's talk about the gestures. There is nothing in the SDK to easily detect gestures, and you'll have to implement everything by yourself! There are some projects out there to help you achieve that job, but it's still complicated.

If you plan to use postures or gestures in your application, you should definitely check the KinectToolbox project on codeplex:

http://kinecttoolbox.codeplex.com/

Kinect SDK 1.0 - 3 - Tracker les mouvements avec le SkeletonStream

19. April 2012 13:04 by Renaud in   //  Tags:   //   Comments (1)
 1. Introduction à l'API
 2. Utilisation du ColorImageStream
 3. Tracker le squelette avec le SkeletonStream
 4. Kinect en profondeur avec le DepthStream
 5. Reconnaissance vocale

Dans les articles précédents, on a vu comment se connecter à une Kinect, et comment se servir du ColorStream. Globalement, avec ça, on n'a pas vraiment utilisé la Kinect différemment d'une bête webcam...

Mais cette fois-ci on entre dans le vif du sujet! On va parler du SkeletonStream...

Le SkeletonStream, c'est quoi?

C'est un flux de données que renvoie la Kinect et qui vous permet de connaître la position d'un utilisateur face à la Kinect, mais pas seulement: on peut obtenir jusqu'à 20 points (Joint) du corps, positionnés dans un espace à 3 dimensions!

L'image suivante vous montre l'ensemble des 20 points qui peuvent être trackés!

Le squelette d'un utilisateur et l'ensemble de ses points sont représentés par un objet de type Skeleton (MSDN).

Le Skeleton en détails

Le Skeleton ne présente pas beaucoup de propriétés mais elles sont très utiles :

  • TrackingState : Le statut vous indique si le Skeleton est tracké (Tracked) ou non (NotTracked), ou si on a que la position globale de l'utilisateur (PositionOnly). La Kinect peut suivre jusqu'à 6 joueurs maximum dont 2 en mode Tracked et 4 en mode PositionOnly. Le mode Tracked permet d'avoir accès au squelette complet (20 Joint).
  • Joints : Une collection de Joint représentant les 20 points du corps humain comme montré sur l'image ci-dessus. (Tracked)
  • TrackingId :  Identifie un Skeleton sur la durée.
  • Position : La position centrale d'un squelette (Tracked et PositionOnly).
  • ClippedEdges : Indique si des parties du corps de l'utilisateur se trouver en dehors du champs de vision.

Il est important de noter que pour le Skeleton et chacun des Joint, les positions sont représentées par un objet de types SkeletonPosition qui a 3 propriétés X, Y et Z. Les distances sont exprimées en mètres, et le référentiel est comme indiqué sur le schéma ci-dessus: X va de la gauche vers la droite, Y du bas vers le haut et Z représente la profondeur. Concrètement, la Kinect est le point (0,0,0).

[caption id="attachment_126" align="aligncenter" width="92" caption="WpfKinect 3 - SkeletonStream"][/caption]

Tracker et dessiner le squelette!

Ce qu'on veut désormais, c'est utiliser le SkeletonStream. On va commencer par l'activer, de la même manière qu'on activait le ColorStream dans les articles précédents :

/// <summary>
/// Ce tableau va contenir les Skeleton
/// reçus avec chaque nouvelle SkeletonFrame
/// </summary>
private Skeleton[] skeletonData = new Skeleton[6];

/// <summary>
/// Démarrer le capteur Kinect.
/// </summary>
/// <param name="newKinect">Le nouveau capteur.</param>
private void OpenKinect(KinectSensor newKinect)
{
    Kinect = newKinect;
    // Active le SkeletonStream avec les paramètres par défaut.
    newKinect.SkeletonStream.Enable();
    // On s'abonne au SkeletonFrameReady pour savoir quand des données sont disponibles
    newKinect.SkeletonFrameReady += Kinect_SkeletonFrameReady;
    // Démarre le capteur
    newKinect.Start();
}

Ensuite on implémente l'eventhandler qui va bien:

        void Kinect_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
        {
            // On ouvre la SkeletonFrame reçue
            using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame())
            {
                // Il se peut que la frame soit null, si on arrive 
                // trop tard ou si la Kinect a été stoppée.
                if (skeletonFrame == null)
                    return;

                // On copie les infos dans un tableau de Skeleton (6 éléments)
                skeletonFrame.CopySkeletonDataTo(skeletonData);

                // TODO : Faire quelque chose avec les Skeletons

            }
        }

Maintenant on peut commencer à faire des traitements en utilisant les infos reçues! On va commencer par dessiner les squelettes des deux joueurs qui seront entièrement trackés (s'il y en a). Pour faire ça proprement, j'ai créé une classe SkeletonDrawing, qui va me permettre de dessiner un squelette à partir de tous les Joints. Le code source est disponible dans le projet! Dans le todo de l'eventhandler Kinect_SkeletonFrameReady, on va ajouter quelques lignes de codes pour récupérer les squelettes trackés et mettre à jour les SkeletonDrawing correspondants:

D'abord, on ajoute une variable d'instance pour garder une trace des squelettes dessinées:

        // Les squelettes dessinés sur la scène
        private Dictionary<int, SkeletonDrawing> drawnSkeletons;

Ensuite on peut ajouter la logique pour chercher les squelettes trackés, et les représenter ou les mettre à jour sur l'écran:

// Récupère les Skeleton avec le statut Tracked
var trackedSkeletons = skeletonData.Where(s => s.TrackingState == SkeletonTrackingState.Tracked);

// Par défaut, on indique que tous les squelettes sont inactifs
foreach (SkeletonDrawing skeleton in drawnSkeletons.Values)
    skeleton.Status = ActivityState.Inactive;

// On parcours chaque item de la liste des Skeleton trackés (2 max.)
foreach (Skeleton trackedSkeleton in trackedSkeletons)
{
    SkeletonDrawing skeletonDrawing;
    // Et on regarde s'il est déjà dessiné
    if (!drawnSkeletons.TryGetValue(trackedSkeleton.TrackingId, out skeletonDrawing))
    {
        // Si pas, on crée un nouveau dessin
        skeletonDrawing = new SkeletonDrawing(this.SkeletonCanvas);
        drawnSkeletons.Add(trackedSkeleton.TrackingId, skeletonDrawing);
    }

    // et on met à jour le dessin du squelette
    Update(trackedSkeleton, skeletonDrawing);
    skeletonDrawing.Status = ActivityState.Active;
}

foreach (SkeletonDrawing skeleton in drawnSkeletons.Values)
{
    // On peut ensuite effacer tous les dessins de squelettes avec un statut Inactive
    if (skeleton.Status == ActivityState.Inactive)
        skeleton.Erase();
}

Et la méthode Update qui va mettre à jour la position de chaque points du squelette dessiné:

private void Update(Skeleton skeleton, SkeletonDrawing drawing)
{
    // On parcourt la liste des Joint du Skeleton
    foreach (Joint joint in skeleton.Joints)
    {
        // On récupère la position correspondante dans un repère où le coin topleft est (0,0)
        var colorPoint = Kinect.MapSkeletonPointToColor(joint.Position, Kinect.ColorStream.Format);
        // On met les points à l'échelle compte tenu de la taille de la fenêtre
        var point = new Point((int)colorPoint.X / 640.0 * this.ActualWidth, 
                              (int)colorPoint.Y / 480.0 * this.ActualHeight);
        // On met à jour le Joint correspondant du dessin
        drawing.Update(joint.JointType, point);
    }
}

Notez l'utilisation de la méthode MapSkeletonPointToColor!  La classe KinectSensor expose quelques méthodes d'aide pour convertir la position d'un point d'une frame vers un point d'une autre frame.

Voici l'ensemble des méthodes disponibles :

Ces méthodes vont vous éviter d'attraper mal à la tête :)

Comme vous le savez, le SkeletonStream donne des informations dans un référentiel différent du ColorStream et du DepthStream. Pour les deux derniers, vous aurez des tableaux de pixels, et donc chaque pixel aura une position dans un espace à deux dimensions avec le point top-left ayant les coordonnées (0,0).

Les SkeletonPoints  quant à eux sont placés dans un référentiel à 3 axes. Maintenant, si vous voulez replacer le squelette dans le référentiel 2D du ColorStream, vous devrez convertir les positions pour être sûre que les points correspondent. C'est à ça que sert la méthode MapSkeletonPointToColor, qui vous retournera une ColorImagePoint basé sur un SkeletonPoint et le ColorImageFormat cible.

Le cas de la conversion depuis une DepthImageFrame vers une ColorImageFrame est un peu différent. Le capteur de profondeur n'est pas tout à fait à la même place que le capteur de couleur. Il y a donc une petite différence entre les positions (mais on en parlera dans le prochain article ) !

Jusqu'à maintenant on a pas utilisé la valeur de Z, mais si vous téléchargez les sources vous verrez que l'on peut également l'utiliser pour modifier l'échelle des formes dessinées et ainsi donner une impression de perspective !

Postures et Gestures

Les gestures, pour ceux qui ne savent pas, c'est un mouvement identifiable que l'utilisateur fait et qui permet de déclencher une action. Si vous êtes familier avec le développement Windows Phone, vous avez certainement déjà entendu parler du Flick, du Pinch, etc...  Ces mouvements sont courants sur des devices prévus pour le touch.

Si vous pensez à la Kinect, vous pensez naturellement à un mouvement de balaiement avec la main, par exemple pour faire défiler une liste horizontale. Ce ce qu'on appelle le Swipe.

J'ai été surpris en découvrant le SDK de voir que rien n'était prévu pour la détection de gestures. Ce n'est pas une tâche facile, parce que contrairement à un mouvement fait avec les doigts sur  un écran tactile, il y a plein de façons différentes de faire le même mouvement, et il faut pouvoir différencier un mouvement fait volontairement d'un simple déplacement de l'utilisateur devant la Kinect. Il y a heureusement quelques projets que vous pouvez trouver sur le web que vous pouvez réutiliser, comme l'excellent Kinect Toolbox.

Kinect SDK 1.0 - 2 - Utilisation du ColorStream

19. April 2012 10:04 by Renaud in   //  Tags:   //   Comments (0)
 1. Introduction à l'API
 2. Utilisation du ColorImageStream
 3. Tracker le squelette avec le SkeletonStream
 4. Kinect en profondeur avec le DepthStream
 5. Reconnaissance vocale

Dans la première partie,  je vous présentais le Kinect SDK 1.0.

Pour cette deuxième partie, on va se concentrer sur l'utilisation du ColorStream. Le ColorStream, c'est ce qui va vous permettre d'accéder au flux vidéo de la Kinect. A la fin de cette article, vous saurez comment réaliser une application très simple pour pouvoir:

  • Afficher l'image de la caméra
  • Sélectionner le type de format d'image souhaité
  • Prendre une photo et la sauvegarder
  • Modifier le rendu de l'image en temps réel

Pour suivre cet article je vous propose de télécharger le projet d'example:

Qu'est-ce que le ColorStream?

C'est le flux de données qui va nous permettre de récupérer l'image que voit la Kinect. Il y a 3 flux principaux avec lesquels on peut jouer:

  • ColorStream: accès à l'image, comme une webcam.
  • DepthStream: accès à des informations sur la distance entre la Kinect et un point
  • SkeletonStream: accès à des informations sur les personnes debout devant le capteur

Globalement, tous ces flux s'utilisent de la même façon (puisqu'ils héritent tous de la même classe de base qui est ImageStream). On parlera des deux autres dans les prochains articles, mais pour le moment concentrons-nous sur le plus simple!

Initialisation du flux

Pour démarrer un flux (et donc commencer à en tirer des informations), il va falloir l'activer explicitement:

        /// <summary>
        /// Démarrer le capteur Kinect.
        /// </summary>
        /// <param name="newKinect">The new kinect.</param>
        private void OpenKinect(KinectSensor newKinect)
        {
            // Active le ColorStream avec un format donné (optionnel)
            newKinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
            // On s'abonne au ColorFrameReady pour savoir quand des données sont disponibles
            newKinect.ColorFrameReady += newKinect_ColorFrameReady;
            // Démarre le capteur
            newKinect.Start();
        }

Ensuite, il va falloir traiter ses données !

Traitement de l'info

On va avoir besoin de quelques variables d'instance pour stocker les infos utiles:

        /// <summary>
        /// La longueur du tableau de données.
        /// </summary>
        private int pixelDataLength;

        /// <summary>
        /// Ce tableau contiendra les données de l'image retournée par la Kinect.
        /// </summary>
        private byte[] pixelData;

        /// <summary>
        /// Zone que l'on mettra à jour dans le writeableBitmap
        /// </summary>
        private Int32Rect int32Rect;

        /// <summary>
        /// le nombre d'octets pour une ligne de pixels dans le writeableBitmap
        /// </summary>
        private int stride;

        /// <summary>
        /// Gets or sets the output image.
        /// </summary>
        /// <value>
        /// L'image de sortie que l'on va afficher.
        /// </value>
        public WriteableBitmap OutputImage { get; set; };

Ensuite on implémente l'event handler pour ColorFrameReady:

        /// <summary>
        /// Gère l'event ColorFrameReady du contrôle newKinect.
        /// </summary>
        /// <param name="sender">La source de l'event.</param>
        /// <param name="e">L'instance de <see cref="Microsoft.Kinect.ColorImageFrameReadyEventArgs"/> contenant les données de l’évènement.</param>
        void newKinect_ColorFrameReady(object sender, ColorImageFrameReadyEventArgs e)
        {
            // On va ajouter du code ici !
        }

D'abord, on tente d'ouvrir une ColorImageFrame. Il se peut parfois que la frame soit null, par exemple dans le cas où on ouvrirait la frame trop longtemps après que l'event ait été déclenché. Ou lors d'un event déclenché juste au moment où l'on stoppe le capteur.

            // Ouvrir la frame reçue
            using (ColorImageFrame colorFrame = e.OpenColorImageFrame())
            {
                // si colorFrame est null, on n'a pas de donnée.
                if (colorFrame == null)
                    return;
            }

Si ce n'est pas le cas, on va comparer la taille du résultat obtenu, avec la taille que l'on connait (et qui est à 0 par défaut).

Si les tailles sont différentes, c'est que l'on passe dans cette méthode pour la première fois, on va donc initialiser les objets nécessaires. On va instancier un tableau contenant les données des pixels, et un WriteableBitmap. Ces objets ne vont pas changer entre chaque frame, ils vont garder les mêmes dimensions. C'est pour cela qu'on utilise des variables d'instances plutôt que de les créer inutilement à chaque nouvelle frame.

                    // Vérifie si la taille a changé (signifie que l'on passe
                    // pour la première fois ou que le format a changé)
                    if (pixelDataLength != colorFrame.PixelDataLength)
                    {
                        // On créé un nouveau tableau assez long pour
                        // pour contenir toutes les infos de la frame
                        pixelData = new byte[colorFrame.PixelDataLength];
                        // On stocke la nouvelle taille
                        pixelDataLength = colorFrame.PixelDataLength;

                        // On utilise un WriteableBitmap plutôt que de créer un nouveau
                        // BitmapImage pour chaque frame, ce qui serait moins efficient
                        // (à cause de la création d'un nouvel objet 30 fois par seconde )
                        OutputImage = new WriteableBitmap(
                                             // dimensions de l'image
                                             colorFrame.Width,
                                             colorFrame.Height,
                                             // Nombre de points par pouce
                                             96, // Valeur par défaut
                                             96, 
                                             // Le format attendu
                                             PixelFormats.Bgr32,
                                             null);

                        // La zone qui sera éditable dans le WriteableBitmap que l'on vient de créer.
                        int32Rect = new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height);

                        // Nombre d'octets pour une ligne de l'image
                        stride = colorFrame.Width * colorFrame.BytesPerPixel;

                        // On affecte le WriteableBitmap comme source au contrôle Image
                        KinectImage.Source = OutputImage;
                    }

KinectImage est un contrôle qui doit être définit dans le XAML.

 <Image x:Name="KinectImage" Width="640" Height="480" />

Ces opérations ne sont donc effectuées qu'une fois, lors du premier déclenchement de l'event.

Mise à jour de l'image

Ensuite, il faut mettre à jour l'image affichée avec les infos reçues à chaque nouvelle frame.

La méthode CopyPixelDataTo va simplement copier le contenu de la frame dans le tableau de bytes donné en paramètre. Il ne reste plus qu'à mettre à jour le WriteableBitmap qui sert de source au contrôle Image définit dans le XAML.

                    // Copie les données des pixels de la frame dans le tableau pixelData.
                    colorFrame.CopyPixelDataTo(pixelData);

                    // Met à jour le WriteableBitmap.
                    OutputImage.WritePixels(
                        // La zone qui va être mise à jour (ici, la totalité)
                        new Int32Rect(0, 0, colorFrame.Width, colorFrame.Height),
                        // nouvelles données
                        pixelData,
                        // stride = nombre de pixels pour une ligne de l'image
                        colorFrame.Width * colorFrame.BytesPerPixel,
                        // index de départ = 0
                        0);

Changez le format !

A tout moment, vous savez que vous pouvez voir le contenu de la propriété Format du ColorStream. Elle contient un enum de type ColorImageFormat. Cette valeur indique deux choses:

  • Comment est représenté chaque pixel.
  • Le nombre d'image par seconde.
On va ajouter une ComboBox à l'interface graphique:
            <ComboBox Width="200" x:Name="FormatComboBox" SelectionChanged="FormatComboBox_SelectionChanged" />

Dans le constructeur de la MainWindow, on va initialiser le contenu de la combobox:

            // Crée une liste contenant les différents formats
            IList<ColorImageFormat> colorImageFormat = new List<ColorImageFormat> 
                                                        { ColorImageFormat.RgbResolution640x480Fps30,
                                                        ColorImageFormat.RgbResolution1280x960Fps12,
                                                        ColorImageFormat.RawYuvResolution640x480Fps15,
                                                        ColorImageFormat.YuvResolution640x480Fps15
                                                        };
            // Attribue la liste à la combobox
            FormatComboBox.ItemsSource = colorImageFormat;

Et pour finir on implémente l'event hander FormatComboBox_SelectionChanged. Il n'est pas possible de modifier directement le format du ColorStream parce que cette propriété est read-only. Pour utiliser un format, il faut le passer en argument de la méthode Enable(...).

        private void FormatComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // Si un élément est sélectionné 
            // ET qu'on a une Kinect 
            // ET que cette Kinect est connectée
            if (e.AddedItems.Count > 0
                && Kinect != null
                && Kinect.Status == KinectStatus.Connected)
            {
                // On désactive le ColorStream
                Kinect.ColorStream.Disable();

                // On récupère le nouveau format
                var newFormat = (ColorImageFormat)e.AddedItems[0];

                // On réactive le stream avec le format spécifié
                Kinect.ColorStream.Enable(newFormat);
            }
        }

Dès que le stream est réactivé, les déclenchements de l'event ColorFrameReady reprennent et l'image se met de nouveau à jour.

Take a picture!

Un des trucs sympas à faire dans une application Kinect, c'est évidemment de prendre une photo!

Il y a plusieurs façon de le faire. Je vais vous en montrer une simple, et une très simple.

D'abord on ajoute un bouton à l'interface graphique:

 <Button Content="Take a picture!" x:Name="PictureTask" Click="PictureTask_Click" />

Et ensuite on implémente l'event handler. On ouvre une fenêtre de dialogue pour demander où l'utilisateur veut enregistrer le fichier, et s'il existe déjà on supprime l'existant.

        /// <summary>
        /// Handles the Click event of the PictureTask button.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void PictureTask_Click(object sender, RoutedEventArgs e)
        {
            string fileName = null;
            SaveFileDialog saveDialog = new SaveFileDialog();
            if (saveDialog.ShowDialog().Value)
            {
                fileName = saveDialog.FileName;
            }

            if (string.IsNullOrWhiteSpace(fileName))
                return;

            if (File.Exists(fileName))
            {
                File.Delete(fileName);
            }

            // Logique pour prendre une photo ici...

        }

A ce code, il faut qu'on a joute la logique de sauvegarde de l'image. Voici les deux méthodes que je vous propose:

Manière simple
            using (FileStream savedSnapshot = new FileStream(fileName, FileMode.CreateNew))
            {
                BitmapSource image = (BitmapSource)KinectImage.Source;
                JpegBitmapEncoder jpgEncoder = new JpegBitmapEncoder();
                jpgEncoder.QualityLevel = 70;
                jpgEncoder.Frames.Add(BitmapFrame.Create(image));
                jpgEncoder.Save(savedSnapshot);
                savedSnapshot.Flush();
                savedSnapshot.Close();
                savedSnapshot.Dispose();
            }
Manière très simple (Coding4Fun Toolkit)

Ajoutez à votre projet une référence au Kinect Toolkit de Coding4Fun:

[caption id="attachment_1709" align="aligncenter" width="635" caption="Ajout du Toolkit via Nuget"][/caption]

Ajoutez également une instruction using pour profiter des méthodes d'extensions:

using Coding4Fun.Kinect.Wpf;

Et pour finir, le tout se résume en deux lignes:

            // Coding4Fun helper's method:
            BitmapSource image = (BitmapSource)KinectImage.Source;
            image.Save(fileName, ImageFormat.Jpeg);

L'intérêt du Toolkit ici est d'ajouter une méthode d'extension à la classe BitmapSource pour pouvoir sauver son contenu avec le chemin et le format spécifié.

Transformer l'image

Pour terminer, voyons comment vous pouvez modifier l'image en temps réel. Comme vous savez, vous avez accès au tableau de bytes représentant l'image, appelé pixelData dans notre exemple.

Prenons le cas du format RgbResolution640x480Fps30:

pixelData contient 640 x 480 x 4 = 1228800 bytes. Cela correspond à 4 bytes par pixel. Les 3 premiers correspondent aux couleurs Bleu, Vert, et Rouge. Le 4ème est inutilisé, et correspondrait au canal alpha (transparence) dans un format Bgra.

Si on essaie maintenant simplement d'inverser toutes les valeurs, avant de mettre à jour le WriteableBitmap, l'image affichée donnera l'effet d'un négatif!

                    for (int i = 0; i < pixelData.Length - 3; i +=4)
                    {
                        // Transformation 1:
                        // ======================================= //
                        // ( ~ ) inverts the bits 
                        pixelData[i] = (byte)~pixelData[i];
                        pixelData[i + 1] = (byte)~pixelData[i + 1];
                        pixelData[i + 2] = (byte)~pixelData[i + 2];
                    }
L'opérateur ~ permet d'inverser tous les bits d'une valeur numérique.

On peut imaginer plein de transformations, en augmentant chacune des valeurs pour donner des couleurs saturées, ou bien en mettant à 0 les canaux Bleu et Vert pour afficher une image de teinte rouge uniquement.

Bref, c'est vous qui voyez!

La suite arrive la semaine prochaine, avec au menu: comment utiliser le SkeletonStream!

 

TextBox

About the author

I'm a developer, blog writer, and author, mainly focused on Microsoft technologies (but not only Smile). I'm Microsoft MVP Client Development since July 2013.

Microsoft Certified Professional

I'm currently working as an IT Evangelist with an awesome team at the Microsoft Innovation Center Belgique, where I spend time and energy helping people to develop their projects. I also give training to enthusiastic developers and organize afterworks with the help of the Belgian community.

MIC Belgique

Take a look at my first book (french only): Développez en HTML 5 pour Windows 8

Développez en HTML5 pour Windows 8

Membre de l'association Fier d'être développeur

TextBox

Month List