TensorFlow.js und clmtrackr.js: Verfolgen der Blickrichtung des Benutzers im Browser

Der Autor des Artikels, dessen Übersetzung wir veröffentlichen, bietet an, über die Lösung von Problemen aus dem Bereich der Bildverarbeitung ausschließlich über einen Webbrowser zu sprechen. Die Lösung eines solchen Problems ist dank der TensorFlow- JavaScript-Bibliothek nicht so schwierig. Anstatt unser eigenes Modell zu schulen und es den Benutzern als Teil des fertigen Produkts anzubieten, bieten wir ihnen die Möglichkeit, unabhängig Daten zu sammeln und das Modell direkt in einem Browser auf unserem eigenen Computer zu trainieren. Bei diesem Ansatz ist eine serverseitige Datenverarbeitung völlig unnötig.


Hier können Sie erfahren, wofür dieses Material bestimmt ist. Dazu benötigen Sie einen modernen Browser, eine Webcam und eine Maus. Hier ist der Quellcode des Projekts. Er ist nicht für die Arbeit mit Mobilgeräten ausgelegt. Der Autor des Materials gibt an, dass er keine Zeit für entsprechende Verbesserungen hatte. Darüber hinaus stellt er fest, dass die hier betrachtete Aufgabe komplizierter wird, wenn Sie den Videostream von einer sich bewegenden Kamera verarbeiten müssen.

Idee


Lassen Sie uns mithilfe der Technologie des maschinellen Lernens genau herausfinden, wohin der Benutzer schaut, wenn er eine Webseite betrachtet. Wir tun dies, indem wir seine Augen mit einer Webcam beobachten.

Es ist sehr einfach, im Browser auf die Webcam zuzugreifen. Wenn wir davon ausgehen, dass das gesamte Bild von der Kamera als Eingabe für das neuronale Netzwerk verwendet wird, können wir sagen, dass es für diese Zwecke zu groß ist. Das System muss viel Arbeit leisten, um die Stelle im Bild zu bestimmen, an der sich die Augen befinden. Dieser Ansatz kann sich gut zeigen, wenn es sich um ein Modell handelt, das der Entwickler selbst trainiert und auf dem Server bereitstellt. Wenn wir jedoch über das Training und die Verwendung des Modells in einem Browser sprechen, ist dies zu viel.

Um die Aufgabe des Netzwerks zu erleichtern, können wir ihm nur einen Teil des Bildes zur Verfügung stellen - den, der die Augen des Benutzers und einen kleinen Bereich um ihn herum enthält. Dieser Bereich, bei dem es sich um ein Rechteck handelt, das die Augen umgibt, kann mithilfe einer Bibliothek eines Drittanbieters identifiziert werden. Daher sieht der erste Teil unserer Arbeit folgendermaßen aus:


Webcam-Eingabe, Gesichtserkennung, Augenerkennung, zugeschnittenes Bild

Um das Gesicht im Bild zu erkennen, habe ich eine Bibliothek namens clmtrackr verwendet . Es ist nicht perfekt, unterscheidet sich jedoch durch geringe Größe, gute Leistung und bewältigt seine Aufgabe im Allgemeinen mit Würde.

Wenn ein kleines, aber intelligent ausgewähltes Bild als Eingabe für ein einfaches neuronales Faltungsnetzwerk verwendet wird, kann das Netzwerk problemlos lernen. So sieht dieser Prozess aus:


Das Eingabebild, das Modell, ist ein Faltungsnetzwerk, Koordinaten, der Ort, den das Netzwerk auf der Seite, auf der der Benutzer sucht, vorhersagt.

Eine voll funktionsfähige Mindestimplementierung der in diesem Abschnitt diskutierten Ideen wird hier beschrieben. Das Projekt, dessen Code sich in diesem Repository befindet, verfügt über viele zusätzliche Funktionen.

Vorbereitung


clmtrackr.js aus dem entsprechenden Repository clmtrackr.js . Wir werden die Arbeit mit einer leeren HTML-Datei beginnen, die jQuery, TensorFlow.js, clmtrackr.js und die Datei main.js mit unserem Code main.js , an dem wir etwas später arbeiten werden:

 <!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> 

Empfangen Sie einen Videostream von einer Webcam


