TensorFlow.js y clmtrackr.js: seguimiento de la dirección de la mirada del usuario en el navegador

El autor del artículo, cuya traducción estamos publicando, ofrece hablar sobre la resolución de problemas desde el campo de la visión por computadora exclusivamente mediante un navegador web. Resolver este problema no es tan difícil gracias a la biblioteca de JavaScript TensorFlow . En lugar de entrenar nuestro propio modelo y ofrecerlo a los usuarios como parte del producto terminado, les daremos la oportunidad de recopilar datos de forma independiente y entrenar el modelo directamente en un navegador, en nuestra propia computadora. Con este enfoque, el procesamiento de datos del lado del servidor es completamente innecesario.


Puede experimentar lo que este material está dedicado a crear aquí . Necesitará un navegador moderno, cámara web y mouse para esto. Aquí está el código fuente del proyecto. No está diseñado para trabajar en dispositivos móviles, el autor del material dice que no tuvo tiempo para las mejoras apropiadas. Además, señala que la tarea considerada aquí se volverá más complicada si tiene que procesar la transmisión de video desde una cámara en movimiento.

Idea


Usemos la tecnología de aprendizaje automático para descubrir exactamente dónde mira el usuario cuando mira una página web. Hacemos esto mirando sus ojos usando una cámara web.

Es muy fácil acceder a la cámara web en el navegador. Si suponemos que toda la imagen de la cámara se utilizará como entrada a la red neuronal, entonces podemos decir que es demasiado grande para estos fines. El sistema tendrá que hacer mucho trabajo solo para determinar el lugar en la imagen donde están los ojos. Este enfoque puede mostrarse bien si estamos hablando de un modelo que el desarrollador entrena solo e implementa en el servidor, pero si estamos hablando de capacitación y uso del modelo en un navegador, esto es demasiado.

Para facilitar la tarea de la red, podemos proporcionarle solo una parte de la imagen, la que contiene los ojos del usuario y un área pequeña a su alrededor. Esta área, que es un rectángulo que rodea los ojos, se puede identificar utilizando una biblioteca de terceros. Por lo tanto, la primera parte de nuestro trabajo se ve así:


Entrada de cámara web, reconocimiento de rostros, detección de ojos, imagen recortada

Para detectar la cara en la imagen, utilicé una biblioteca llamada clmtrackr . No es perfecto, pero difiere en tamaño pequeño, buen rendimiento y, en general, hace frente a su tarea con dignidad.

Si se utiliza una imagen pequeña pero seleccionada de manera inteligente como entrada para una red neuronal convolucional simple, la red puede aprender sin ningún problema. Así es como se ve este proceso:


La imagen de entrada, el modelo es una red neuronal convolucional, coordenadas, el lugar predicho por la red en la página donde está buscando el usuario.

Aquí se describirá una implementación mínima totalmente funcional de las ideas discutidas en esta sección. El proyecto, cuyo código está en este repositorio, tiene muchas características adicionales.

Preparación


Para comenzar, clmtrackr.js del repositorio apropiado. Comenzaremos el trabajo con un archivo HTML vacío, que importa jQuery, TensorFlow.js, clmtrackr.js y el archivo main.js con nuestro código, en el que trabajaremos un poco más tarde:

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

Reciba una transmisión de video desde una cámara web


Para activar la cámara web y mostrar la transmisión de video en la página, necesitamos obtener el permiso del usuario. Aquí no proporciono código que resuelva los problemas de compatibilidad del proyecto con varios navegadores. Partiremos de la suposición de que nuestros usuarios trabajan en Internet utilizando la última versión de Google Chrome.

Agregue el siguiente código al archivo HTML. Debe ubicarse dentro de la <body> , pero encima de las etiquetas <script> :

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

Ahora main.js con el archivo main.js :

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

Prueba este código por tu cuenta. Cuando abre la página, el navegador debe pedir permiso, y luego aparecerá una imagen de la cámara web en la pantalla.

Más adelante onStreaming() el código de la función onStreaming() .

Búsqueda de caras


Ahora usemos la biblioteca clmtrackr.js para buscar caras en el video. Primero, inicialice el sistema de seguimiento facial agregando el siguiente código después de const video = ... :

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

Ahora, en la función onStreaming() , conectamos el sistema de búsqueda facial agregando allí el siguiente comando:

 ctrack.start(video); 

Eso es todo lo que necesitamos. Ahora el sistema podrá reconocer la cara en la transmisión de video.

