Detectar rostros y expresiones en p5.js

Repositorio Github

Estudio comparativo de librerías de detección de rostro: del cálculo manual al poder de los Blendshapes

¿Qué conviene usar para detectar gestos faciales, los landmarks (puntos brutos del rostro) o los blendshapes (valores de expresión pre-calculados)?

Esa distinción es clave cuando lo que se busca no es solo identificar un rostro, sino entender su expresión. Por ejemplo, medir cuándo se abre la boca para activar un sistema de partículas o animar un efecto visual.

En este post explico cómo abordé esa decisión, qué librerías comparé y por qué finalmente elegí MediaPipe Face Landmarker, que combina precisión, rendimiento y una API perfecta para proyectos creativos con p5.js.


Explorando opciones: cuatro librerías de detección facial

En el ecosistema web hay muchas herramientas para detección de rostros, pero cuatro destacan por su solidez y versatilidad:

1. MediaPipe (Face Landmarker)

Desarrollada por Google, es una solución completa para visión por ordenador. Ofrece 478 landmarks 3D y, lo más importante, una serie de blendshapes que representan expresiones faciales en valores normalizados (de 0 a 1). Entre ellos están jawOpen (apertura de mandíbula), mouthFunnel (forma de túnel o soplo) o mouthSmile (sonrisa), que simplifican muchísimo la lógica de interacción.

2. ml5.js (FaceMesh)

Basada en el modelo FaceMesh, es una librería pensada para el entorno educativo y artístico. Funciona muy bien con p5.js, pero solo devuelve los landmarks brutos. Eso significa que, si quiero saber si una persona tiene la boca abierta, debo calcularlo manualmente midiendo la distancia entre el labio superior e inferior y normalizarla según la escala de la cara.

3. face-api.js

Una de las veteranas de JavaScript para detección facial, construida sobre TensorFlow.js. Detecta caras, estima edad y género y reconoce expresiones básicas (“happy”, “sad”, “surprised”…). Sin embargo, trabaja con solo 68 puntos 2D y sus expresiones son clasificaciones discretas, no valores continuos: sabe si alguien está “sorprendido”, pero no cuán abierta está su mandíbula.

4. Jeeliz (jeelizFaceFilter.js)

Una opción sorprendentemente rápida y ligera. Usa WebGL para ofrecer un rendimiento altísimo, ideal para proyectos de realidad aumentada tipo filtros de Snapchat. Aunque no tan conocida, incluye también coeficientes de expresión parecidos a los blendshapes, entre ellos el de apertura de boca.


Comparativa general

  • Tecnología base:
    MediaPipe: Propia de Google ·
    ml5.js: Wrapper de TensorFlow.js ·
    face-api.js: TensorFlow.js ·
    Jeeliz: WebGL + IA propia
  • Nº de puntos:
    MediaPipe: ~478 (3D) ·
    ml5.js: ~478 (3D) ·
    face-api.js: 68 (2D) ·
    Jeeliz: ~11 + pose 3D
  • Rendimiento:
    MediaPipe: Muy alto ·
    ml5.js: Alto ·
    face-api.js: Medio-alto ·
    Jeeliz: Extremadamente alto
  • Facilidad con p5.js:
    MediaPipe: Media ·
    ml5.js: Muy fácil ·
    face-api.js: Fácil ·
    Jeeliz: Media
  • Detección de boca:
    MediaPipe: Blendshapes ·
    ml5.js: Landmarks (distancia manual) ·
    face-api.js: Clasificación (sorpresa) ·
    Jeeliz: Coeficiente de expresión


Del método “difícil” al método “inteligente”

Escenario 1: el enfoque clásico (ml5.js)

Con ml5.js, la detección de la boca abierta requiere varios pasos:

  1. Obtener el array de landmarks.
  2. Localizar los puntos del labio superior e inferior.
  3. Calcular la distancia euclidiana entre ellos.
  4. Normalizar esa distancia con respecto al tamaño de la cara.
  5. Definir un umbral (un “número mágico”) para decidir cuándo la boca está abierta.

Funciona, pero implica muchos cálculos y ajustes manuales. Además, la distancia cambia si la persona se acerca o se aleja de la cámara.

Escenario 2: el enfoque inteligente (MediaPipe)

Con MediaPipe Face Landmarker, todo ese trabajo desaparece. El modelo ya analiza internamente los landmarks y devuelve coeficientes de expresión listos para usar. Basta con obtener el valor de jawOpen o mouthFunnel:

let jawOpenScore = results.faceBlendshapes[0].categories
  .find(c => c.categoryName === 'jawOpen').score;

let mouthFunnelScore = results.faceBlendshapes[0].categories
  .find(c => c.categoryName === 'mouthFunnel').score;

Ese jawOpenScore ya viene normalizado (de 0.0 a 1.0) y es independiente de la distancia a la cámara. En otras palabras: un indicador robusto, limpio y directo de la apertura de la mandíbula.


