Kinect SDK 1.0 – 2 – Utilisation du ColorStream
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.
<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:
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!
1 commentaire
Kinect SDK 1.0 – Hands-on! | Renaud Dumont - { .NET blog } · 01/05/2012 à 4:49
[…] […]
Les commentaires sont fermés.