No lo creo? Dibujemos una "máscara" alrededor de su cara para asegurarnos de que esto sea cierto.
Para hacer esto, necesitamos mostrar la imagen en la parte superior del elemento responsable de mostrar el video. Puede dibujar algo en páginas HTML utilizando la <canvas> . Por lo tanto, crearemos dicho elemento superponiéndolo en el elemento que muestra el video. El siguiente código nos ayudará con esto, que debe agregarse al archivo HTML bajo el elemento <video> que ya está allí:

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

Si lo desea, puede mover el estilo en línea a un archivo CSS separado.

Aquí agregamos un <canvas> mismo tamaño a la página que el elemento <video> . El hecho de que los elementos se ubicarán en la misma posición está garantizado por los estilos utilizados aquí.

Ahora, cada vez que el navegador muestre el siguiente cuadro del video, vamos a dibujar algo en el <canvas> . La ejecución de cualquier código durante la salida de cada trama se realiza utilizando el mecanismo requestAnimationLoop() . Antes de enviar algo al <canvas> , necesitamos eliminar de él lo que tenía antes, borrarlo. Luego podemos sugerir clmtrackr para generar el gráfico directamente en el <canvas> .

Aquí está el código que implementa lo que acabamos de hablar. ctrack.init() debajo del 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); } } 

Ahora llame a la función trackingLoop() en la función onStreaming() inmediatamente después de ctrack.start() . Esta función planificará su propio reinicio en cada cuadro.

Actualiza la página y mira la cámara web. Debería ver una "máscara" verde alrededor de su cara en la ventana de video. A veces, para que el sistema reconozca correctamente la cara, debe mover ligeramente la cabeza en el marco.


Resultados de reconocimiento facial

Identificar el área de la imagen que contiene los ojos.


Ahora necesitamos encontrar el área rectangular de la imagen en la que se encuentran los ojos y colocarla en un <canvas> separado.

Afortunadamente, cmltracker nos brinda no solo información sobre la ubicación de la cara, sino también 70 puntos de control. Si mira la documentación de cmltracker, puede seleccionar exactamente los puntos de control que necesitamos.


Puntos de control

Decidimos que los ojos son la parte rectangular de la imagen, cuyos bordes tocan los puntos 23, 28, 24 y 26, expandidos en 5 píxeles en cada dirección. Este rectángulo debe incluir todo lo que es importante para nosotros, a menos que el usuario incline demasiado la cabeza.

Ahora, antes de que podamos usar este fragmento de la imagen, necesitamos un <canvas> para su salida. Sus dimensiones serán de 50x25 píxeles. Un rectángulo con ojos encajará en este elemento. La ligera deformación de la imagen no es un problema.

Agregue este código al archivo HTML que describe el <canvas> , que incluirá la parte de la imagen que tiene ojos:

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

La siguiente función devolverá las coordenadas y , así como el ancho y la altura del rectángulo que rodea los ojos. Como entrada, toma una serie de positions recibidas de clmtrackr. Tenga en cuenta que cada coordenada recibida de clmtrackr tiene componentes x e y . Esta función debe agregarse a 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]; } 

Ahora, en cada cuadro, vamos a extraer un rectángulo con ojos del flujo de video, rodearlo con una línea roja en el <canvas> , que se superpone en el elemento <video> , y luego copiarlo al nuevo <canvas> . Tenga en cuenta que para identificar correctamente el área que necesitamos, calcularemos los indicadores resizeFactorX y resizeFactorY .

Reemplace el bloque if en la función trackingLoop() con el siguiente código:

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

Después de volver a cargar la página ahora, debería ver un rectángulo rojo alrededor de los ojos, y lo que contiene este rectángulo está en el <canvas> correspondiente. Si tus ojos son más grandes que los míos, experimenta con la función getEyeRectangle .


Elemento <canvas> que dibuja un rectángulo que contiene la imagen de los ojos del usuario

Recogida de datos


Hay muchas formas de recopilar datos. Decidí usar la información que se puede obtener del mouse y el teclado. En nuestro proyecto, la recopilación de datos se ve así.

El usuario mueve el cursor por la página y lo mira con los ojos, presionando la en el teclado cada vez que el programa necesita grabar otra muestra. Con este enfoque, es fácil recopilar rápidamente un gran conjunto de datos para entrenar el modelo.

▍ Seguimiento de ratón


