Aprendizaje automático sin codificación para tareas front-end. Veamos cómo funciona el marco MediaPipe.

El desarrollo front-end moderno no se trata solo de interfaces de usuario. Contamos con una potente API web, WebGL, WebAssembly y muchas bibliotecas y marcos para resolver problemas no triviales. Estas herramientas le permiten utilizar gráficos 2D y 3D, VR/AR en navegadores, así como participar en aprendizaje automático, visión por computadora y no limitarse a lo que ya sabemos y podemos hacer.

Mi nombre es Yaroslav Frantsuzyak y soy desarrollador front-end en GARPIX. En este artículo hablaré sobre una herramienta como el marco MediaPipe de Google. Basado en modelos prefabricados, permite a los desarrolladores implementar funciones complejas de procesamiento multimedia y visión por computadora en aplicaciones web: reconocimiento facial, análisis de imágenes, seguimiento de movimiento, procesamiento de video en tiempo real y mucho más. Analizaremos el trabajo del marco usando un ejemplo, nos sumergiremos en las matemáticas vectoriales en el espacio tridimensional y la complejidad de reconocer puntos clave de una cara.

Para un proyecto de tutorial, utilicé MediaPipe para crear un sistema de seguimiento ocular a través de una cámara web. El objetivo era controlar la atención de los estudiantes siguiendo la dirección de su mirada mientras leían un libro de texto. Si todos los materiales educativos se presentan en formato electrónico, durante el proceso de lectura se puede observar el comportamiento: qué lee el alumno, a qué velocidad, qué secciones se salta y si vuelve a lo leído. Con este enfoque, es conveniente que el docente calcule métricas y, en base a ellas, reciba información sobre la participación del alumno, la integridad del material dominado y, posiblemente, decida si lo admite al examen.

Me enfrenté a un problema similar al que se hace en marketing y en investigación de UX cuando se estudia el comportamiento del usuario. A partir de una variedad de datos recopilados en recursos web, construyen gráficos de fijación de los ojos, donde la ruta de los ojos a lo largo de los elementos de la página es visible y los mapas de calor resaltan los lugares de mayor interés.

A partir de estos gráficos se puede entender a qué contenido prestan atención los usuarios y si encuentran problemas en la interfaz, por ejemplo, al buscar el botón correcto. La experiencia del usuario también se puede monitorear y mejorar en beneficio de la empresa.

Todo lo que quedaba era encontrar una herramienta que me permitiera crear una solución a mi problema.

La experiencia de las grandes empresas en la solución de este tipo de problemas suele reducirse a las condiciones de laboratorio y al uso de equipos especiales y costosos. Solo tenía una computadora portátil con cámara web integrada, así que comencé a buscar una solución entre las tecnologías web. Y lo encontré.

Herramientas: marco MediaPipe

MediaPipe es un marco multiplataforma de Google que consta de muchos modelos de aprendizaje automático entrenados. Tiene tres grupos principales de modelos para trabajar con texto, sonido e imágenes.

Para trabajar con imágenes, se encuentran disponibles modelos para reconocimiento de patrones, segmentación de objetos, detección de gestos con las manos, posición del cuerpo en el espacio y otros. Entre ellos, nos interesa el modelo de reconocimiento de puntos clave. Con su ayuda, puedes recrear un modelo tridimensional del rostro de una persona.

Imaginemos el problema de determinar la mirada del usuario en la pantalla: hay una pantalla, una cámara web y un observador.

Si determinamos el vector de dirección de visión, que se puede representar como un rayo, y la posición de la pantalla con respecto al observador, podemos encontrar el punto de intersección del rayo con la pantalla.

Imaginemos todos los objetos de esta escena en un espacio tridimensional y midámoslos.

MediaPipe ayudará con el modelo tridimensional de la cara, pero todo lo demás deberá tratarse por separado. En este artículo simplificaremos la tarea: buscaremos la dirección de la cabeza, no la mirada.

Herramientas: motor Three.js

Además de MediaPipe, necesitaremos Three.js, un motor 3D basado en WebGL. No existe una necesidad estricta de utilizarlo, es engorroso y no utilizaremos la mayoría de sus capacidades. Implementa las matemáticas necesarias y con su ayuda conviene demostrar la lógica del sistema.

