Comment développer avec le SDK Kinect en C++ : Part I.

Si vous n’avez pas déjà téléchargé le SDK Kinect il est disponible à cette adresse :https://research.microsoft.com/en-us/um/redmond/projects/kinectsdk/ mais attention il ne peut pas être utilisé à des fins commerciales.

Télécharger le code source.

La procédure d’installation est simple et rapide, moins de 100MB,

La documentation, les librairies et les fichiers d’entêtes sont disponibles ici

C:\Program Files (x86)\Microsoft Research KinectSDK

Les exemples de code ici :

C:\Users\Public\Documents\Microsoft Research KinectSDK Samples

Architecture

clip_image002

clip_image003

Le capteur Kinect envoie vers le système un ensemble de trois flux :

  • Le flux image peut être affiché comme pour n’importe quelle caméra.
    Le capteur Kinect peur retourner le flux en 640x480 (à 30 images par seconde) et en 1280x1024 (mais à 15 images par seconde).
  • Le flux de profondeur est le facteur déterminant. Il va en effet donner à chaque pixel une profondeur depuis le capteur. Ainsi en plus de la position en 2D de chaque pixel (et de leur couleur) nous disposons désormais de leur profondeur. Cela va énormément faciliter les recherches de formes.
  • Un troisième flux est également envoyé depuis le capteur : il s’agit du flux audio en provenance des quatre microphones du Kinect. Le point clef ici concerne donc la capacité du Kinect à nous donner des informations tri-dimensionnelles. En utilisant ces dernières la librairie NUI (qui est fournie avec le SDK) est capable de détecter la présence d’humains en face du capteur. Elle peut ainsi “voir” jusqu’à 4 personnes et en suivre précisément deux.

Quand Kinect suit précisément une personne, elle peut fournir au développeur un squelette formé de points 20 points clefs détectés sur la personne comme illustré en couleur sur la figure suivante:

En bas à droite le capteur de profondeur.

clip_image005

Mise en œuvre.

Tout d’abord, il faut inclure le fichier d’entête MSR_NuiApi.h, puis se lier à la librairie MSRKinectNUI.lib

Ces deux fichiers ont pour préfixe MSR, ce qui indique leur provenance, Microsoft Research. A n’utiliser donc qu’à des fins de recherches et non commerciales.

Remarques: L’installation du SDK Kinect créée une variable d’environnement $(MSRKINECTSDK) que vous pouvez utiliser en tant que référence aux fichiers d’entête et librairie.
$(MSRKINECTSDK)\inc
$(MSRKINECTSDK)\lib

Bien que vous puissiez utiliser le SDK Kinect sur un Windows 7 32/64 Bits, la librairie n’existe qu’en 32 Bits.

Il faut tout d’abord initialiser Kinect, à l’aide de l’API NuiInitialize() en lui passant une combinaison de drapeau.
Ici nous décidons de recevoir des images (NUI_INITIALIZE_FLAG_USES_COLOR), de traquer le squelette, c’est-à-dire les 20 points (NUI_INITIALIZE_FLAG_USES_SKELETON) et de recevoir du capteur de profondeur, à la fois les informations de profondeurs, mais également l’index du « player », c’est-à-dire des personnes qui seront reconnues NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX) , comme nous le détaillerons dans un second billet.

hr = NuiInitialize(NUI_INITIALIZE_FLAG_USES_DEPTH_AND_PLAYER_INDEX |
NUI_INITIALIZE_FLAG_USES_SKELETON |
NUI_INITIALIZE_FLAG_USES_COLOR);

Pour arrêter la capture il faut utiliser l’API NuiShutdown() .

NuiShutdown();