Para saber exactamente dónde se encuentra el puntero del mouse en la página web, necesitamos un controlador de eventos document.onmousemove . Nuestra función, además, normaliza las coordenadas para que encajen en el rango [-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; 

▍ Captura de imagen


Para capturar la imagen mostrada por el <canvas> y guardarla como un tensor, TensorFlow.js ofrece la función auxiliar tf.fromPixels() . Lo usamos para guardar y luego normalizar la imagen del <canvas> que muestra un rectángulo que contiene los ojos del usuario:

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

Tenga en cuenta que la función tf.tidy() se usa para limpiar una vez completada.

Podríamos guardar todas las muestras en un conjunto de entrenamiento grande, sin embargo, en el aprendizaje automático es importante verificar la calidad del entrenamiento del modelo. Es por eso que necesitamos guardar algunas muestras en una muestra de control separada. Después de eso, podemos verificar el comportamiento del modelo con los nuevos datos y averiguar si el modelo ha sido sobreentrenado. Para este propósito, el 20% del número total de muestras se incluye en la muestra de control.

Aquí está el código utilizado para recopilar datos y muestras:

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

Y finalmente, necesitamos vincular esta función a la :

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

Ahora, cada vez que presiona la , la imagen del ojo y las coordenadas del puntero del mouse se agregan a uno de los conjuntos de datos.

Entrenamiento modelo


Crea una red neuronal convolucional simple. TensorFlow.js proporciona una API que recuerda a Keras para este propósito. La red debe tener una capa conv2d , una capa maxPooling2d y, finalmente, una capa dense con dos valores de salida (representan las coordenadas de la pantalla). En el camino, agregué una capa de dropout y una capa flatten a la red, como regularizador, para convertir los datos bidimensionales en unidimensionales. La capacitación en red se realiza utilizando el optimizador Adam.

Tenga en cuenta que me decidí por la configuración de red utilizada aquí después de experimentar con mi MacBook Air. Bien puede elegir su propia configuración del modelo.

Aquí está el código del modelo:

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

Antes de comenzar a entrenar la red, establecemos un número fijo de eras y un tamaño de paquete variable (ya que probablemente trabajaremos con conjuntos de datos muy pequeños).

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

Ahora agregue un botón a la página para comenzar a aprender. Este código va al archivo HTML:

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

Este código debe agregarse al archivo JS:

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

¿Dónde está buscando el usuario?


Ahora que podemos recopilar los datos y preparar el modelo, podemos comenzar a predecir el lugar en la página donde está buscando el usuario. Señalamos este lugar con la ayuda de un círculo verde, que se mueve por la pantalla.

Primero, agregue un círculo a la página:

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

Para moverlo por la página, transmitimos periódicamente la imagen actual de los ojos de la red neuronal y le hacemos una pregunta sobre dónde está mirando el usuario. El modelo en respuesta produce dos coordenadas a lo largo de las cuales se debe mover el círculo:

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

Configuré el intervalo en 100 milisegundos. Si su computadora no es tan poderosa como la mía, puede decidir ampliarla.

Resumen


Ahora tenemos todo lo que necesitamos para implementar la idea presentada al comienzo de este material. Experimenta lo que hemos hecho. Mueva el cursor del mouse, siguiendo sus ojos, y presione la barra espaciadora. Luego haz clic en el botón de inicio de entrenamiento.

Recopile más datos, haga clic en el botón nuevamente. Después de un tiempo, el círculo verde comenzará a moverse alrededor de la pantalla después de su mirada. Al principio, no será particularmente bueno llegar al lugar donde está buscando, pero, comenzando con unas 50 muestras recolectadas, después de varias etapas de entrenamiento, y si tiene suerte, se moverá con bastante precisión al punto de la página que está viendo. . El código completo del ejemplo analizado en este material se puede encontrar aquí .

Aunque lo que hicimos ya parece bastante interesante, todavía hay muchas mejoras que se pueden hacer. ¿Qué sucede si el usuario mueve la cabeza o cambia su posición frente a la cámara? Nuestro proyecto no afectaría las posibilidades con respecto a la selección del tamaño, la posición y el ángulo del rectángulo que limita el área de la imagen en la que se encuentran los ojos. De hecho, se implementan bastantes características adicionales en la versión completa del ejemplo discutido aquí. Aquí hay algunos de ellos:

  • Las opciones para personalizar el rectángulo llamativo descrito anteriormente.
  • Convierte una imagen a escala de grises.
  • Usando CoordConv .
  • Un mapa de calor para comprobar dónde funcionó bien el modelo y dónde no.
  • Posibilidad de guardar y cargar conjuntos de datos.
  • Posibilidad de guardar y cargar modelos.
  • Preservación de los pesos que mostraron una pérdida mínima de entrenamiento después del entrenamiento.
  • Interfaz de usuario mejorada con breves instrucciones para trabajar con el sistema.

Estimados lectores! ¿Utiliza TensorFlow?

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


All Articles