Débutant

[Actor Studio] Face Tracking et UWP

Je vous l'avais promis, on revient aujourd'hui décortiquer un peu notre application "Actor Studio" utilisée lors des Microsoft Experiences '18
Pour ceux qui n'auraient pas eu la chance de se rendre à cet événement ou de venir tester notre animation je vous invite à suivre l'article post-event que nous avions publié.

Maintenant que vous êtes au fait de ce que nous avons présenté, je vous propose que nous découvrions ensemble les coulisses d'une telle application.

Tout d'abord il est nécessaire de détailler les composants techniques nécessaires :

  • Une application Windows UWP (Universal Windows Platform)
  • Les Cognitive Services de Microsoft
  • Une webcam (la caméra de votre laptop suffit)
  • Un dataset (base de données de visages)
  • Une connexion internet

Grâce à ces éléments, vous serez en capacité de refaire notre animation, de la reconnaissance de visages dans un flux vidéo temps-réel à la détection d'émotions, en passant par la reconnaissance de personnes.

Code source

Le code source de notre application est disponibe sur notre repository Github.
Le code est complexe et peut largement être amélioré.
Il a été développé en quelques jours pour l'événement Microsoft Experiences '18 et sert de support à cet article.
Je vous invite donc à lire l'article pour en comprendre les éléments avant d'entrer dans le code complet.

Sommaire

Dans cet article, nous aborderons les bases de cette application, à savoir :

  • L'application Windows UWP
  • La capture et l'affichage du flux vidéo
  • La détection du visage le plus proche
  • L'affichage des cadres de visages

Universal Windows Platform

Il faut savoir que le code que nous fournirons en support de cet article est basé sur le framework UWP, nous permettant de faire fonctionner indifférement notre application sur un PC, un Raspberry Pi ou une XBox.
Hormis la détection de visage qui est native UWP, tous les autres éléments, logique applicative ou API cognitives, pourraient être réutilisés dans une application ciblant une autre platforme.

Commençons par l'élément principal de notre animation : l'application UWP.
Une fois Visual Studio et le SDK UWP installés, vous pourrez récupérer le code source de l'application via votre client Git préféré.
Vérifiez que vous pouvez compiler la solution, puis regardons ensemble le code.

La solution est composée de plusieurs projets :

  • Actor Studio
    il s'agit de l'application UWP comportant les pages, la logique et les dépendances vers les autres projets.
  • FaceApiProxy
    comme son nom l'indique il s'agit d'une couche logique faisant l'interface entre l'application et l'API de reconnaissance de visages
  • FaceControls
    ce projet contient les composants, graphiques ou logiques, destinés à la gestion des visages dans notre application

Ce découpage a pour objectif d'améliorer la lisibilité, de dissocier les responsabilités et de permettre une réutilisation de la logique dans d'autres applications ou le remplacement de la couche d'API par celle d'un autre service.

Attaquons par le projet FaceControls.
C'est dans ce projet que nous allons voir comment capter le flux de la caméra, l'afficher à l'écran et y déceler des visages.

Capture Vidéo

Pour afficher le flux vidéo, rien de sorcier, il faut utiliser le composant natif CaptureElement. Un composant de ce type est donc créé dans le composant FacetrackingControl avec le nom PreviewControl.
Dans le code behind de la classe FaceTrackingControl, nous avons ajouté une méthodeInitCameraAsync qui sera appelée par le ViewModel logique lorsque la page principale de l'animation sera ouverte.

MediaCapture

Un objet de type MediaCapture est créé et initialisé de manière à capturer uniquement le flux vidéo. Sans cette précision, les droits de vidéo ET audio auraient été nécessaires.
Dans notre repository vous noterez que la caméra est également choisie explicitement et non par défaut grâce à la gestion de Settings, mais nous resterons simple dans cet article :

// Initialize Camera Capture
VideoCapture = new MediaCapture();
MediaCaptureInitializationSettings settings = new MediaCaptureInitializationSettings()
{
	StreamingCaptureMode = StreamingCaptureMode.Video
};
await VideoCapture.InitializeAsync(settings);

Ensuite, le contenu de l'objet PreviewControl se voit attribuer l'objet MediaCapture comme source, en attribuant un effet miroir.

// Start Preview
PreviewControl.Source = VideoCapture;
PreviewControl.FlowDirection = mirroringPreview ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;

Enfin, la capture est démarrée et, avec elle, le rendu dans la page :

await VideoCapture.StartPreviewAsync();

Canvas

Sur le contrôle de capture vidéo, nous déposerons un Canvas, nommé FacesCanvas qui aura pour tâche de superposer les contours des visages detectés à la vidéo.

<Grid>
    <!--Camera preview-->
    <CaptureElement Name="PreviewControl" Stretch="UniformToFill" RenderTransformOrigin="0.5,0.5"/>

    <!--Canvas that will host the face detection bounding boxes, will share the same bounds as the preview within the CaptureElement-->
    <Canvas Name="FacesCanvas" RenderTransformOrigin="0.5,0.5"/>
