Canalblog
Editer l'article Suivre ce blog Administration + Créer mon blog
Publicité
Fanfois et ses papillons
3 juillet 2011

Kinect en .NET (la profondeur)

Kinect peut nous fournir en plus du flux d'image, que nous avons utilisé dans le précédent post, un flux de profondeur.

Le principe reste le même, nous allons recevoir un tableau de point, ce qui change c'est la signification des points et leur représentation. Il ne s'agit plus de pixel RGB mais d'une information sur deux octets qui est la profondeur en millimètre (ouf, on a évité les pouces). L'information est sur deux octets mais seuls 12 bits sont réellement utilisés, ce sont les bits 0 à 11 qui donnent la profondeur.

Au niveau programme la chose se manipule comme une image, en particulier il faut choisir une résolution. Trois possibilités d'après "ProgrammingGuide_KinectSDK.docx"  640x480, 320x240 et 80x60. Comme précédemment nous allons choisir 640x480.

Pour "voir" la profondeur nous devons la transformer en une couleur. Pour rester basique nous allons transformer la profondeur en un certain niveau de gris et définir arbitrairement qu'un point proche de la Kinect sera plus clair qu'un point éloigné.

Pour commencer il nous faut modifier le code d'initialisation existant pour récupérer ce second flux de profondeur :

// On demande l'accès à la caméra en couleur et à la profondeur.
kinectRuntime.Initialize(RuntimeOptions.UseColor | RuntimeOptions.UseDepth);

Ensuite nous ajoutons un abonnement à l'événement permettant de récupérer le flux profondeur :

// On s'abonne à l'événement permettant de récupérer la profondeur.
kinectRuntime.DepthFrameReady += new EventHandler(kinectRuntime_DepthFrameReady);

Avant de coder la fonction "kinectRuntime_DepthFrameReady" il nous faut ouvrir le flux profondeur. Le code suit exactement la même logique que pour le flux image normale :

// On ouvre le flux profondeur :
// - ImageStreamType.Depth indique que nous voulons le flux profondeur;
// - 2 donne le nombre de buffers que la Runtime peut utiliser;
// - ImageResolution.Resolution640x480 indique la taille de la pseudo image souhaitée;
// - ImageType.Depth précise que nous la voulons avec les informations de profondeur.
kinectRuntime.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution640x480, ImageType.Depth);

Avant de voir la fonction de traitement du flux profondeur, ajoutons un second contrôle "Image" à notre fenêtre :

<Window x:Class="TestKinect.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="535" Width="1335" Loaded="Window_Loaded" Closed="Window_Closed">
    <Grid>
        <Image Height="480" HorizontalAlignment="Left" Margin="12,12,0,0" Name="image1" Stretch="Fill" VerticalAlignment="Top" Width="640" />
        <Image Height="480" HorizontalAlignment="Left" Margin="662,12,0,0" Name="image2" Stretch="Fill" VerticalAlignment="Top" Width="640" />
    </Grid>
</Window>

Il nous faut maintenant construire une image affichable à partir de l'"image" profondeur fournie par Kinect. Evidement l'"image" se trouve dans le paramètre "e" qui est d'ailleurs du même type que précédemment. En effet le squelette généré par VS est :

void kinectRuntime_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
    throw new NotImplementedException();
}

Nous allons récupérer les informations de profondeur dans le tableau d'octets "e.ImageFrame.Image.Bits".

Nous devons prendre les octets deux par deux et les transformer en un entier correspondant à la profondeur en millimètre. Pour cela il nous faut "coller" les deux octets qui se suivent et éliminer les 4 derniers bits du second octet au cas où il y aurait autre chose que des 0 (normalement il y a des 0, mais j'ai déjà donné).

Le début de notre profondeur se trouve donc dans l'octet d'indice pair (rappel, le premier élément d'un tableau est à l'indice 0 donc pair).

La suite se trouve dans l'octet impair suivant, mais il nous faut éliminer les 4 bits de poids fort qui devraient déjà être à zéro (un et binaire avec 0xF fait cela très simplement) et décaler les 4 bits que nous avons conservés pour les mettre à gauche de l'octet d'indice pair. Cela donne :

profondeur = e.ImageFrame.Image.Bits[2n]  + ((e.ImageFrame.Image.Bits[2n + 1] & 0xF) << 8);

Bon en général vous trouverez la formule sans parenthèses, dans l'ordre des bits et avec un ou binaire plutôt qu'un plus. Cela nous donne :

profondeur  = (e.ImageFrame.Image.Bits[2n + 1] & 0xF) << 8 |e.ImageFrame.Image.Bits[2n];

Si vous croyez qu'il y a bien les 0 dès le départ vous pouvez vous limiter à :

profondeur  = e.ImageFrame.Image.Bits[2n + 1] << 8 |e.ImageFrame.Image.Bits[2n];

Remarque : notez qu'il n'y a pas d'erreur car C# passe en entier pour les "roll", car s'il restait en octet nous aurions un problème.


Maintenant que nous avons notre profondeur il nous faut la convertir en couleur. Il y a plein de possibilités, comme indiqué précédemment nous allons faire des niveaux de gris. Pour avoir du gris il faut mettre les trois composantes RGB à la même valeur et plus cette valeur est élevée plus le gris sera clair.

Le problème c'est que notre profondeur est sur 12 bits et que pour les pixels RGB il nous en faut 8. Pour réaliser cette transformation et obtenir une image assez contrastée, même en restant assis à notre bureau (ma Kinect personnelle est à 80 cm de moi, c'est-à-dire un peu trop près), nous allons utiliser le code suivant :