No entraremos en todos los conceptos de gráficos 3D o en cómo funciona Three.js, pero hablaré mucho sobre “vector”. Un vector es un segmento dirigido caracterizado por su longitud y ángulo. Si nos alejamos del álgebra lineal, entonces un vector puede ser simplemente un punto en un sistema de coordenadas y, por ejemplo, las dimensiones de un rectángulo y, de hecho, cualquier conjunto ordenado de números.

Three.js proporciona métodos declarativos para sumar, restar, multiplicar, dividir vectores y encontrar la distancia entre ellos.

Diseño de un sistema de seguimiento ocular

Así, el sistema constará de tres módulos principales.

  • CaraCámara — módulo para trabajar con la cámara. Inicializa la transmisión de video y determina algunas características del espacio interno de la cámara.

  • rastreador facial — módulo de seguimiento facial. Aquí se detectan los puntos clave, se determina la dirección de la cabeza y se calculan los puntos de intersección del rayo de dirección con el plano.

  • Controles faciales — el módulo resultante, que calcula las coordenadas finales de un punto en la pantalla.

Veamos estos módulos en orden. Empecemos por la cámara.

Módulo de cámara facial

Aquí inicializamos la transmisión de video a través de parámetros.

class FaceCamera {

  ...

  async init() {

    const stream = await navigator.mediaDevices.getUserMedia({

      video: {

        facingMode: { ideal: 'user' },

        width: { ideal: 4096 },

        height: { ideal: 2160 },

      },

    })

    this.video.srcObject = stream

    await this.video.play()

  }

}

Te informamos que queremos recibir vídeo de la cámara frontal y configurar la resolución ideal en 4k. Si el dispositivo no admite dicha resolución, se instalará lo más cerca posible de ella.

Transferimos el flujo de vídeo al elemento de vídeo para que reciba la imagen de la cámara e iniciamos la transmisión.

class FaceCamera {
  ...

  async init() {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: {
          facingMode: { ideal: 'user' },
          width: { ideal: 4096 },
          height: { ideal: 2160 },
      },
    })

    this.video.srcObject = stream

    await this.video.play()
  }
}

Especificaciones de la cámara

Entre los parámetros disponibles nos interesa, en primer lugar, ancho Y altura la imagen resultante. En base a ellos podemos calcular diagonal. ¿Qué otra cosa? Para responder, averigüemos cómo funciona la cámara desde el interior.

Los rayos de luz atraviesan la lente y se proyectan sobre la matriz. La distancia recorrida por el haz desde la lente hasta la matriz se denomina “focal”. Depende de la diagonal de la matriz y del ángulo de visión diagonal de la lente. Conociendo la distancia focal y las dimensiones del rostro, podemos calcular la distancia real entre el rostro y la cámara, y esto es necesario para calcular el punto de intersección del haz con la pantalla.

De cara al futuro, en lugar de determinar la posición de los objetos en el espacio real, propongo transferir todo al espacio interno de la cámara. Entonces la distancia entre el rostro y la cámara siempre corresponderá a la distancia focal, pero el tamaño de la pantalla cambiará proporcionalmente, dependiendo del acercamiento o distancia de la cabeza a la cámara. A continuación te mostraré cómo funciona.

Desafortunadamente, no podemos obtener el ángulo de visión de la lente mediante programación; el usuario debe informarnos. Puede que lo sepa, por ejemplo, por las características técnicas de la cámara, pero si no están ahí, tendrá que adivinar y calibrar.

Entonces, agreguemos varios captadores para calcular los parámetros necesarios de la cámara.

class FaceCamera { ...
  get width() {
    return this.video.videoWidth
  }

  get height() {
    return this.video.videoHeight
  }

  get aspectRatio() {
    return this.width / this.height
  }

  get diagonal() {
    return Math.hypot(this.width, this.height)
  }

  get focalLength() {
    return (this.diagonal / 2) * (1 / Math.tan(this.diagonalFov / 2))
  }
}