Aplicación en p5.js: de la expresión a la interacción

Una vez se tiene ese valor, la creatividad entra en juego. En mi caso, lo uso para generar partículas que salen de la boca según el grado de apertura:

let particleCount = map(jawOpenScore, 0, 1, 0, 500);
let particleSpeed = map(jawOpenScore, 0, 1, 1, 10);

Incluso podría combinar otros coeficientes:

  • mouthFunnel para simular un soplido que empuja las partículas.
  • mouthSmile para cambiar su color o comportamiento.

De este modo, el rostro se convierte en un controlador expresivo, no solo en un disparador binario de eventos.


Conclusión

Después de probar varias opciones, MediaPipe Face Landmarker se impone claramente por su equilibrio entre precisión, rendimiento y simplicidad. Mientras que ml5.js sigue siendo ideal para empezar o para proyectos educativos, MediaPipe ofrece una capa de abstracción mucho más potente gracias a los blendshapes, que permiten traducir la expresión facial directamente en comportamiento interactivo, sin cálculos intermedios.


Diseño de la aplicación

Moodboard Inspiracional

Wireframe de alta definición


Workflow de la App

La aplicación sigue un flujo de trabajo continuo centrado en la detección facial en tiempo real. Al iniciarse, setup() configura el canvas WEBGL en 1080p (aunque se podría adaptar a otros tamaños), inicializa MediaPipe Face Landmarker para análisis facial, y carga los recursos multimedia (fuentes Halloween, imágenes decorativas y audio).

El bucle principal draw() ejecuta tres procesos simultáneos: primero captura video de la cámara del usuario aplicando efecto espejo para interacción natural, luego procesa cada frame mediante detectFaceAndJaw() que utiliza MediaPipe para analizar 468 landmarks faciales y extraer el valor de apertura de mandíbula (jawOpen), y finalmente coordina la respuesta visual generando partículas Halloween desde las coordenadas exactas de la boca detectada con los landmarks del labio superior e inferior y comisuras.

El sistema de partículas implementa física realista con gravedad, colisiones y apilamiento, mientras que elementos visuales como el texto 3D "ABRE LA BOCA", el background dinámico y el audio ambiental responden en tiempo real al estado de la boca, creando una experiencia interactiva completa donde la expresión facial del usuario controla directamente los efectos visuales y sonoros del entorno Halloween.

 


Proceso de desarrollo de código — Proyecto Halloween p5.js

Fase 1: Configuración Inicial y Setup Básico

Primeros Pasos

El proyecto se estructuró con los archivos principales index.html, sketch.js, particulas.js y style.css. Se definió un canvas WEBGL para habilitar gráficos 3D y se preparó la integración con MediaPipe y p5.sound.

<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'
  https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://storage.googleapis.com;
  style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:;
  connect-src 'self' https://cdn.jsdelivr.net https://storage.googleapis.com;">
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/addons/p5.sound.min.js"></script>
<script type="module" src="sketch.js"></script>
<script src="particulas.js"></script>

Durante esta fase aparecieron errores menores con el Content-Security-Policy y la carga del modelo de MediaPipe, que se solucionaron ajustando permisos y rutas.


Fase 2: Integración de Video y Cámara

Desafío: Captura de Video

La cámara se configuró con createCapture(), solicitando una resolución de 1280×720 (para conseguir un 1080p) y orientación frontal. Se aplicó un efecto espejo en la visualización para que la interacción fuese natural. No funcionaba con flipped y se añadió scale().

// sketch.js
capture = createCapture({
  video: { width: 1280, height: 720, facingMode: 'user' },
  audio: false,
  flipped: true
});
capture.hide();

// En draw():
push();
scale(-1, 1); // espejo horizontal
imageMode(CENTER);
image(capture, 0, 0, newVideoWidth, newVideoHeight);
pop();

El canvas se adaptó dinámicamente al tamaño de ventana con resizeCanvas(windowWidth, windowHeight), logrando un diseño responsivo incluso en modo WEBGL, aunque el uso está pensado para un monitor 1080p.


Fase 3: Implementación de MediaPipe

Mayor Desafío Técnico

Integrar MediaPipe Face Landmarker (vía @mediapipe/tasks-vision) fue el reto más complejo. El modelo se carga asíncronamente desde el CDN de Google y se ejecuta en modo VIDEO para análisis frame a frame.

Si se carga la librería https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3 desde el index da error.

// sketch.js — inicialización
const vision = await import('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3');
const resolver = await vision.FilesetResolver.forVisionTasks(
  "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm"
);
faceLandmarker = await vision.FaceLandmarker.createFromOptions(resolver, {
  baseOptions: {
    modelAssetPath:
      "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
  },
  outputFaceBlendshapes: true,
  runningMode: "VIDEO",
  numFaces: 1
});

Fase 4: Detección de Gestos Faciales

La detección de apertura de boca se basó en el valor jawOpen dentro de los blendshapes de MediaPipe. Este valor (0–1) permite medir el grado de apertura mandibular sin tener que calcular distancias entre landmarks.