</Grid>

Manifeste

Afin d'utiliser le flux vidéo, l'application doit en avoir le droit. Comme une application mobile sur votre smartphone, elle n'a par exemple pas le droit d'accéder, par défaut, à vos dossiers, à la caméra, etc.
Pour lui accorder ce droit, vous devez déclarer, dans le manifeste de votre application, les capacités (droits) qu'elle requiert et ce droit devra être validé par l'utilisateur.
Cela se fait en ouvrant le fichier Package.appxmanifest du projet principal de votre application (ici ActorStudio), il vous faut naviguer dans l'onglet Capabilities et cocher la case Webcam.

Une fois cette capacité déclarée, il sera proposé à l'utilisateur de valider ou refuser cet accès dès la première utilisation, par votre application, de la webcam.

Face Tracking

Nous avons capturé le flux vidéo, il ne nous reste plus qu'à y déceler les visages... Plus facile à dire qu'à faire me direz-vous ! Et bien non !

Pour déceler des visages dans un flux vidéo, Microsoft nous fournit nativement une API via l'objet MediaCapture et sa méthode AddVideoEffectAsync.

FaceDetectionEffectDefinition

Cette méthode permet, comme son nom l'indique, d'ajouter des effets au flux capté, qu'ils soient audio ou vidéo.
En l'occurence nous utilisons un effet de détection de visages dont la définition est déclarée via l'objet FaceDetectionEffectDefinition.

Le code d'ajout de cet effet à notre objet VideoCapture est le suivant :

private async Task InitFaceTrackerAsync()
{
        // Create the definition, which will contain some initialization settings
        var definition = new FaceDetectionEffectDefinition();

        // To ensure preview smoothness, do not delay incoming samples
        definition.SynchronousDetectionEnabled = false;

        // In this scenario, choose detection speed over accuracy
        definition.DetectionMode = FaceDetectionMode.HighPerformance;

        // Add the effect to the preview stream
        _faceDetectionEffect = 
                (FaceDetectionEffect) await VideoCapture.AddVideoEffectAsync(definition, MediaStreamType.VideoPreview);

        // Choose the shortest interval between detection events
        _faceDetectionEffect.DesiredDetectionInterval = TimeSpan.FromMilliseconds(33);

        // Register for face detection events
        _faceDetectionEffect.FaceDetected += FaceDetectionEffect_FaceDetected;

        Status = "Face tracker sucessfully initialized!";
}

Vous voyez dans cet exemple que nous créons la définition de cet effet, que nous appliquons certaines propriétés visant à améliorer la performance de détection en temps-réel, que nous appliquons cet effet à notre objet MediaCapture.
Une fois cet effet ajouté à notre capture, des événements seront levés à chaque détection.
Nous pouvons alors déclarer l'intervalle entre chacun de ces événement ainsi que le comportement à adopter en s'y abonnant.
Une fois la détection de visages liée à la capture, chaque frame du flux vidéo sera scanné et un événement et sera déclenché si un ou plusieurs visages sont détectés. Cela dans la limite de l'intervalle DesiredDetectionInterval.

Détection du visage le plus proche

La question est donc : Que faire de ces événements ?
Nous allons afficher un rectangle autour des visages détectés et les placer dans le Canvas que nous avons déjà superposé au rendu vidéo.

Pour cela nous allons utiliser l'objet de type FaceDetectedEventArgs levé par l'événement de détection. Cet objet contient un objet ResultFrame qui contient lui même une liste des visages par la propriété DetectedFaces.
Il peut arriver que certains événements n'aient détecté aucun visage, il faudra donc en faire la vérfiication en amont.
Pour chaque visage, un objet DetectedFace nous permet de récupérer la FaceBox. Il s'agit d'un objet de type BitmapBounds qui contient les contours du visage par rapport à la frame vidéo.
Nous pouvons donc filtrer les visages selon la taille de cette FaceBox et ne récupérer que le viage le plus volumineux, il s'agira probablement du visage le plus proche.