En esta etapa, la escena 3D parece un poco vacía. Lo iremos llenando poco a poco.

Imaginemos que la cámara está fija con respecto a un plano infinito abstracto; es con este plano que determinaremos el punto de intersección del rayo. Este plano está dirigido hacia la cabeza, que dibujaremos próximamente, y se encuentra a una distancia focal de ella.

class FaceCamera { ...
  plane!: THREE.Plane

  async init() {
    ...

    this.plane = new THREE.Plane(
      new THREE.Vector3(0, 0, -1), this.focalLength
    )
  }
}

Ahora trabajemos con el módulo de seguimiento facial.

Módulo de seguimiento facial

En el módulo de seguimiento declaramos un conjunto de archivos que contienen código compilado en WebAssembly para trabajar con el modelo.

import { FilesetResolver, FaceLandmarker } from '@mediapipe/tasks-vision'

class FaceTracker {

  landmarker!: FaceLandmarker

  async init() {

    const wasmFileset = await FilesetResolver.forVisionTasks('./wasm')

    this.landmarker = await FaceLandmarker.createFromOptions(wasmFileset, {

      baseOptions: {

        modelAssetPath: './face_landmarker.task',

        delegate: 'GPU',

      },

      runningMode: 'VIDEO',

      numFaces: 1,

      outputFacialTransformationMatrixes: true,

      outputFaceBlendshapes: false,

    })

  }

}

También creamos una instancia de la clase FaceLandmarker, que toma la ruta al archivo con el modelo y un conjunto de parámetros. Veámoslos con más detalle:

  • delegar determina qué dispositivo realizará los cálculos: el procesador central o el procesador de gráficos. En mi experiencia, la GPU procesa más rápido, pero no puedo garantizarlo para todos los dispositivos.

  • modo de ejecución Determina si el modelo está trabajando con una imagen estática o un vídeo. En nuestro caso, este es un video.

  • numCaras Indica cuántas caras se pueden identificar en una imagen. Es decir, el sistema podría ser utilizado por varias personas al mismo tiempo, y a partir de ello se puede crear algo interesante. Sólo una advertencia, cuantas más caras, más recursos requerirá el modelo, lo que obviamente afectará al rendimiento.

  • salidaFacialTransformaciónMatrices — una bandera que, cuando se activa, además de los puntos de control, proporciona una matriz de transformación. Esta matriz contiene información sobre cómo se desplaza, escala y gira la cara en relación con su posición original. En la posición inicial, la cara simplemente mira hacia adelante.

En los gráficos tridimensionales conviene representar todo en forma de matrices para poder realizar sobre ellas operaciones aritméticas, invertir y transponer.

  • salidaFaceBlendshapes — una bandera interesante, pero en nuestro ejemplo no será necesaria. Si está activo, el modelo mostrará muchos parámetros que caracterizan las expresiones faciales: la boca abierta o cerrada, los ojos, si las cejas están levantadas. Usando estas características, puedes dar vida a avatares 3D especiales, así como reconocer emociones humanas.

El método de actualización de la clase FaceTracker toma una instancia de cámara para pasar un cuadro de video al método de detección de puntos clave sincrónicos. Si tiene éxito, el método devolverá un conjunto de puntos detectados, cada uno con coordenadas x, y y z, pero con valores normalizados. Esto significa que tienen un valor de cero a uno, y para obtener la dimensión habitual en píxeles, es necesario multiplicar las coordenadas x y z por el ancho de la imagen y la coordenada y por la altura.

Posición de caras en una escena 3D.

Y aquí en la escena 3D se puede ver una cara: está en la esquina, porque la posición del centro de la imagen no coincide con la posición del centro de la escena.

import * as THREE from 'three'

class FaceTracker { ...

  points = Array.from({ length: 468 + 10 }, () => new THREE.Vector3())

  update(camera: FaceCamera) {

    const { faceLandmarks: (landmarks) } =

      this.landmarker.detectForVideo(camera.video, performance.now())

    if (!landmarks) return

    landmarks.forEach(({ x, y, z }, i) => {

      this.points(i).set(

        ( x - 0.5 ) * camera.width  *  1,

        ( y - 0.5 ) * camera.height * -1,

        ( z       ) * camera.width  * -1

      )

    })

  }

}

