TensorFlow.js et clmtrackr.js: suivre la direction du regard de l'utilisateur dans le navigateur

L'auteur de l'article, dont nous publions la traduction, propose de parler de la résolution de problèmes dans le domaine de la vision par ordinateur exclusivement au moyen d'un navigateur Web. Résoudre un tel problème n'est pas si difficile grâce à la bibliothèque JavaScript TensorFlow . Au lieu de former notre propre modèle et de le proposer aux utilisateurs dans le cadre du produit fini, nous leur donnerons la possibilité de collecter indépendamment des données et de former le modèle directement dans un navigateur, sur notre propre ordinateur. Avec cette approche, le traitement des données côté serveur est totalement inutile.


Vous pouvez découvrir ce que ce matériau est dédié à la création ici . Pour cela, vous aurez besoin d'un navigateur, d'une webcam et d'une souris modernes. Voici le code source du projet. Il n'est pas conçu pour fonctionner sur des appareils mobiles, l'auteur du document dit qu'il n'a pas eu le temps d'apporter les améliorations appropriées. De plus, il note que la tâche considérée ici deviendra plus compliquée si vous devez traiter le flux vidéo d'une caméra en mouvement.

Idée


Utilisons la technologie d'apprentissage automatique pour savoir exactement où l'utilisateur regarde lorsqu'il regarde une page Web. Nous le faisons en regardant ses yeux à l'aide d'une webcam.

Il est très facile d'accéder à la webcam dans le navigateur. Si nous supposons que l'image entière de la caméra sera utilisée comme entrée pour le réseau neuronal, alors nous pouvons dire qu'elle est trop grande pour ces fins. Le système devra faire beaucoup de travail juste pour déterminer la place dans l'image où se trouvent les yeux. Cette approche peut bien se montrer si nous parlons d'un modèle que le développeur forme seul et déploie sur le serveur, mais si nous parlons de formation et d'utilisation du modèle dans un navigateur, c'est trop.

Afin de faciliter la tâche du réseau, nous pouvons lui fournir seulement une partie de l'image - celle qui contient les yeux de l'utilisateur et une petite zone autour d'eux. Cette zone, qui est un rectangle entourant les yeux, peut être identifiée à l'aide d'une bibliothèque tierce. Par conséquent, la première partie de notre travail ressemble à ceci:


Entrée webcam, reconnaissance faciale, détection des yeux, image recadrée

Pour détecter le visage dans l'image, j'ai utilisé une bibliothèque appelée clmtrackr . Il n'est pas parfait, mais diffère par sa petite taille, ses bonnes performances et, en général, il accomplit sa tâche avec dignité.

Si une image petite mais sélectionnée intelligemment est utilisée comme entrée pour un réseau neuronal convolutionnel simple, le réseau peut apprendre sans aucun problème. Voici à quoi ressemble ce processus:


L'image d'entrée, le modèle est un réseau neuronal convolutif, les coordonnées, l'endroit prédit par le réseau sur la page où l'utilisateur regarde.

Une mise en œuvre minimale pleinement opérationnelle des idées discutées dans cette section sera décrite ici. Le projet, dont le code se trouve dans ce référentiel, possède de nombreuses fonctionnalités supplémentaires.

La préparation


Pour commencer, clmtrackr.js partir du référentiel approprié. Nous allons commencer le travail avec un fichier HTML vide, qui importe jQuery, TensorFlow.js, clmtrackr.js et le fichier main.js avec notre code, sur lequel nous travaillerons un peu plus tard:

 <!doctype html> <html> <body>   <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>   <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@0.12.0"></script>   <script src="clmtrackr.js"></script>   <script src="main.js"></script> </body> </html> 

Recevoir un flux vidéo depuis une webcam


Afin d'activer la webcam et d'afficher le flux vidéo sur la page, nous devons obtenir l'autorisation de l'utilisateur. Ici, je ne fournis pas de code qui résout les problèmes de compatibilité du projet avec différents navigateurs. Nous partirons de l'hypothèse que nos utilisateurs travaillent sur Internet en utilisant la dernière version de Google Chrome.

Ajoutez le code suivant au fichier HTML. Il doit être situé dans la <body> , mais au-dessus des balises <script> :

 <video id="webcam" width="400" height="300" autoplay></video> 