Um die Webcam zu aktivieren und den Videostream auf der Seite anzuzeigen, benötigen wir die Benutzerberechtigung. Hier stelle ich keinen Code zur Verfügung, der die Kompatibilitätsprobleme des Projekts mit verschiedenen Browsern löst. Wir gehen davon aus, dass unsere Nutzer mit der neuesten Version von Google Chrome im Internet arbeiten.

Fügen Sie der HTML-Datei den folgenden Code hinzu. Es sollte sich innerhalb des <body> , jedoch über den <script> -Tags:

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

Lassen Sie uns nun mit der Datei main.js :

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

Probieren Sie diesen Code selbst aus. Wenn Sie die Seite öffnen, sollte der Browser um Erlaubnis bitten, und dann wird ein Bild von der Webcam auf dem Bildschirm angezeigt.

Später werden wir den Code der Funktion onStreaming() .

Gesichtssuche


Verwenden wir nun die Bibliothek clmtrackr.js, um nach Gesichtern im Video zu suchen. Initialisieren Sie zunächst das Gesichtsverfolgungssystem, indem Sie nach const video = ... den folgenden Code hinzufügen:

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

In der Funktion onStreaming() verbinden wir nun das Gesichtssuchsystem, indem wir dort den folgenden Befehl hinzufügen:

 ctrack.start(video); 

Das ist alles was wir brauchen. Jetzt kann das System das Gesicht im Videostream erkennen.

Glaubst du nicht? Zeichnen wir eine "Maske" um Ihr Gesicht, um sicherzustellen, dass dies wahr ist.
Dazu müssen wir das Bild über dem Element anzeigen, das für die Anzeige des Videos verantwortlich ist. Mit dem <canvas> können Sie etwas auf HTML-Seiten zeichnen. Daher erstellen wir ein solches Element, indem wir es dem Element überlagern, das das Video anzeigt. Der folgende Code hilft uns dabei, der der HTML-Datei unter dem bereits vorhandenen <video> -Element hinzugefügt werden muss:

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

Wenn Sie möchten, können Sie den Inline-Stil in eine separate CSS-Datei verschieben.

Hier haben wir der Seite ein <canvas> der gleichen Größe wie das <video> -Element hinzugefügt. Dass sich die Elemente an derselben Position befinden, wird durch die hier verwendeten Stile bereitgestellt.

Jedes Mal, wenn der Browser das nächste Bild des Videos anzeigt, zeichnen wir etwas auf das <canvas> . Die Ausführung eines beliebigen Codes während der Ausgabe jedes Frames erfolgt mithilfe des requestAnimationLoop() -Mechanismus. Bevor wir etwas an das <canvas> ausgeben, müssen wir das, was zuvor darauf war, entfernen und löschen. Wir können dann clmtrackr vorschlagen, um die Grafik direkt an das <canvas> auszugeben.

Hier ist der Code, der implementiert, worüber wir gerade gesprochen haben. Fügen Sie es unter dem Befehl 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); } } 

Rufen Sie nun die Funktion onStreaming() Funktion onStreaming() unmittelbar nach ctrack.start() . Diese Funktion plant selbst einen eigenen Neustart in jedem Frame.

Aktualisieren Sie die Seite und sehen Sie sich die Webcam an. Sie sollten im Videofenster eine grüne „Maske“ um Ihr Gesicht sehen. Manchmal muss der Kopf leicht im Rahmen bewegt werden, damit das System das Gesicht richtig erkennt.


Gesichtserkennungsergebnisse

Identifizieren Sie den Bereich des Bildes, der die Augen enthält


Jetzt müssen wir den rechteckigen Bereich des Bildes finden, in dem sich die Augen befinden, und ihn auf einem separaten <canvas> .

Glücklicherweise gibt uns cmltracker nicht nur Informationen über die Position des Gesichts, sondern auch 70 Kontrollpunkte. Wenn Sie sich die Dokumentation zu cmltracker ansehen, können Sie genau die Kontrollpunkte auswählen, die wir benötigen.


Kontrollpunkte

Wir entscheiden, dass die Augen der rechteckige Teil des Bildes sind, dessen Ränder die Punkte 23, 28, 24 und 26 berühren, die in jeder Richtung um 5 Pixel erweitert sind. Dieses Rechteck sollte alles enthalten, was für uns wichtig ist, es sei denn, der Benutzer neigt seinen Kopf zu stark.