Cambiemos las coordenadas de los puntos de control a lo largo de los ejes horizontal y vertical a la mitad del tamaño de la imagen.

import * as THREE from 'three'

class FaceTracker { ...

  points = Array.from({ length: 468 + 10 }, () => new THREE.Vector3())

  update(camera: FaceCamera) {

    const { faceLandmarks: (landmarks) } =

      this.landmarker.detectForVideo(camera.video, performance.now())

    if (!landmarks) return

    landmarks.forEach(({ x, y, z }, i) => {

      this.points(i).set(

        ( x - 0.5 ) * camera.width  *  1,

        ( y - 0.5 ) * camera.height * -1,

        ( z       ) * camera.width  * -1

      )

    })

  }

}

Ha mejorado, pero la cara está al revés y apuntando en dirección opuesta a la cámara. Para solucionar este problema, debe invertir los ejes Y y Z.

import * as THREE from 'three'

class FaceTracker { ...

  points = Array.from({ length: 468 + 10 }, () => new THREE.Vector3())

  update(camera: FaceCamera) {

    const { faceLandmarks: (landmarks) } =

      this.landmarker.detectForVideo(camera.video, performance.now())

    if (!landmarks) return

    landmarks.forEach(({ x, y, z }, i) => {

      this.points(i).set(

        ( x - 0.5 ) * camera.width  *  1,

        ( y - 0.5 ) * camera.height * -1,

        ( z       ) * camera.width  * -1

      )

    })

  }

}

La cara ahora se muestra correctamente.

Por cierto el campo agujas La clase FaceTracker es un conjunto de vectores tridimensionales, su número es siempre constante e igual a la suma: 468 puntos en la cara y 10 puntos que describen la forma del iris.

dirección de la cabeza

Para determinar la dirección de la cabeza, agregue dos campos a FaceTracker:

  • transformar — una matriz de búfer de 4×4 en la que colocamos los valores de la matriz de transformación.

  • dirección — vector de dirección de la cabeza. Su valor es igual a la tercera columna de la matriz.

class FaceTracker { ...

  transform = new THREE.Matrix4()

  direction = new THREE.Vector3()

  update(camera: FaceCamera) {

    const {

      faceLandmarks: (landmarks),

      facialTransformationMatrixes: (transformationMatrix),

    } = this.landmarker.detectForVideo(...)

    ...

    this.transform.fromArray(transformationMatrix.data)

    this.direction.setFromMatrixColumn(this.transform, 2)

  }

}

Cada columna de la matriz de transformación, excepto la última, muestra la dirección de la cabeza a lo largo de tres ejes. El tercer eje Z está dirigido hacia nosotros y corresponde a la dirección hacia la que apunta la cabeza.

Cuando haya decidido la dirección de la cara, debe imaginar su encarnación física.

modelo de cara

Dado que estamos siguiendo la dirección de la cabeza, debemos disparar el haz de dirección desde un punto ubicado exactamente entre los ojos. 168 es el índice del punto entre los ojos.

class FaceTracker {

  ...

  ray = new THREE.Ray()

  update(camera: FaceCamera) {

    ...

    this.ray.set(

      this.points(168), this.direction

    )

  }

}

Todos los índices de puntos se almacenan en el repositorio de MediaPipe, en un archivo con un modelo de cara canónico. Puedes abrirlo, por ejemplo, en Blender y habilitar la visualización de índices.

Ahora calculemos el punto de intersección del rayo con el plano abstracto respecto al cual está fija la cámara.

class FaceTracker {

  ...

  ray = new THREE.Ray()

  intersection = new THREE.Vector3()

  update(camera: FaceCamera) {

    ...

    this.ray.intersectPlane(

      camera.plane,

      this.intersection

    )

  }

}

En la figura de arriba se puede ver cómo el rayo atraviesa el avión. En este punto mostramos la esfera, que será el punto de intersección que necesitamos. Ahora necesitas transferir este punto al sistema de coordenadas de la pantalla. Y transfiera la pantalla al sistema de coordenadas de la cámara. Para hacer esto, necesita conocer las dimensiones reales de la pantalla (píxeles) en unidades de medida convenientes (milímetros).