Maintenant, travaillons avec le fichier main.js :

 $(document).ready(function() { const video = $('#webcam')[0]; function onStreaming(stream) {   video.srcObject = stream; } navigator.mediaDevices.getUserMedia({ video: true }).then(onStreaming); }); 

Essayez ce code par vous-même. Lorsque vous ouvrez la page, le navigateur doit demander la permission, puis une image de la webcam apparaîtra à l'écran.

Plus tard, nous étendrons le code de la fonction onStreaming() .

Recherche de visage


Utilisons maintenant la bibliothèque clmtrackr.js pour rechercher des visages dans la vidéo. Tout d'abord, initialisez le système de suivi des visages en ajoutant le code suivant après const video = ... :

 const ctrack = new clm.tracker(); ctrack.init(); 

Maintenant, dans la fonction onStreaming() , nous connectons le système de recherche de visage en y ajoutant la commande suivante:

 ctrack.start(video); 

C'est tout ce dont nous avons besoin. Désormais, le système pourra reconnaître le visage dans le flux vidéo.

Ne croyez pas? Dessinons un «masque» autour de votre visage pour nous assurer que cela est vrai.
Pour ce faire, nous devons afficher l'image au-dessus de l'élément responsable de la diffusion de la vidéo. Vous pouvez dessiner quelque chose sur des pages HTML à l'aide de la <canvas> . Par conséquent, nous allons créer un tel élément en le superposant à l'élément qui affiche la vidéo. Le code suivant nous aidera à cela, qui doit être ajouté au fichier HTML sous l'élément <video> qui est déjà là:

 <canvas id="overlay" width="400" height="300"></canvas> <style>   #webcam, #overlay {       position: absolute;       top: 0;       left: 0;   } </style> 

Si vous le souhaitez, vous pouvez déplacer le style en ligne dans un fichier CSS distinct.

Ici, nous avons ajouté un <canvas> la même taille à la page que l'élément <video> . Le fait que les éléments seront placés dans la même position est assuré par les styles utilisés ici.

Maintenant, chaque fois que le navigateur affiche la prochaine image de la vidéo, nous allons dessiner quelque chose sur l' <canvas> . L'exécution de tout code lors de la sortie de chaque trame est effectuée à l'aide du mécanisme requestAnimationLoop() . Avant de sortir quoi que ce soit vers l' <canvas> , nous devons en retirer ce qui était dessus avant, le vider. Nous pouvons alors suggérer à clmtrackr de sortir le graphique directement vers l' <canvas> .

