Implémenter la reconnaissance faciale multiple avec Kinect et EmguCV

A l’occasion des TechDays 2012, nous avons développé des jeux multi-écrans collaboratifs sur Kinect, Silverlight et Windows Phone ainsi qu’un BackOffice en WPF style “métro” qui en plus d’enregistrer les joueurs pour leur envoyer leurs photos par e-mail et les publier sur notre page Facebook, nous l’avons doté d’une fonctionnalité amusante : la reconnaissance faciale avec Kinect.

Dans cet article, nous allons voir comment mettre en place cette fonctionnalité en utilisant le SDK Kinect en version Beta 2 ainsi la librairie Open CV qui est une bibliothèque Open Source de traitement d’image en temps réel.

La mise en place de cette fonctionnalité a été faite à partir de ce très bon article qui implémentait déjà une reconnaissance faciale multiple avec une webcam en Windows Forms : http://www.codeproject.com/Articles/239849/Multiple-face-detection-and-recognition-in-real-ti

Avant de commencer il vous faudra donc télécharger et installer le SDK Kinect Beta 2, disponible ici : http://www.microsoft.com/download/en/details.aspx?id=27876

Kinect SDK

Le capteur Kinect possède 3 flux : Image / Audio / Profondeur

Nous allons utiliser uniquement le flux d’image qui peut nous fournir deux types de résolutions différentes :

–          Résolution 640×480 : caméra vidéo uniquement – 30 images/s.
–          Résolution 1280×1024 : caméra vidéo uniquement – 15 images/s qui en réalité est une résolution de 1280×960.

Emgu CV

EmguCV permet d’utiliser la librairie OpenCV dans un environnement .NET. C’est ce que nous allons utiliser ici, nous aurons donc besoin des librairies suivantes (disponibles dans le code source de l’application démo à télécharger à la fin de cet article)  :

– Emgu.CV.dll
– Emgu.CV.GPU.dll
– Emgu.CV.ML.dll
– Emgu.CV.UI.dll
– Emgu.Util.dll

Création de l’application

Nous allons commencer par créer une nouvelle application de type WPF. Dans notre MainPage.xaml, nous voulons afficher notre flux d’image de notre capteur Kinect et sur cette image les rectangles autour des visages détectés avec le nom si ces visages sont enregistrés dans notre base de données. Nous aurons également un petit formulaire pour ajouter des visages. Voici notre interface :

Création de l’interface

Dans notre MainPage.xaml, on a ainsi le code suivant pour notre interface :

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Name="image1" Grid.RowSpan="2" />
<StackPanel Grid.Column="1" Margin="20">
<TextBlock Text="Person in the current sceen" Margin="0,5,0,0"/>
<TextBlock x:Name="txtPersons" Margin="0,5,0,0"/>
<TextBlock Text="Number of face detected" Margin="0,5,0,0"/>
<TextBlock x:Name="txtNbPersons" Margin="0,5,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1" Margin="20" Grid.Row="1">
<Image x:Name="imageBoxFace" MinWidth="150"/>
<TextBlock TextWrapping="Wrap" Text="Name :" Margin="0,10,0,0"/>
<TextBox x:Name="textBox_Name" Text="Name" Margin="0,10,0,0"/>
<Button x:Name="btnAddFace" Content="Add Face" Click="btnAddFace_Click" HorizontalAlignment="Right" Height="21.96" Margin="0,10,0,0"/>
</StackPanel>
</Grid>

Ensuite, on passe dans le code behind de notre page et on commence par déclarer les variables que nous allons utiliser :

//Kinect Runtime
Runtime nui;

//Declararation of all variables, vectors and haarcascades
private HaarCascade haarCascade;
int ContTrain, NumLabels, t;
Image<Gray, byte> result, TrainedFace = null;
Image<Gray, byte> gray = null;
Image<Bgr, Byte> currentFrame;
List<Image<Gray, byte>> trainingImages = new List<Image<Gray, byte>>();
MCvFont font = new MCvFont(FONT.CV_FONT_HERSHEY_TRIPLEX, 0.5d, 0.5d);