Transferir la pantalla al sistema de coordenadas de la cámara.

Le pedimos al usuario que nos diga la diagonal de la pantalla y obtenga el resultado en pulgadas. Para convertir pulgadas a píxeles, utilizamos un parámetro llamado PPI (densidad de píxeles). Este valor muestra cuántos píxeles hay por pulgada.

Es inconveniente trabajar con pulgadas, así que convirtámoslas a milímetros. Pero surge la pregunta: ¿cómo convertir milímetros en píxeles del espacio de la cámara? Para hacer esto, necesita conocer las dimensiones de algún objeto real en milímetros y píxeles. Y en el rostro humano existe tal objeto, este es el iris del ojo. Su diámetro es aproximadamente el mismo en todos los adultos y mide en promedio doce milímetros. Entonces, teniendo el tamaño real del iris en milímetros y píxeles, podemos encontrar su proporción. De ahora en adelante lo llamaremos “coeficiente de iris”, al multiplicarlo por el cual los milímetros se convertirán en píxeles.

Disponemos de 10 puntos de control que se encargan de la forma del iris. Por lo tanto, en la clase FaceTracker agregaremos un captador que devuelve su ancho promedio.

class FaceTracker {

  ...

  get irisWidthInPx() {

    const rightIrisWidth =

      this.points(469).distanceTo(this.points(471))

    

    const leftIrisWidth =

      this.points(474).distanceTo(this.points(476))

    return (rightIrisWidth + leftIrisWidth) / 2

  }

}

Módulo de controles faciales

En el módulo FaceControls resultante, obtenemos el tamaño real del iris en milímetros, que por defecto es doce, y la diagonal de la pantalla en pulgadas. A continuación, calculamos las dimensiones reales de la pantalla en milímetros utilizando el valor de ppMm.

type FaceControlsConfig = {

  ... irisWidth?: number; screenDiagonal: number }

class FaceControls { ...

  irisWidth: number

  screenHalfScaleReal: THREE.Vector3

  constructor({ ..., irisWidth = 12, screenDiagonal }) { ...

    this.irisWidth = irisWidth

    const ppMm =

      Math.hypot(window.screen.width, window.screen.height) /

      (screenDiagonal * 2.54 * 10)

    this.screenHalfScaleReal = new THREE.Vector3(

      window.screen.width  / 2 / ppMm,

      window.screen.height / 2 / ppMm

    )

  }

}

Coeficiente del iris y posición central de la pantalla.

Agreguemos un captador a la clase para obtener el coeficiente del iris. En un bucle sin fin, determinaremos las dimensiones reales de la pantalla y las reduciremos a los píxeles del espacio de la cámara. Para hacer esto, multiplique el tamaño de la pantalla por el coeficiente del iris. Y también determinaremos la posición del centro de la pantalla.

class FaceControls { ...
  screen = {
    halfScale: new THREE.Vector3(),
    center: new THREE.Vector3(),
  }

  get irisRatio() {
    return this.tracker.irisWidthInPx / this.irisWidth
  }

  loop() { ...
    this.screen.halfScale
      .copy(this.screenHalfScaleReal)
      .multiplyScalar(this.irisRatio)

    this.screen.center.setY(-this.screen.halfScale.y)
  }
}

Dibujemos una pantalla en una escena 3D.

Puede notar que la pantalla cambia proporcionalmente de tamaño a medida que la cabeza se aleja y se acerca a la cámara. En la parte superior, la cara está alejada de la pantalla y, en consecuencia, es pequeña. En la parte inferior de la imagen la cara es más grande, lo que significa que está muy cerca de la cámara.

Ahora podemos trasladar el punto de intersección al espacio de la propia pantalla.

class FaceControls { ...
  screen = {
    halfScale: new THREE.Vector3(),
    center: new THREE.Vector3(),
  }

  get irisRatio() {
    return this.tracker.irisWidthInPx / this.irisWidth
  }