Voici le code qui implémente ce dont nous venons de parler. Ajoutez-le sous la commande ctrack.init() :

 const overlay = $('#overlay')[0]; const overlayCC = overlay.getContext('2d'); function trackingLoop() { // ,     , //     -   . requestAnimationFrame(trackingLoop); let currentPosition = ctrack.getCurrentPosition(); overlayCC.clearRect(0, 0, 400, 300); if (currentPosition) {   ctrack.draw(overlay); } } 

Appelez maintenant la fonction trackingLoop() dans la fonction onStreaming() immédiatement après ctrack.start() . Cette fonction planifiera elle-même son propre redémarrage dans chaque trame.

Actualisez la page et regardez la webcam. Vous devriez voir un «masque» vert autour de votre visage dans la fenêtre vidéo. Parfois, pour que le système reconnaisse correctement le visage, vous devez déplacer légèrement votre tête dans le cadre.


Résultats de reconnaissance faciale

Identification de la zone d'image contenant les yeux


Maintenant, nous devons trouver la zone rectangulaire de l'image dans laquelle se trouvent les yeux et la placer sur un <canvas> séparé.

Heureusement, cmltracker nous donne non seulement des informations sur l'emplacement du visage, mais aussi 70 points de contrôle. Si vous consultez la documentation de cmltracker, vous pouvez sélectionner exactement les points de contrôle dont nous avons besoin.


Points de contrôle

Nous décidons que les yeux sont la partie rectangulaire de l'image, dont les bords touchent les points 23, 28, 24 et 26, agrandis de 5 pixels dans chaque direction. Ce rectangle doit inclure tout ce qui est important pour nous, à moins que l'utilisateur incline trop la tête.

Maintenant, avant de pouvoir utiliser ce fragment de l'image, nous avons besoin d'un <canvas> pour sa sortie. Ses dimensions seront de 50x25 pixels. Un rectangle avec des yeux rentrera dans cet élément. Une légère déformation de l'image n'est pas un problème.

Ajoutez ce code au fichier HTML qui décrit l' <canvas> , qui inclura la partie de l'image qui a des yeux:

 <canvas id="eyes" width="50" height="25"></canvas> <style>   #eyes {       position: absolute;       top: 0;       right: 0;   } </style> 

La fonction suivante renverra les coordonnées x et y , ainsi que la largeur et la hauteur du rectangle entourant les yeux. Il prend en entrée un tableau de positions reçues de clmtrackr. Notez que chaque coordonnée reçue de clmtrackr a des composants x et y . Cette fonction doit être ajoutée à main.js :

 function getEyesRectangle(positions) { const minX = positions[23][0] - 5; const maxX = positions[28][0] + 5; const minY = positions[24][1] - 5; const maxY = positions[26][1] + 5; const width = maxX - minX; const height = maxY - minY; return [minX, minY, width, height]; } 

Maintenant, dans chaque image, nous allons extraire un rectangle avec les yeux du flux vidéo, l'encercler avec une ligne rouge sur l' <canvas> , qui est superposé à l'élément <video> , puis le copier dans le nouvel <canvas> . Veuillez noter que pour identifier correctement la zone dont nous avons besoin, nous calculerons les indicateurs resizeFactorX et resizeFactorY .

Remplacez le bloc if dans la fonction trackingLoop() par le code suivant:

 if (currentPosition) { //  ,     //   <canvas>,    <video> ctrack.draw(overlay); //  ,  ,    //   const eyesRect = getEyesRectangle(currentPosition); overlayCC.strokeStyle = 'red'; overlayCC.strokeRect(eyesRect[0], eyesRect[1], eyesRect[2], eyesRect[3]); //      , //        //      const resizeFactorX = video.videoWidth / video.width; const resizeFactorY = video.videoHeight / video.height; //          //    <canvas> const eyesCanvas = $('#eyes')[0]; const eyesCC = eyesCanvas.getContext('2d'); eyesCC.drawImage(   video,   eyesRect[0] * resizeFactorX, eyesRect[1] * resizeFactorY,   eyesRect[2] * resizeFactorX, eyesRect[3] * resizeFactorY,   0, 0, eyesCanvas.width, eyesCanvas.height ); } 

Après avoir rechargé la page maintenant, vous devriez voir un rectangle rouge autour des yeux, et ce que ce rectangle contient est dans l' <canvas> correspondant. Si vos yeux sont plus grands que les miens, getEyeRectangle fonction getEyeRectangle .


élément <canvas> qui dessine un rectangle contenant l'image des yeux de l'utilisateur

Collecte de données


Il existe de nombreuses façons de collecter des données. J'ai décidé d'utiliser les informations qui peuvent être obtenues à partir de la souris et du clavier. Dans notre projet, la collecte de données ressemble à ceci.

L'utilisateur déplace le curseur sur la page et le regarde des yeux, en appuyant sur la du clavier chaque fois que le programme a besoin d'enregistrer un autre échantillon. Avec cette approche, il est facile de collecter rapidement un grand ensemble de données pour la formation du modèle.

▍Suivi de la souris


Afin de savoir exactement où se trouve le pointeur de la souris sur la page Web, nous avons besoin d'un gestionnaire d'événements document.onmousemove . De plus, notre fonction normalise les coordonnées afin qu'elles s'inscrivent dans la plage [-1, 1]:

 //   : const mouse = { x: 0, y: 0, handleMouseMove: function(event) {   //      ,    [-1, 1]   mouse.x = (event.clientX / $(window).width()) * 2 - 1;   mouse.y = (event.clientY / $(window).height()) * 2 - 1; }, } document.onmousemove = mouse.handleMouseMove; 

▍ Capture d'image


Pour capturer l'image affichée par l' <canvas> et l'enregistrer en tant que tenseur, TensorFlow.js propose la fonction d'assistance tf.fromPixels() . Nous l'utilisons pour enregistrer puis normaliser l'image de l' <canvas> qui affiche un rectangle contenant les yeux de l'utilisateur:

 function getImage() { //       return tf.tidy(function() {   const image = tf.fromPixels($('#eyes')[0]);   //  <i><font color="#999999"></font></i>:   const batchedImage = image.expandDims(0);   //    :   return batchedImage.toFloat().div(tf.scalar(127)).sub(tf.scalar(1)); }); } 

Notez que la fonction tf.tidy() est utilisée pour nettoyer une fois l'opération terminée.

Nous pourrions simplement enregistrer tous les échantillons dans un grand ensemble de formation, mais en apprentissage automatique, il est important de vérifier la qualité de la formation du modèle. C'est pourquoi nous devons enregistrer certains échantillons dans un échantillon de contrôle séparé. Après cela, nous pouvons vérifier le comportement du modèle sur les nouvelles données et découvrir si le modèle a été trop entraîné. À cette fin, 20% du nombre total d'échantillons sont inclus dans l'échantillon témoin.

Voici le code utilisé pour collecter des données et des échantillons:

 const dataset = { train: {   n: 0,   x: null,   y: null, }, val: {   n: 0,   x: null,   y: null, }, } function captureExample() { //            tf.tidy(function() {   const image = getImage();   const mousePos = tf.tensor1d([mouse.x, mouse.y]).expandDims(0);   // ,    (  )     const subset = dataset[Math.random() > 0.2 ? 'train' : 'val'];   if (subset.x == null) {     //        subset.x = tf.keep(image);     subset.y = tf.keep(mousePos);   } else {     //          const oldX = subset.x;     const oldY = subset.y;     subset.x = tf.keep(oldX.concat(image, 0));     subset.y = tf.keep(oldY.concat(mousePos, 0));   }   //     subset.n += 1; }); } 

Et enfin, nous devons lier cette fonction à la :

 $('body').keyup(function(event) { //         if (event.keyCode == 32) {   captureExample();   event.preventDefault();   return false; } }); 

Désormais, chaque fois que vous appuyez sur la , l'image des yeux et les coordonnées du pointeur de la souris sont ajoutées à l'un des ensembles de données.

Formation modèle


Créez un réseau neuronal convolutif simple. TensorFlow.js fournit une API qui rappelle Keras à cet effet. Le réseau doit avoir une couche conv2d , une couche maxPooling2d et enfin une couche dense avec deux valeurs de sortie (elles représentent les coordonnées de l'écran). En cours de route, j'ai ajouté une couche d' dropout et une couche d' flatten au réseau, en tant que régularisateur, afin de convertir les données bidimensionnelles en une dimension. La formation réseau est effectuée à l'aide de l'optimiseur Adam.

Veuillez noter que j'ai choisi les paramètres réseau utilisés ici après avoir expérimenté avec mon MacBook Air. Vous pouvez très bien choisir votre propre configuration du modèle.

Voici le code du modèle:

 let currentModel; function createModel() { const model = tf.sequential(); model.add(tf.layers.conv2d({   kernelSize: 5,   filters: 20,   strides: 1,   activation: 'relu',   inputShape: [$('#eyes').height(), $('#eyes').width(), 3], })); model.add(tf.layers.maxPooling2d({   poolSize: [2, 2],   strides: [2, 2], })); model.add(tf.layers.flatten()); model.add(tf.layers.dropout(0.2)); //    x  y model.add(tf.layers.dense({   units: 2,   activation: 'tanh', })); //   Adam     0.0005     MSE model.compile({   optimizer: tf.train.adam(0.0005),   loss: 'meanSquaredError', }); return model; } 

Avant de commencer la formation du réseau, nous avons défini un nombre fixe d'ères et une taille de paquet variable (car nous travaillerons probablement avec de très petits ensembles de données).

 function fitModel() { let batchSize = Math.floor(dataset.train.n * 0.1); if (batchSize < 4) {   batchSize = 4; } else if (batchSize > 64) {   batchSize = 64; } if (currentModel == null) {   currentModel = createModel(); } currentModel.fit(dataset.train.x, dataset.train.y, {   batchSize: batchSize,   epochs: 20,   shuffle: true,   validationData: [dataset.val.x, dataset.val.y], }); } 

Ajoutez maintenant un bouton à la page pour commencer à apprendre. Ce code va dans le fichier HTML:

 <button id="train">Train!</button> <style>   #train {       position: absolute;       top: 50%;       left: 50%;       transform: translate(-50%, -50%);       font-size: 24pt;   } </style> 

Ce code doit être ajouté au fichier JS:

 $('#train').click(function() { fitModel(); }); 

Où cherche l'utilisateur?


Maintenant que nous pouvons collecter les données et préparer le modèle, nous pouvons commencer à prédire l'endroit sur la page où l'utilisateur regarde. Nous indiquons cet endroit à l'aide d'un cercle vert, qui se déplace autour de l'écran.

Tout d'abord, ajoutez un cercle à la page:

 <div id="target"></div> <style>   #target {       background-color: lightgreen;       position: absolute;       border-radius: 50%;       height: 40px;       width: 40px;       transition: all 0.1s ease;       box-shadow: 0 0 20px 10px white;       border: 4px solid rgba(0,0,0,0.5);   } </style> 

Afin de la déplacer sur la page, nous transmettons périodiquement l'image actuelle des yeux du réseau neuronal et lui posons une question sur l'endroit où l'utilisateur regarde. Le modèle en réponse produit deux coordonnées le long desquelles le cercle doit être déplacé:

 function moveTarget() { if (currentModel == null) {   return; } tf.tidy(function() {   const image = getImage();   const prediction = currentModel.predict(image);   //          const targetWidth = $('#target').outerWidth();   const targetHeight = $('#target').outerHeight();   const x = (prediction.get(0, 0) + 1) / 2 * ($(window).width() - targetWidth);   const y = (prediction.get(0, 1) + 1) / 2 * ($(window).height() - targetHeight);   //     :   const $target = $('#target');   $target.css('left', x + 'px');   $target.css('top', y + 'px'); }); } setInterval(moveTarget, 100); 

J'ai réglé l'intervalle à 100 millisecondes. Si votre ordinateur n'est pas aussi puissant que le mien, vous pouvez décider de l'agrandir.

Résumé


Nous avons maintenant tout ce dont nous avons besoin pour mettre en œuvre l'idée présentée au tout début de ce document. Découvrez ce que nous avons fait. Déplacez le curseur de la souris en suivant ses yeux et appuyez sur la barre d'espace. Cliquez ensuite sur le bouton Démarrer l'entraînement.

Collectez plus de données, cliquez à nouveau sur le bouton. Après un certain temps, le cercle vert commencera à se déplacer autour de l'écran après votre regard. Au début, il ne sera pas particulièrement bon d'arriver à l'endroit où vous regardez, mais, à partir d'une cinquantaine d'échantillons collectés, après plusieurs étapes de formation, et si vous avez de la chance, il se déplacera assez précisément au point de la page que vous regardez. . Le code complet de l'exemple analysé dans ce document peut être trouvé ici .

Bien que ce que nous avons fait semble déjà très intéressant, de nombreuses améliorations peuvent encore être apportées. Que faire si l'utilisateur bouge la tête ou change de position devant la caméra? Notre projet ne nuirait pas aux possibilités concernant la sélection de la taille, de la position et de l'angle du rectangle qui délimite la zone d'image dans laquelle se trouvent les yeux. En fait, plusieurs fonctionnalités supplémentaires sont implémentées dans la version complète de l'exemple présenté ici. En voici quelques uns:

  • Les options de personnalisation du rectangle de délimitation des yeux décrites ci-dessus.
  • Convertissez une image en niveaux de gris.
  • Utilisation de CoordConv .
  • Une carte thermique pour vérifier où le modèle a bien fonctionné et où il ne l'a pas fait.
  • Possibilité d'enregistrer et de charger des jeux de données.
  • Possibilité d'enregistrer et de charger des modèles.
  • Préservation des poids qui ont montré une perte d'entraînement minimale après l'entraînement.
  • Interface utilisateur améliorée avec de brèves instructions pour travailler avec le système.

Chers lecteurs! Utilisez-vous TensorFlow?

Source: https://habr.com/ru/post/fr426055/


All Articles