Pour débuter avec le SDK Kinect, la première opération que nous allons faire c’est de capturer les images afin de les afficher et en créer une vidéo.
Pour récupérer les images, il faut ouvrir un flux à l’aide de l’API NuiImageStreamOpen() comme suit :

  • Déclarez deux variables de type HANDLE, l’une _videoStreamHandle qui servira comme pointeur afin de recevoir le flux de données, l’autre _videoNextFrame, qui servira pour signaler la disponibilité de la trame suivante. Nous utiliserons par la suite l’API WaitForSingleObject avec ce handle, afin d’attendre la disponibilité de la prochaine trame.
    HANDLE _videoStreamHandle;
    HANDLE _videoNextFrame;
    _videoNextFrame=CreateEvent( NULL, TRUE, FALSE, NULL );
  • Nous décidons de capturer des images en RGB NUI_IMAGE_TYPE_COLOR dans une résolution de 640*480 NUI_IMAGE_RESOLUTION_640x480
  • Le 3ième paramètre contrôle le pré-processing de l’image. Nous le mettons ici est à 0, car ce drapeau n’est pas utilisé dans la version actuelle du SDK Kinect.
  • Le 4ième paramètre correspond au nombre de frame que le runtime NUI doit buffériser. Dans la documentation il est noté que le maximum est 4 (NUI_IMAGE_STREAM_FRAME_LIMIT_MAXIMUM), mais 2 suffit pour la plupart des applications.
  • Enfin les 5ièmes et 6ièmes paramètres correspondent à nos deux HANDLE définis plus haut.
    HRESULT hr=S_OK;
    //Création du manual reset event, afin d'attendre la disponibilité de la prochaine trame
    _videoNextFrame=CreateEvent( NULL, TRUE, FALSE, NULL );
    hr = NuiImageStreamOpen(NUI_IMAGE_TYPE_COLOR,
    NUI_IMAGE_RESOLUTION_640x480,
    0,
    2,
    _videoNextFrame,
    &_videoStreamHandle);

    L’API NuiImageStreamOpen, retourne un HRESULT, indiquant la réussite (S_OK) ou une erreur. (Pour de plus amples informations sur les erreurs possibles, reportez-vous à la documentation, que vous pouvez trouver à cette adresse . C:\Program Files (x86)\Microsoft Research KinectSDK\docs\KinectSDK.chm

Pour récupérer la trame suivante, nous allons utiliser le mode polling, c’est-à-dire dans une boucle sans fin, l’API NuiImageStreamGetNextFrame de la manière suivante :

HRESULT hr = NuiImageStreamGetNextFrame(
_videoStreamHandle,
0,
&pImageFrame );

  • Le 1er paramètre est _videoStreamHandle crée par l’API NuiImageStreamOpen

  • Le second paramètre est un délai d’attente exprimé en millisecondes avant que la méthode ne retourne sans nouvelle trame.

  • Le 3ième est un pointeur sur la structure NUI_IMAGE_FRAME qui recevra les données. C’est cette structure que nous manipulerons pour afficher la trame à l’écran.
    const NUI_IMAGE_FRAME * pImageFrame = NULL;

  • A partir de la trame, nous allons récupérer les données de l’image à l’aide de l’objet NuiImageBuffer (Cet objet est similaire à la texture Direct3D)
    NuiImageBuffer * pTexture = pImageFrame->pFrameTexture;

  • A partir de cette texture, nous allons récupérer un pointeur sur la structure KINECT_LOCKED_RECT
    pTexture->LockRect( 0, &LockedRect, NULL, 0 ) ;

    Structure qui contiendra un pointeur sur les données de la trame

    BYTE * pBuffer = (BYTE*) LockedRect.pBits;
    Ainsi que la longueur de chaque ligne
    LockedRect.Pitch

  • Une fois ces deux informations obtenues, nous utilisons ici Direct2D pour afficher la trame à l’écran.
    hr=_pD2D1Helper->D2D1DrawFrame (pBuffer,LockedRect.Pitch,640,480);
    (Je ne rentre pas dans le détail de Direct2D, vous retrouverez en fin de billet un listing sur la création d’une Bitmap Direct2D et son affichage à l’écran. L’avantage d’utiliser ici Direct2D plutôt que le GDI/GDI+ c’est que je peux profiter de l’accélération Hardware)

  • Enfin il est nécessaire de libérer les ressources avec l’API NuiImageStreamReleaseFrame

    hr=NuiImageStreamReleaseFrame(_videoStreamHandle, pImageFrame ); Listing complet

    HRESULT DPEKinectHelper::KinDemarrerVideo()
    {while(true)
    {
    if (Context::IsCurrentTaskCollectionCanceling ())
    {
    break;
    }
    _pD2D1Helper->D2D1GetContexteRendu()->BeginDraw ();
    _pD2D1Helper->D2D1PeindreArrierePlan ();
    const NUI_IMAGE_FRAME * pImageFrame = NULL;
    WaitForSingleObject (_videoNextFrame,INFINITE);
    HRESULT hr = NuiImageStreamGetNextFrame(
    _videoStreamHandle,
    0,
    &pImageFrame );

    if(SUCCEEDED(hr))
    {
    NuiImageBuffer * pTexture = pImageFrame->pFrameTexture;
    KINECT_LOCKED_RECT LockedRect;
    pTexture->LockRect( 0, &LockedRect, NULL, 0 );
    if( LockedRect.Pitch != 0 )
    {
    BYTE * pBuffer = (BYTE*) LockedRect.pBits;
    hr=_pD2D1Helper->D2D1DrawFrame (pBuffer,LockedRect.Pitch,640,480);
    hr=pTexture->UnlockRect (0);
    }
    else
    {
    OutputDebugString( L"Longueur du buffer erronée\r\n" );
    }
    hr=NuiImageStreamReleaseFrame(_videoStreamHandle, pImageFrame );
    }
    _pD2D1Helper->D2D1GetContexteRendu()->EndDraw();
    }

    }

Notes : Avant de récupérer la trame suivante, nous attendons que le runtime NUI nous signale sa disponibilité.
WaitForSingleObject (_videoNextFrame,INFINITE);

Et pour éviter de figer l’interface graphique, j’encapsule tout ce petit monde dans une tâche que je peux arrêter à n’importe quel moment.
_tasks=new task_group ();
_tasks->run ([=]()
{
HRESULT hr=S_OK;
hr=_KinectHelper->KinDemarrerVideo();
if (FAILED(hr))
{
AfficherMessageErreur(_hWnd,hr,_hInst);
_tasks->cancel ();
}
});

Pour savoir si une tâche est en cours d’arrêt, j’utilise ici le contexte courant
if (Context::IsCurrentTaskCollectionCanceling ())
{
break;
}

Dans le second billet nous abordons la manière de manipuler les informations de profondeurs.
Comment développer avec le SDK Kinect en C++ Part II

Télécharger le code source.

Eric Vernié

Pour information, voici le listing Direct2D (incomplet) de la méthode d’affichage d’une trame
HRESULT DPEDirectD2D1helper::D2D1DrawFrame(BYTE* frame, UINT32 pitch,UINT32 width,UINT32 height)
{
HRESULT hr=S_OK;
D2D1_SIZE_U size;
size.height =height;
size.width =width;
D2D1_BITMAP_PROPERTIES properties=D2D1::BitmapProperties (); properties.pixelFormat.format =DXGI_FORMAT_B8G8R8A8_UNORM ;
properties.pixelFormat.alphaMode =D2D1_ALPHA_MODE_PREMULTIPLIED;
hr=_pcontexteRenduD2D1->CreateBitmap(size,
frame,
pitch,
properties,&_pD2D1Frame);
_pD2D1Frame->AddRef ();
if (SUCCEEDED(hr))
{
RECT rc;
GetClientRect(_pcontexteRenduD2D1->GetHwnd (),&rc);
D2D1_RECT_F rect;
rect.top =(FLOAT)rc.top ;
rect.bottom =(FLOAT)rc.bottom ;
rect.right =(FLOAT)rc.right;
rect.left =(FLOAT)rc.left;
_pcontexteRenduD2D1->DrawBitmap (_pD2D1Frame,&rect); }
SafeRelease (&_pD2D1Frame);
return hr;
}