  loop() { ...
    this.screen.halfScale
      .copy(this.screenHalfScaleReal)
      .multiplyScalar(this.irisRatio)

    this.screen.center.setY(-this.screen.halfScale.y)
  }
}

Para ello calculamos la diferencia entre el punto de intersección y el centro de la pantalla. ¿Qué significa este valor? Si un punto se ubica arriba a la derecha del centro, entonces tiene un valor negativo en el eje X y un valor positivo en el eje Y, en la parte inferior izquierda, por el contrario, es positivo en el eje X y negativo en el. el eje Y. Si el punto está en el centro, tendrá coordenadas cero. Estos valores son relevantes para el sistema de coordenadas de la cámara, pero no tienen nada que ver con el sistema de coordenadas dentro de la pantalla misma.

Dividamos el resultado por el tamaño de la pantalla en el espacio de la cámara y así normalicemos las coordenadas. Ahora bien, si un punto está en el borde derecho de la pantalla, tiene una coordenada X igual a menos uno. En el borde izquierdo hay una coordenada Y igual a más uno.

Teniendo las coordenadas normalizadas, podemos multiplicarlas por la resolución de la pantalla, y finalmente obtener un punto en el sistema de coordenadas de la pantalla. Es cierto que el origen del sistema de coordenadas de la pantalla no se encuentra en el centro, sino en la esquina superior izquierda, así que no olvides mover el punto.

Suavizar el punto de intersección

Podemos dibujar el punto de intersección en la pantalla y asegurarnos de que todo funciona correctamente, pero el punto se sacudirá debido al ruido en la imagen de la cámara web. Esto se nota especialmente en condiciones de poca luz.

Para calmar un punto, es necesario aplicarle un algoritmo de suavizado, por ejemplo, un filtro de Kalman, para el cual ya existe una biblioteca preparada.

Inicializamos el filtro dentro de la clase FaceControls con esta configuración, en la que especificamos los parámetros:

  • sensorDimensión — la dimensión del espacio, en nuestro caso es bidimensional.

  • covarianza — tasa de cambio del estado del sistema. Cuanto más rápido cambia el sistema, menos estable es, lo que significa que el punto se contraerá. Y, a la inversa, si el sistema cambia lentamente, la punta no seguirá el movimiento de la cabeza.

import { KalmanFilter } from 'kalman-filter'

class FaceControls { ...
  kalman = new KalmanFilter({
    observation: { name: 'sensor', sensorDimension: 2 },
    dynamic: { name: 'constant-position', covariance: (0.005, 0.005) },
  })
  kalmanState: any // простите

  loop() { ...
    this.kalmanState = this.kalman.filter({
      previousCorrected: this.kalmanState,
      observation: (this.target.x, this.target.y),
    })
    const { mean } = this.kalmanState
    this.target.setX(mean(0)(0))
    this.target.setY(mean(1)(0))
    ...
  }
}

En un bucle infinito, aplique un método de filtro a un punto, que devolverá el estado del sistema. Y de este estado tomaremos los valores suavizados de las coordenadas del punto.

import { KalmanFilter } from 'kalman-filter'

class FaceControls { ...
  kalman = new KalmanFilter({
    observation: { name: 'sensor', sensorDimension: 2 },
    dynamic: { name: 'constant-position', covariance: (0.005, 0.005) },
  })
  kalmanState: any // простите

  loop() { ...
    this.kalmanState = this.kalman.filter({
      previousCorrected: this.kalmanState,
      observation: (this.target.x, this.target.y),
    })
    const { mean } = this.kalmanState
    this.target.setX(mean(0)(0))
    this.target.setY(mean(1)(0))
    ...
  }
}

Ahora el problema puede considerarse resuelto. Ahora tenemos la base para desarrollar un sistema completo de seguimiento ocular.

Posibilidad de utilizar el modelo.

¿Dónde más puedes utilizar este mismo modelo de reconocimiento de puntos clave? Los ejemplos incluyen el desarrollo de juegos, así como los sectores social y de entretenimiento.

Desarrollo de juegos

El sistema también se puede utilizar para desarrollar juegos interactivos. Te mostraré cómo funciona el modelo usando el ejemplo del juego Fruit Ninja.