private async void FaceDetectionEffect_FaceDetected(FaceDetectionEffect sender, FaceDetectedEventArgs args)
{
        if (args.ResultFrame.DetectedFaces.Any())
        {
                var biggestFace = args.ResultFrame.DetectedFaces.OrderByDescending(f => f.FaceBox.Height * f.FaceBox.Width).FirstOrDefault();
                    ...

La détection du visage le plus proche nous permet d'appliquer sur celui-ci un effet particulier, à savoir "étendre la FaceBox" si possible, pour englober la tête et pas uniquement le visage.
Cette transformation est faite via la méthode TryExtendFaceBounds.

Affichage des visages

Afin d'afficher les visages détectés, et notamment le plus proche, nous allons devoir dessiner dans notre Canvas, un rectangle dont les propriétés Hauteur, Largeur, X et Y seront récupérées dans la FaceBox de l'objet biggestFace.
Pour cela nous allons créer une méthode HighlightDetectedFaces qui sera exécutée depuis le thread UI. Sans cette précaution, l'éxécution du code nous lèverait une exception nous indiquant que nous essayons d'accéder à un élément graphique depuis un autre thread. À cette méthode nous allons fournir les objets correspondant aux visages détectés.

// Ask the UI thread to render the face bounding boxes
await Dispatcher.RunAsync(
    CoreDispatcherPriority.Normal, 
    () => HighlightDetectedFaces(args.ResultFrame.DetectedFaces));

Dans la méthode HighlightDetectedFaces, nous commençons par supprimer tous les objets du Canvas. Il est plus simple et performant de vider le Canvas et d'y placer les nouveaux rectangles plutôt que de maintenir des rectangles pour chaque visage et modifier leurs positions. Ce nettoyage est fait via la méthode CleanCanvas.

Nous avons maintenant un Canvas tout propre. Nous pouvons donc parcourir les FaceBox et construire, pour chacune, un rectangle que nous positionnerons dans le Canvas.
Cet ajout se fait en convertissant un objet FaceBox en un Rectangle via la méthode ConvertPreviewToUiRectangle et en ajoutant cet élément graphique au Canvas.

// Set bounding box stroke properties
faceBoundingBox.StrokeThickness = 2;

// Highlight the first face in the set
faceBoundingBox.Stroke = _faceBoundingBoxColor;

// Add grid to canvas containing all face UI objects
FacesCanvas.Children.Add(faceBoundingBox);

Pour aller un peu plus loin, nous avons décidé d'afficher un smiley au lieu d'un rectangle pour le visage le plus proche.
Nous avons donc créé, dans le constructeur de la page, un objet de type BitmapIcon nommé smiley, basé sur une image.

_smiley = new BitmapIcon
{
        UriSource = new Uri(_smileyAssetPath),
        Foreground = _smileyColor
};

Nous avons ensuite modifié sa taille selon celle du visage pour l'englober complètement et nous l'avons ajouté au Canvas.
La méthode complète d'ajout des rectangles et du smiley ressemble donc à ça :

        /// <summary>
        /// Iterates over all detected faces, creating and adding Rectangles to the FacesCanvas as face bounding boxes
        /// </summary>
        /// <param name="faces">The list of detected faces from the FaceDetected event of the effect</param>
        private void HighlightDetectedFaces(IReadOnlyList<DetectedFace> faces)
        {
            CleanCanvas();

            if (faces.Count > 1)
                return;

            var orderedFaces = faces.OrderByDescending(f => f.FaceBox.Height * f.FaceBox.Width).ToList();
            // For each detected face
            for (int i = 0; i > orderedFaces.Count; i++)
            {
                // Face coordinate units are preview resolution pixels, which can be a different scale from our display resolution, so a conversion may be necessary
                Rectangle faceBoundingBox = ConvertPreviewToUiRectangle(faces[i].FaceBox);

                if (i != 0)
                {
                    // Set bounding box stroke properties
                    faceBoundingBox.StrokeThickness = 2;
                    // Highlight the first face in the set
                    faceBoundingBox.Stroke = _faceBoundingBoxColor;
                    // Add grid to canvas containing all face UI objects
                    FacesCanvas.Children.Add(faceBoundingBox);
                }
                else
                {
                    var left = Canvas.GetLeft(faceBoundingBox);
                    var top = Canvas.GetTop(faceBoundingBox);
                    var faceIcon =_smiley;
                    Canvas.SetLeft(faceIcon, left - faceBoundingBox.Width / 4);
                    Canvas.SetTop(faceIcon, top - faceBoundingBox.Height / 4);
                    faceIcon.Width = faceBoundingBox.Width * 1.5;
                    faceIcon.Height = faceBoundingBox.Height * 1.5;
                    FacesCanvas.Children.Add(faceIcon);
                }
            }
        }

Conclusion

Dans cet article, nous avons découvert comment les API natives des applications UWP nous permettent de déctecter des visages dans un flux vidéo, et de superposer des éléments graphiques au rendu vidéo pour mettre en évidence les visages détectés.

Tous ces effets sont disponibles en local, sans aucune connexion internet, grâce à la reconnaissance de visage locale. Il s'agit d'une mise en œuvre d'un modèle pré-entraîné par Microsoft et proposé via le SDK UWP.

Dans le prochain article, nous découvrirons comment créer, entraîner et consommer un modèle de reconnaissance de visages utilisant une base de visages sélectionnés par nos soins.

© SOAT
Toute reproduction est interdite sans autorisation de l’auteur.

Nombre de vue : 225

AJOUTER UN COMMENTAIRE