//General
Bitmap currentImage;
List<string> labels = new List<string>();
List<string> NamePersons = new List<string>();
string name, names = null;

Initialisation du capteur Kinect

Dans notre évènement Loaded de notre page, on commence par initialiser le capteur Kinect pour utiliser le flux d’image en résolution 640×480 :

private void SetupKinect()
{
if (Runtime.Kinects.Count == 0)
{
this.Title = "No Kinect connected";
}
else
{
//use first Kinect
nui = Runtime.Kinects[0];
//Initialize to return Color images
nui.Initialize(RuntimeOptions.UseColor);
//Set camera ElevationAngle to Maximum
nui.NuiCamera.ElevationAngle = Camera.ElevationMaximum;
//Initialize Image Flux
nui.VideoFrameReady += new EventHandler<ImageFrameReadyEventArgs>(nui_VideoFrameReady);
nui.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color);
}
}

Initialisation de la reconnaissance faciale

Toujours dans notre évènement Loaded de notre page, on initialise la reconnaissance faciale en chargeant notre fichier haarCascade pour la détection des visages. On vérifie également si des visages ne sont pas déjà existants et si c’est le cas on les charge dans notre tableau trainingImages :

haarCascade = new HaarCascade(@"haarcascade_frontalface_alt_tree.xml");
try
{
//Load of previus trainned faces and labels for each image
string Labelsinfo = File.ReadAllText(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/TrainedLabels.txt");
string[] Labels = Labelsinfo.Split('%');
NumLabels = Convert.ToInt16(Labels[0]);
ContTrain = NumLabels;
string LoadFaces;
for (int tf = 1; tf < NumLabels + 1; tf++)
{
LoadFaces = "face" + tf + ".bmp";
trainingImages.Add(new Image<Gray, byte>(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/" + LoadFaces));
labels.Add(Labels[tf]);
}
}
catch (Exception ex)
{
MessageBox.Show("Nothing in binary database, please add at least a face", "Triained faces load", MessageBoxButton.OK);
}

Implémentation de la reconnaissance

Nous allons utiliser l’évènement nui_VideoFrameReady qui nous fournit 30 images par seconde dans notre cas puis on va analyser chaque image avec notre librairie EmguCV.

Pour analyser les images avec notre librairie, il faut convertir celles-ci en new Image<Bgr, Byte> :

void nui_VideoFrameReady(object sender, ImageFrameReadyEventArgs e)
{
BitmapSource bSource = BitmapSource.Create(e.ImageFrame.Image.Width, e.ImageFrame.Image.Height, 96, 96, PixelFormats.Bgr32, null, e.ImageFrame.Image.Bits, e.ImageFrame.Image.Width * e.ImageFrame.Image.BytesPerPixel);
currentImage = ImageHelper.GetBitmapFromBitmapSource(bSource);

//Get the current frame form capture device
currentFrame = new Image(currentImage);

if (currentFrame != null)
{
//Convert it to Grayscale
Image grayFrame = currentFrame.Convert();

txtNbPersons.Text = "0";
NamePersons.Add("");

//Face Detector
MCvAvgComp[][] facesDetected = grayFrame.DetectHaarCascade(
haarCascade,
1.2,
2,
Emgu.CV.CvEnum.HAAR_DETECTION_TYPE.DO_CANNY_PRUNING,
new System.Drawing.Size(80, 80));

//Action for each element detected
foreach (MCvAvgComp f in facesDetected[0])
{
t = t + 1;
result = grayFrame.Copy(f.rect).Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
//draw the face detected in the 0th (gray) channel with blue color
currentFrame.Draw(f.rect, new Bgr(38, 191, 140), 3);

if (trainingImages.ToArray().Length != 0)
{
//TermCriteria for face recognition with numbers of trained images like maxIteration
MCvTermCriteria termCrit = new MCvTermCriteria(ContTrain, 0.001);

//Eigen face recognizer
EigenObjectRecognizer recognizer = new EigenObjectRecognizer(
trainingImages.ToArray(),
labels.ToArray(),
5000,
ref termCrit);

name = recognizer.Recognize(result);

//Draw the label for each face detected and recognized
currentFrame.Draw(name, ref font, new System.Drawing.Point(f.rect.X - 2, f.rect.Y - 2), new Bgr(System.Drawing.Color.Red));
}

NamePersons[t - 1] = name;
NamePersons.Add("");

//Set the number of faces detected on the scene
txtNbPersons.Text = facesDetected[0].Length.ToString();
}
t = 0;

//Names concatenation of persons recognized
for (int nnn = 0; nnn < facesDetected[0].Length; nnn++)
{
names = names + NamePersons[nnn] + ", ";
}
//Show the faces procesed and recognized
txtPersons.Text = names;
names = "";
//Clear the list(vector) of names
NamePersons.Clear();

image1.Source = ImageHelper.ToBitmapSource(currentFrame);
}
else
{
image1.Source = e.ImageFrame.ToBitmapSource();
}
}

Comment ça marche ?

Pour chaque image, on utilise la méthode DetectHaarCascade qui va détecter des visages et pour chaque visage détecté, on affiche un rectangle autour de ce dernier. Ensuite pour chaque visage détecté, on le compare à ceux déjà enregistrés dans notre base de données grâce à la méthode EigenObjectRecognizer. Si une personne est reconnue on affiche son nom.

Comment optimiser les performances ?

Ce projet de test a été créé pour tester et trouver les paramètres les plus adaptés avec une utilisation de Kinect de manière à ce que l’application reste le plus fluide possible.

Il est possible de changer certains paramètres de la méthode DetectHaarCascade dans le but d’affiner la détection des visages. Afin de comprendre ces paramètres je vous invite à consulter l’article cité ci-dessus. Il faut savoir que la détection peut plus ou moins bien fonctionner en fonction de la résolution des images stockées et des conditions d’éclairage. Plus la résolution des images sera grande, plus la méthode EigenObjectRecognizer sera longue à exécuter c’est pour cela qu’on utilise la résolution minimum de notre capteur Kinect. Je vous invite d’ailleurs à tester avec la 2ème résolution pour voir ce qu’il se passe.

Ajouter un visage à notre BDD

Au niveau de notre interface, on y trouve un formulaire permettant d’enregistrer des nouveaux visages, pour cela il suffit simplement de remplir le nom et de cliquer sur le bouton « Add Face », l’évènement ainsi déclenché permettra de sauvegarder le visage :

//Trained face counter
ContTrain = ContTrain + 1;

//Get a gray frame from capture device
Image tst = new Image(currentImage);
gray = tst.Resize(320, 240, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);

//Face Detector
MCvAvgComp[][] facesDetected = gray.DetectHaarCascade(
haarCascade,
1.2,
10,
Emgu.CV.CvEnum.HAAR_DETECTION_TYPE.DO_CANNY_PRUNING,
new System.Drawing.Size(20, 20));

//Action for each element detected
foreach (MCvAvgComp f in facesDetected[0])
{
TrainedFace = currentFrame.Copy(f.rect).Convert();
break;
}

//resize face detected image for force to compare the same size with the
//test image with cubic interpolation type method
TrainedFace = result.Resize(100, 100, Emgu.CV.CvEnum.INTER.CV_INTER_CUBIC);
trainingImages.Add(TrainedFace);
labels.Add(textBox_Name.Text);

//Show face added in gray scale
imageBoxFace.Source = ImageHelper.ToBitmapSource(TrainedFace);

//Write the number of triained faces in a file text for further load
File.WriteAllText(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/TrainedLabels.txt", trainingImages.ToArray().Length.ToString() + "%");

//Write the labels of triained faces in a file text for further load
for (int i = 1; i < trainingImages.ToArray().Length + 1; i++)
{
trainingImages.ToArray()[i - 1].Save(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/face" + i + ".bmp");
File.AppendAllText(System.IO.Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + "/TrainedFaces/TrainedLabels.txt", labels.ToArray()[i - 1] + "%");
}

Pour finaliser

Vous aurez besoin de créer dans votre dossier bin/Debug, un dossier “TrainedFaces” dans lequel il faut ajouter un fichier texte vierge “TrainedLabels.txt”. C’est dans ce dossier que les visages viendront s’enregistrer (au format *.bmp) et les noms seront enregistrés dans le fichier texte.

Pour faire fonctionner votre application, vous devrez également ajouter les *.dll que vous trouverez dans le dossier bin/Debug du projet téléchargeable ici :

Ces *.dll sont celles de la librairie OpenCV, elles sont nécessaires pour faire fonctionner le projet qui utilise EgmuCV : opencv_calib3d220.dll, opencv_contrib220.dll, opencv_core220.dll, opencv_features2d220.dll, opencv_ffmpeg220.dll, opencv_flann220.dll, opencv_gpu220.dll, opencv_highgui220.dll, opencv_imgproc220.dll, opencv_legacy220.dll, opencv_ml220.dll, opencv_objdetect220.dll and opencv_video220.dll, cv110.dll cvaux110.dll cvextern.dll cxcore110.dll highgui110.dll

Conclusion

Nous avons donc vu à travers cet article comment mettre en place la reconnaissance faciale avec Kinect, qui fait partie d’une des fonctionnalités de notre application BackOffice réalisée pour les Techdays 2012 et qui sera bientôt disponible en téléchargement sur CodePlex : sogames.codeplex.com

La nouvelle version de Kinect for Windows disponible depuis le 1er février permettra d’optimiser et d’avoir un meilleur résultat pour la reconnaissance faciale grâce à son nouveau capteur amélioré pour de la détection plus proche. “Windows 8 reconnaîtra les visages”, la reconnaissance de visage ferait partie des fonctionnalités censées être intégrées à Windows 8, affaire à suivre donc…

Nombre de vue : 1008

COMMENTAIRES 6 commentaires

  1. […] Lire la suite sur So@t [+] Share & Bookmark • Twitter • StumbleUpon • Digg • Delicious • Facebook […]

  2. douda dit :

    Très cool comme tuto. Merci !

    Cependant, j’utilise le sdk 1.5 et j’ai réussi à faire tourner l’appli sans erreur (syntax) mais des que je rajoute l’inststruction pour détecter les visages, elle affiche juste 1 carré noir et se plante :

    MCvAvgComp[][] facesDetected = grayFrame.DetectHaarCascade(
    haarCascade,
    1.2,
    2,
    Emgu.CV.CvEnum.HAAR_DETECTION_TYPE.DO_CANNY_PRUNING,
    new System.Drawing.Size(80, 80));

    Pouvez vous m’aider svp ?
    Merci d’avance

  3. Rafol dit :

    Merci pour cet article , je le trouve trés interessant , cependant je n’arrive pas à trouver ” ImageHelper”, c’est quoi exactement?

    Merci d’avance

  4. Bonjour,

    Merci pour cet article dont je me suis insipiré pour intégrer la notion dans la 2.6 de SARAH (http://encausse.net/s-a-r-a-h)

    Par contre j’étais assez étonné des perfs. J’ai réécris et optimisé tout le code mais malgré tout sur une machine puissante ça rame.

    Au final je fais la détection toutes les 24 images.

    – Est ce que c’est normal ?
    – Est ce qu’il y a des ruses concernant les backbuffer ?
    – Est ce qu’il y a des ruses pour utiliser le GPU de la CG?
    – Ne faudrait-il pas tracker la tête avec le skelton stream et ne faire que le matching du visage avec OpenCV.

  5. Une petite mise en pratique rapide (Vidéo H.264):
    https://plus.google.com/u/0/103307507185339634362/posts/UcX61bthvJH

    Par contre je me demandais si vous aviez des pointeur pour faire de la reconnaissance d’objets de la même manière ?

    Tout particulièrement avec rgb-d pour exploiter la profondeur du kinect ?

  6. seth dit :

    j’implementer l’application grace a votre tuto.merci maisd au moment de lancer l’application il me sort une erreur comme :”NOTHING IN BINARY DATABASE,PLEASE ADD ADD AT LEAST A FACE

AJOUTER UN COMMENTAIRE