// sketch.js — detección facial
const results = faceLandmarker.detectForVideo(video, nowInMs);
if (results.faceBlendshapes?.length > 0) {
  const blendshapes = results.faceBlendshapes[0].categories;
  for (let c of blendshapes) {
    if (c.categoryName === 'jawOpen') {
      jawOpenValue = c.score;
      jawOpen = jawOpenValue > jawOpenThreshold;
      break;
    }
  }
}

El cálculo de posición del centro de la boca se realizó con los landmarks 13 y 14, mapeando coordenadas normalizadas al tamaño del vídeo dentro del canvas WEBGL:

// sketch.js
const upperLip = landmarks[13];
const lowerLip = landmarks[14];
const normalizedMouthX = (upperLip.x + lowerLip.x) / 2;
const normalizedMouthY = (upperLip.y + lowerLip.y) / 2;

mouthCenterX = map(1 - normalizedMouthX, 0, 1, -newVideoWidth/2, newVideoWidth/2);
mouthCenterY = map(normalizedMouthY, 0, 1, -newVideoHeight/2, newVideoHeight/2);

Fase 5: Sistema de Partículas

Evolución del Sistema

El sistema comenzó como un conjunto de partículas simples sin física. Se evolucionó hacia una clase con gravedad, fricción, rebote y tipos visuales (spark y brush).

// particulas.js — clase Particle
class Particle {
  constructor(x, y, size, col, life, type = 'spark') {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D().mult(random(0.3, 5.5));
    this.gravity = 0.1;
    this.bounce = 0.3;
    this.friction = 0.95;
    this.size = size;
    this.color = color(col);
    this.life = life;
    this.type = type;
  }

  update() {
    this.vel.y += this.gravity;
    this.pos.add(this.vel);
    // rebote con el suelo
    let ground = height / 2;
    if (this.pos.y >= ground - this.size/2) {
      this.pos.y = ground - this.size/2;
      this.vel.y *= -this.bounce;
      this.vel.x *= this.friction;
    }
    this.life -= 1;
  }

  draw() {
    push();
    fill(this.color);
    noStroke();
    if (this.type === 'spark') ellipse(this.pos.x, this.pos.y, this.size);
    else rect(this.pos.x, this.pos.y, this.size*2.2, this.size*0.7, 6);
    pop();
  }
}

Cada vez que jawOpen supera el umbral, se generan partículas en la posición de la boca:

// sketch.js
if (jawOpen && frameCount % 3 === 0) {
  createHalloweenParticles();
}

function createHalloweenParticles() {
  let x = mouthCenterX;
  let y = mouthCenterY;
  let color = random(Object.values(COLORS));
  let particleSize = map(jawOpenValue, 0, 1, 1, 25);
  let type = random(['spark', 'brush']);
  particles.push(new Particle(x, y, particleSize, color, 1000, type));
}

Fase 6: Coordinación Facial–Partículas

El reto principal fue sincronizar los valores de jawOpen con el sistema de partículas sin provocar caídas de rendimiento. Además, se añadió desvanecimiento progresivo al cerrar la boca:

// sketch.js — updateParticles()
if (!jawOpen) {
  particles[i].alpha = lerp(particles[i].alpha, 0, 0.03);
  particles[i].life -= 1;
} else {
  particles[i].alpha = lerp(particles[i].alpha, 255, 0.08);
}

La función lerp() (interpolación lineal) permite una transición visual suave al pasar de estado activo (boca abierta) a inactivo (boca cerrada).


Fase 7: Elementos Visuales y Audio

Se implementó un texto 3D “ABRE LA BOCA” que rota con rotateX() y rotateY(), y un sistema de sonido reactivo que se activa cuando la boca se abre. Al inicio del proceso de programación funcionaba con el ratón:

// sketch.js — audio reactivo
if (jawOpen && !sonido.isPlaying()) {
  sonido.play();
  sonido.loop();
} else if (!jawOpen && sonido.isPlaying()) {
  sonido.stop();
}

Dificultades principales superadas

  1. Coordinación de sistemas asíncronos: MediaPipe, partículas, audio y render 3D corren en paralelo.
  2. Optimización de rendimiento: límite de partículas simultáneas y eliminación cuando salen del canvas para finalmente realizar rebote y acumulación.
  3. Responsive design: adaptación del canvas WEBGL al tamaño de ventana, mantener ratio de video y efecto espejo.

Resultado Final

El proyecto evolucionó de un sketch básico a una aplicación interactiva que integra:

  • Utilización del entorno de p5.js v2 y cargas asíncronas.
  • Detección facial en tiempo real con MediaPipe
  • Sistema de partículas con física y apilamiento
  • Elementos visuales y texto 3D p5.js v2
  • Audio ambiental reactivo
  • Canvas responsivo en WEBGL p5.js v2
  • Paleta cromática temática de Halloween

Publicaciones Similares

Deja una respuesta