En el juego original debes hacer clic para cortar las frutas que aparecen en la pantalla. La hoja se controla moviendo el dedo por la pantalla táctil. ¿Qué pasa si intentas controlar la espada moviendo la cabeza? Intentemos implementar esto.

Necesitaremos la biblioteca Matter.js. Este es un motor de física 2D.

Primero, declaremos las entidades principales, indiquemos el contenedor para renderizar, la gravedad y el tamaño del mundo.

const canvas = document.createElement('canvas')

canvas.id = 'fruit'

document.body.appendChild(canvas)

const engine = Matter.Engine.create({ gravity: { y: 0.5 } })
const render = Matter.Render.create({ canvas, engine, options: {
  width: WIDTH,
  height: HEIGHT,
  wireframes: false,
}})

Matter.Render.run(render)

La hoja que corta la fruta parece una línea curva que se estrecha de principio a fin. La posición de la punta se actualiza constantemente en función del punto al que apunta la cabeza.

class Blade {
  trail: Point() = ( { x: 0, y: 0 }, { x: 0, y: 0 } )

  get angle() {
    return getAngleBetweenPoints(this.trail(0), this.trail(1))
  }
  get velocity() {
    return getDistanceBetweenPoints(this.trail(0), this.trail(1))
  }

  update({ x, y }: Point) {
    this.trail = ({ x, y }, ...this.trail.slice(0, BLADE.TRAIL_LENGTH - 1))
  }

  render(ctx: CanvasRenderingContext2D) { }
}

blade.update(faceControls.target)

Las frutas salen volando desde la parte inferior de la pantalla con cierta periodicidad, como si fueran una catapulta. El intervalo entre tiros se acorta o aumenta, dependiendo de los puntos obtenidos.

delayCounter++

if (delayCounter > delay) {
  const fruit = createCircle(SLING.X, SLING.Y)
  fruits.push(fruit)

  Matter.Composite.add(engine.world, fruit)
  Matter.Body.applyForce(fruit, fruit.position,
    Matter.Vector.create(
      Matter.Common.random(-0.1, 0.1),
      Matter.Common.random(-0.3, -0.5)
    )
  )

  delayCounter = 0
}

Si una fruta sale volando de la pantalla, la eliminamos de la memoria, reducimos los puntos y el intervalo de disparo.

fruits = fruits.filter(fruit => {
  if (fruit.position.y > SLING.Y) {
    Matter.World.remove(engine.world, fruit)
    score--
    delay += DELAY_STEP
    return false
  }

  ...
})

Se retira la fruta cortada y en su lugar se crean dos mitades, que vuelan hacia los lados, dependiendo de la dirección y velocidad de la cuchilla.

fruits = fruits.filter(fruit => { ...
  if (!areCirclesColliding(bladeCircle, fruitCircle)) return true
  
  const velocity = blade.velocity * 0.002
  const pieces = (blade.angle, blade.angle + Math.PI).map((angle, i) => {
    ...
  })

  fruitPieces.push(...pieces)
  Matter.Composite.add(engine.world, pieces)
  Matter.World.remove(engine.world, fruit)
  score++
  delay -= DELAY_STEP
  return false
})

Con el tiempo, los trozos de fruta desaparecen y son eliminados del mundo.

fruitPieces = fruitPieces.filter(piece => {
  const opacity = (piece.render.opacity || 1) - 0.02

  if (opacity <= 0) {
    Matter.World.remove(engine.world, piece)
    return false
  }

  piece.render.opacity = opacity

  return true
})

Todo el código fuente está disponible. en mi repositorio. Ahora veamos cómo se ve este juego en la vida real.

La curva roja representa la dirección de visión, que reemplaza efectivamente el control manual de la hoja.

Dado que el modelo es capaz de reconocer varias caras a la vez, puedes jugar con amigos. O agregue algunos efectos basados ​​​​en expresiones faciales: levantó las cejas, las frutas vuelan más alto, las bajó, más abajo, abrió la boca, salió volando una bomba. La demostración se puede mejorar.

Sector del entretenimiento