Bevor wir dieses Fragment des Bildes verwenden können, benötigen wir ein weiteres <canvas> für seine Ausgabe. Die Abmessungen betragen 50 x 25 Pixel. Ein Rechteck mit Augen passt in dieses Element. Eine leichte Bildverformung ist kein Problem.

Fügen Sie diesen Code der HTML-Datei hinzu, die das <canvas> , das den Teil des Bildes enthält, der Augen hat:

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

Die folgende Funktion gibt die x und y Koordinaten sowie die Breite und Höhe des die Augen umgebenden Rechtecks ​​zurück. Als Eingabe nimmt es eine Reihe von positions die von clmtrackr empfangen wurden. Beachten Sie, dass jede von clmtrackr empfangene Koordinate die Komponenten x und y . Diese Funktion muss zu main.js hinzugefügt 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]; } 

Jetzt extrahieren wir in jedem Frame ein Rechteck mit Augen aus dem Videostream, kreisen es mit einer roten Linie auf dem <canvas> , das dem <video> -Element überlagert ist, und kopieren es dann in das neue <canvas> . Bitte beachten Sie, dass wir die Indikatoren resizeFactorX und resizeFactorY berechnen, um den von uns benötigten Bereich korrekt zu identifizieren.

Ersetzen Sie den if Block in der Funktion trackingLoop() durch den folgenden Code:

 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 ); } 

Nachdem Sie die Seite jetzt neu geladen haben, sollten Sie ein rotes Rechteck um die Augen sehen. Dieses Rechteck befindet sich im entsprechenden <canvas> . Wenn Ihre Augen größer als meine sind, experimentieren Sie mit der Funktion getEyeRectangle .


<canvas> -Element, das ein Rechteck mit dem Bild der Augen des Benutzers zeichnet

Datenerfassung


Es gibt viele Möglichkeiten, Daten zu sammeln. Ich habe mich entschieden, die Informationen zu verwenden, die über Maus und Tastatur abgerufen werden können. In unserem Projekt sieht die Datenerfassung so aus.

Der Benutzer bewegt den Cursor über die Seite, beobachtet ihn mit den Augen und drückt jedes Mal die auf der Tastatur, wenn das Programm ein weiteres Sample aufnehmen muss. Mit diesem Ansatz ist es einfach, schnell einen großen Datensatz für das Training des Modells zu erfassen.

▍Mausverfolgung


Um genau herauszufinden, wo sich der Mauszeiger auf der Webseite befindet, benötigen wir einen document.onmousemove Ereignishandler. Unsere Funktion normalisiert außerdem die Koordinaten so, dass sie in den Bereich [-1, 1] passen:

 //   : 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; 

▍ Bilderfassung


Um das vom <canvas> angezeigte Bild zu erfassen und als Tensor zu speichern, bietet TensorFlow.js die tf.fromPixels() . Wir verwenden es, um das Bild aus dem <canvas> zu speichern und dann zu normalisieren, das ein Rechteck mit den Augen des Benutzers anzeigt:

 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)); }); } 

Beachten Sie, dass die Funktion tf.tidy() verwendet wird, um nach Abschluss zu bereinigen.

Wir könnten einfach alle Proben in einem großen Trainingssatz speichern. Beim maschinellen Lernen ist es jedoch wichtig, die Qualität des Modelltrainings zu überprüfen. Aus diesem Grund müssen wir einige Proben in einer separaten Kontrollprobe speichern. Danach können wir das Verhalten des Modells anhand neuer Daten überprüfen und feststellen, ob das Modell übermäßig trainiert wurde. Zu diesem Zweck sind 20% der Gesamtzahl der Proben in der Kontrollprobe enthalten.

Hier ist der Code, der zum Sammeln von Daten und Beispielen verwendet wird:

 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; }); } 

Und schließlich müssen wir diese Funktion an die binden:

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

Jedes Mal, wenn Sie die , werden das Augenbild und die Koordinaten des Mauszeigers zu einem der Datensätze hinzugefügt.

Modelltraining


Erstellen Sie ein einfaches neuronales Faltungsnetzwerk. TensorFlow.js bietet zu diesem Zweck eine API, die an Keras erinnert. Das Netzwerk sollte eine conv2d Schicht, eine maxPooling2d Schicht und schließlich eine dense Schicht mit zwei Ausgabewerten haben (sie repräsentieren die Bildschirmkoordinaten). Unterwegs habe ich dem Netzwerk eine dropout Ebene und eine flatten als Regularisierer hinzugefügt, um zweidimensionale Daten in eindimensionale zu konvertieren. Das Netzwerktraining wird mit dem Adam-Optimierer durchgeführt.

Bitte beachten Sie, dass ich mich nach dem Experimentieren mit meinem MacBook Air für die hier verwendeten Netzwerkeinstellungen entschieden habe. Sie können auch Ihre eigene Konfiguration des Modells wählen.

Hier ist der Modellcode:

 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; } 

Bevor wir mit dem Training des Netzwerks beginnen, legen wir eine feste Anzahl von Epochen und eine variable Paketgröße fest (da wir wahrscheinlich mit sehr kleinen Datenmengen arbeiten werden).

 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], }); } 

Fügen Sie nun der Seite eine Schaltfläche hinzu, um mit dem Lernen zu beginnen. Dieser Code geht in die HTML-Datei:

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

Dieser Code muss der JS-Datei hinzugefügt werden:

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

Wo sucht der Benutzer?


Nachdem wir die Daten sammeln und das Modell vorbereiten können, können wir beginnen, die Stelle auf der Seite vorherzusagen, an der der Benutzer sucht. Wir zeigen mit Hilfe eines grünen Kreises auf diesen Ort, der sich auf dem Bildschirm bewegt.

Fügen Sie der Seite zunächst einen Kreis hinzu:

 <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> 

Um es auf der Seite zu verschieben, senden wir regelmäßig das aktuelle Bild der Augen des neuronalen Netzwerks und stellen ihr eine Frage, wohin der Benutzer schaut. Das Antwortmodell erzeugt zwei Koordinaten, entlang derer der Kreis verschoben werden soll:

 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); 

Ich habe das Intervall auf 100 Millisekunden eingestellt. Wenn Ihr Computer nicht so leistungsfähig ist wie meiner, können Sie ihn vergrößern.

Zusammenfassung


Jetzt haben wir alles, was wir brauchen, um die am Anfang dieses Materials vorgestellte Idee umzusetzen. Erleben Sie, was wir getan haben. Bewegen Sie den Mauszeiger seinen Augen und drücken Sie die Leertaste. Klicken Sie dann auf die Schaltfläche Training starten.

Sammeln Sie weitere Daten und klicken Sie erneut auf die Schaltfläche. Nach einer Weile beginnt sich der grüne Kreis nach Ihrem Blick auf dem Bildschirm zu bewegen. Zuerst ist es nicht besonders gut, an den Ort zu gelangen, an dem Sie suchen, aber ab etwa 50 gesammelten Proben nach mehreren Trainingsphasen, und wenn Sie Glück haben, bewegt es sich ziemlich genau zu dem Punkt auf der Seite, den Sie betrachten . Den vollständigen Code des in diesem Material analysierten Beispiels finden Sie hier .

Obwohl das, was wir getan haben, bereits sehr interessant aussieht, können noch viele Verbesserungen vorgenommen werden. Was ist, wenn der Benutzer seinen Kopf bewegt oder seine Position vor der Kamera ändert? Unser Projekt würde die Möglichkeiten hinsichtlich der Auswahl der Größe, Position und des Winkels des Rechtecks, das den Bildbereich begrenzt, in dem sich die Augen befinden, nicht beeinträchtigen. Tatsächlich sind in der Vollversion des hier diskutierten Beispiels einige zusätzliche Funktionen implementiert. Hier sind einige davon:

  • Die oben beschriebenen Optionen zum Anpassen des augenbegrenzenden Rechtecks.
  • Konvertieren Sie ein Bild in Graustufen.
  • Verwenden von CoordConv .
  • Eine Heatmap, mit der überprüft werden kann, wo das Modell eine gute Leistung erbracht hat und wo nicht.
  • Möglichkeit zum Speichern und Laden von Datensätzen.
  • Möglichkeit zum Speichern und Laden von Modellen.
  • Erhaltung der Gewichte, die nach dem Training einen minimalen Trainingsverlust zeigten.
  • Verbesserte Benutzeroberfläche mit kurzen Anweisungen für die Arbeit mit dem System.

Liebe Leser! Verwenden Sie TensorFlow?

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


All Articles