const float ProfondeurMin = 800;
const float ProfondeurMax = 1200;
const float PlageTraitee = ProfondeurMax - ProfondeurMin;
byte couleur = (byte)(255 - (255 * Math.Max(Math.Min(profondeur, ProfondeurMax) - ProfondeurMin, 0) / PlageTraitee));

Cette formule magique fait que ce qui est avant "ProfondeurMin" est blanc, ce qui est après "ProfondeurMax" est noir, et ce qui est entre les deux est de moins en moins clair plus on s'éloigne de "ProfondeurMin". Le code complet de la fonction de traitement de l'événement est :

/// <summary>
/// Handler permettant le traitement des images "profondeurs" fournies par Kinect.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void kinectRuntime_DepthFrameReady(object sender, ImageFrameReadyEventArgs e)
{
    // Création du tableau contenant l'image que nous allons fabriquer.
    // Les dimensions dépendent de l'"image" reçue.
    byte[] imageAffichage = new byte[e.ImageFrame.Image.Width * e.ImageFrame.Image.Height * 4];
 
    // Récupération du détail de l'image reçue.
    byte[] imageRecue = e.ImageFrame.Image.Bits;
 
    // Traitement des éléments de l'image un par un.
    // iRecu permet le parcours de l'image reçue où chaque élément occupe 2 octets.
    // iAffiche permet le parcours de l'image que nous afficherons où chaque élément occupe 4 octets.
    for (int iRecu = 0, iAffiche = 0; (iRecu < imageRecue.Length) && (iAffiche < imageAffichage.Length); iRecu += 2, iAffiche += 4)
    {
        // La vraie distance en mm par rapport à Kinect est sur 12 bits.
        int profondeur = (imageRecue[iRecu + 1] & 0xF) << 8 | imageRecue[iRecu];
 
        // Pour l'affichage on veut une profondeur sur un octet donc on doit traiter;
        // Ici ce qui est à ProfondeurMin ou moins est blanc,
        // ce qui est à ProfondeurMax ou plus est noir.
        // Ajustez les valeurs en fonction de votre espace.
        const float ProfondeurMin = 800;
        const float ProfondeurMax = 1200;
        const float PlageTraitee = ProfondeurMax - ProfondeurMin;
        byte couleur = (byte)(255 - (255 * Math.Max(Math.Min(profondeur, ProfondeurMax) - ProfondeurMin, 0) / PlageTraitee));
 
        // Pour obtenir des niveaux de gris on met les 3 composantes RGB à la même valeur.
        imageAffichage[iAffiche] = couleur;
        imageAffichage[iAffiche + 1] = couleur;
        imageAffichage[iAffiche + 2] = couleur;
    }
    image2.Source = BitmapSource.Create(e.ImageFrame.Image.Width, e.ImageFrame.Image.Height, 96, 96,
                        PixelFormats.Bgr32, null, imageAffichage, e.ImageFrame.Image.Width * 4);
}

Si vous lancez l'application vous devez voir deux images, à priori de vous, une relativement habituelle et l'autre un peu moins mais assez reconnaissable tout de même (sauf si vous codez dos à une fenêtre en plein soleil). On peut noter au passage que les deux images sont miroir l'une de l'autre.

En cas de problème, voici les deux fichiers xaml et xaml.cs dans un zip TestKinect02

Dans le prochain post nous mettrons un pied dans le vrai monde Kinect avec l'introduction de la notion de joueur.

Publicité
Publicité
Commentaires
Fanfois et ses papillons
Publicité
Archives
Publicité