MediaPipe también ofrece utilizar el modelo para aplicar filtros y efectos al rostro humano.

De hecho, esto puede ser muy solicitado entre los creadores de contenido, pero daré ejemplos más serios y socialmente significativos.

Esfera social

Con el modelo, se puede implementar la autenticación basada en la geometría facial, por ejemplo, para acceder a datos corporativos, transacciones en efectivo a través de cajeros automáticos o en cajas de autoservicio. Un modelo similar se utiliza en el metro de Moscú.

Las expresiones faciales se pueden utilizar para determinar las emociones, el bienestar e incluso las intenciones de una persona, y esto puede resultar útil en el campo del marketing, la medicina o la ciencia forense.

Quién sabe, tal vez lleguemos a una comprensión completamente nueva de la interactividad en las interfaces de usuario. Los clics en los elementos se producirán enfocándose en ellos y parpadeando, y la dirección de su mirada hacia arriba o hacia abajo en la pantalla le permitirá desplazarse por la página a diferentes velocidades. Puede completar formularios utilizando el teclado en pantalla o incluso basándose en el reconocimiento de voz. Creo que este enfoque para gestionar interfaces ayudará a las personas con discapacidades que no pueden utilizar dispositivos de entrada convencionales.

Privacidad y seguridad

El uso de cualquier dispositivo multimedia sólo debe ocurrir en un contexto seguro. El trabajo del modelo de reconocimiento facial impone ciertas condiciones a la interacción con navegador. Es imperativo solicitar permiso al usuario y, si el dispositivo multimedia está encendido, el indicador correspondiente debe estar encendido.

Si el usuario ha concedido acceso a la cámara, los requisitos se aplican no sólo al navegador, sino también a desarrolladores. MediaPipe afirma que todo el procesamiento se produce únicamente en el cliente. En principio, este es un marco de código abierto, su código fuente se puede examinar cuidadosamente para detectar violaciones de confidencialidad, pero se desconoce qué sucede allí en el momento en que el paquete se compila y publica en NPM.

En el ejemplo del juego, todo el procesamiento también ocurre en el cliente y los datos siguen siendo propiedad del usuario. Pero hay ejemplos de uso del modelo en los que es imposible prescindir de enviar datos al servidor, por ejemplo, cuando se necesita autenticación facial. Esto ya se aplica a los datos biométricos, mediante los cuales es posible la identificación personal. Según la Ley de Datos Personales, será necesario el consentimiento por escrito del usuario para el tratamiento.

El usuario también debe ser consciente de para qué se utilizan sus datos, quién más tiene acceso a ellos, durante cuánto tiempo se almacenan y cómo se protegen. Los datos almacenados deben ser anonimizados para que no sea posible vincularlos a un tema específico sin alguna información adicional. Y si es posible, es mejor anonimizar completamente los datos.

Conclusiones

El marco MediaPipe resuelve problemas de visión por computadora y procesamiento de datos multimedia. Se utiliza en diversos campos, desde el entretenimiento hasta cosas socialmente significativas. En este artículo, analizamos solo un modelo entre muchos disponibles e intentamos implementarlo para resolver el problema del seguimiento ocular a través de la cámara web de una computadora portátil.

El marco funciona muy bien e incluye muchos módulos y componentes ya preparados, pero no debes percibirlo como un producto terminado. Esta es una herramienta que deberá modificarse para adaptarse a sus necesidades.

En cuanto al desarrollo, el principal problema son las exigencias, y no tanto a la potencia informática del dispositivo, sino al usuario. Debe conocer la diagonal de la pantalla, el ángulo de visión de la cámara, que debe estar colocada en la parte superior central de la pantalla, y también abrir el navegador en modo de pantalla completa.

Y aunque el modelo de reconocimiento de puntos nos da una idea de la geometría del rostro humano, el resultado que obtuvimos es idealizado, porque se basa en el modelo canónico y no tiene en cuenta las características individuales del usuario. Si necesita un modelo más preciso, tendrá que utilizar el sistema junto con un sensor de profundidad LIDAR, y esto impone serias restricciones de disponibilidad y multiplataforma.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *