Como hacer un juego que reconoce manos con IA


Objetivo: Crear un proyecto simple de videojuego que va a estar conectado a un modelo de IA que reconoce manos. Es decir, la estructura sera muy simple y es para demostrar como se hace, si gustas lo puedes ampliar y aplicarlo a lo que sea mas cómodo para ti si quieres hacer algo mas grande.

Requisitos

Entrenar a un modelo de IA:

Si saben inglés, se pueden ayudar del tutorial oficial de Google pero igual vas a necesitar abrirlo, porque de ese link, tienes que darle click al botón que dice ‘Run in Colab’

Donde se encuentra Run in Colab

Se va a abrir algo llamado Notebook que es como un entorno de ejecución para Python, no es necesario que sepas usarlo realmente, pero quiero que sigas mis pasos, por defecto en todo el código que esta aquí tenemos imágenes para entrenar un modelo que reconoce los gestos de mano para Piedra, Papel y Tijeras.

Si quieres que reconozca otro tipo de gestos, necesitas encontrar mínimo unas 20 imágenes de ese gesto, aquí un ejemplo de las que yo recolecte de Google imágenes y un par de mis propias manos para reconocer al gesto de Zorro:

Imágenes de gesto de zorro

Por supuesto que también puedes tomar un montón de fotos de tus manos o de alguien mas que te pueda ayudar, no pueden ser la misma imagen una y otra vez porque no sirve de nada para que la IA aprenda a diferenciar los gestos.

Para que el modelo se entrene en base a nuestras imágenes, tenemos que preparar una carpeta que se llame “rps_data_sample” en nuestro PC, aquí dentro vamos a meter las carpetas que tendrán imágenes de gestos de mano para entrenar, puedes poner gestos con 2 manos aunque le va a costar un poco mas entenderlas. Asi quedo mi carpeta por dentro:

Ejemplo orden de carpetas

Con eso listo, vamos a crear un .zip de la carpeta “rps_data_sample”

Crear un zip a partir de una carpeta en windows

Volviendo al Google Colab, vamos a conectarnos al servidor usando el siguiente botón:

Conectar Google Colab

Ahora vamos a hacer 3 cosas, primero comentamos o borramos la linea de código indicada en el Colab, segundo hacemos click en el icono de carpeta para abrir los archivos del servidor, y tercero hacemos click en el icono de subir archivo, subimos nuestro rps_data_sample.zip.

Pasos importantes de subida de datos

Nota: Si por lo que sea se te reinicia la pagina del Colab, tienes que empezar desde cero estos pasos.

Ahora, vamos a ejecutar todos los segmentos de código desde Prerequisites hasta la celda que dice files.download('exported_model/gesture_recognizer.task') se deben ir ejecutando en orden, si se ejecuto todo correctamente debería de descargarse un archivo gesture_recognizer.task este vendría a ser nuestro modelo de IA entrenado, tenlo guardado.

Inicio ejecuciones Ultima ejecución

Si sabes Python y le mueves al machine learning, puedes ir mas allá modificando el entrenamiento del modelo siguiendo las instrucciones de Google que salen en este Notebook.

Si solo querías el modelo, puedes dejarlo hasta aquí y guiarte con Google para aplicarlo en Web, Python, Android o iOS.

Creando el Videojuego con Excalibur:

A partir de aquí, voy a utilizar un IDE online pero lo puedes replicar en tu PC sin problemas.

Creamos una carpeta donde vamos a tener el proyecto, la abrimos usando vscode y dentro, usando la terminal, ejecutamos el comando: npm create vite@latest . para generar un nuevo proyecto de node con Vite.

En la terminal te van a pedir información para crear el proyecto, yo le puse de nombre sample-game y también vamos a elegir Vanilla y luego Typescript, puedes moverte con las flechas por las opciones:

Comando resultado

Dentro de la misma terminal, vamos a instalar las dependencias con los siguientes comandos:

  • npm i
  • npm i @mediapipe/tasks-vision excalibur

Tendrías que tener una estructura de archivos similar a la siguiente (ignorar .github, .codesandbox, .devcontainer), siéntete libre de borrar todo lo que este dentro de la carpeta src:

Estructura inicial

Vamos a modificar el contenido del index.html tendría que quedar algo parecido a esto:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Juego de Ejemplo</title>
    <style>
      video {
        clear: both;
        display: block;
        transform: rotateY(180deg);
        -webkit-transform: rotateY(180deg);
        -moz-transform: rotateY(180deg);
        height: 280px;
      }

      .videoView {
        position: absolute;
        min-height: 467px;
        top: 12px;
        left: 440px;
      }

      .videoView p {
        padding-top: 5px;
        padding-bottom: 5px;
        background-color: #007f8b;
        color: #fff;
        border: 1px dashed rgba(255, 255, 255, 0.7);
        z-index: 2;
        margin: 0;
      }

      .highlighter {
        background: rgba(0, 255, 0, 0.25);
        border: 1px dashed #fff;
        z-index: 1;
        position: absolute;
      }

      .canvas {
        z-index: 1;
        position: absolute;
        pointer-events: none;
      }

      .output_canvas {
        transform: rotateY(180deg);
        -webkit-transform: rotateY(180deg);
        -moz-transform: rotateY(180deg);
      }

      .output {
        display: none;
        width: 45%;
        font-size: 24px;
      }
    </style>
  </head>
  <body>
    <canvas id="game"></canvas>
    <div id="liveView" class="videoView">
      <div style="position: relative">
        <video id="webcam" autoplay playsinline></video>
        <canvas
          class="output_canvas"
          id="output_canvas"
          width="360"
          height="240"
          style="position: absolute; left: 0px; top: 0px"
        ></canvas>
        <p id="gesture_output" class="output"></p>
      </div>
    </div>

    <script type="module" src="./src/game.ts"></script>
  </body>
</html>

Seguido a esto, tenemos que agregar los archivos que tendrán el juego y el código que he modificado a partir de un ejemplo de Google para poder usarlo en otros lados.

Dentro de la carpeta src creamos gesture.ts y game.ts ademas de esto, creamos una carpeta llamada resources a nivel del proyecto, dentro de dicha carpeta vamos a meter el archivo gesture_recognizer.task, osea el modelo de IA con el objetivo de usarlo en gesture.ts.

También vamos a necesitar un archivo llamado files.d.ts en el proyecto, créalo y este seria su contenido:

declare module "*.task?url" {
  const value: string;
  export default value;
}

Estructura Media

Vamos a ver el código que va para el archivo gesture.ts:

Primero realizamos los imports:

import {
  GestureRecognizer,
  FilesetResolver,
  DrawingUtils,
  GestureRecognizerResult,
} from "@mediapipe/tasks-vision";

import modelAssetPath from "../resources/gesture_recognizer.task?url";

Ahora vamos a agregar unas variables y un enum Gestures en este ponemos todos los nombres de gestos con los que entrenamos a la IA, es decir, los nombres que quedaron en la carpetas del .zip, deben ser tal cual:

export enum Gestures {
  NONE = "none",
  FOX = "fox",
  SCISSORS = "scissors",
  PAPER = "paper",
  ROCK = "rock",
}

let gestureRecognizer: GestureRecognizer;
let runningMode = "IMAGE";
const videoWidth = "360px"; // ancho de la webcam en el html
const videoHeight = "240px"; // altura de la webcam en el html
let webcamRunning = false;
export let isPredictionsStarted = false;
export let lastGesture: string = Gestures.NONE;

Posterior a eso, creamos una función que va a inicializar la IA en el navegador, dentro podemos modificar una serie de parámetros:

export const createGestureRecognizer = async () => {
  const vision = await FilesetResolver.forVisionTasks(
    "https://cdn.jsdelivr.net/npm/@mediapipe/[email protected]/wasm"
  );
  gestureRecognizer = await GestureRecognizer.createFromOptions(vision, {
    baseOptions: {
      modelAssetPath,
      delegate: "GPU",
    },
    // numHands: 2, /* Descomenta esta linea si entrenaste tu modelo de IA con imágenes que incluyen gestos de 2 manos */
    runningMode: runningMode as any,
    /* Hay mas parámetros que pueden serte de utilidad, solo modificar si sabes que es lo que estas haciendo */
  });
};

Creamos unas constantes que van a ser usadas para manipular el html de webcam y la visualización de tu mano siendo detectada:

const video: HTMLVideoElement = document.getElementById(
  "webcam"
) as HTMLVideoElement;
const canvasElement: HTMLCanvasElement = document.getElementById(
  "output_canvas"
) as HTMLCanvasElement;
const canvasCtx = canvasElement.getContext("2d");
const gestureOutput: HTMLParagraphElement = document.getElementById(
  "gesture_output"
) as HTMLParagraphElement;

Ingresamos las siguientes funciones que son para utilizar la webcam y transferir esa información de tu cam a la IA:

// Verificar acceso a la webcam
export function hasGetUserMedia() {
  return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}

// Iniciar la webcam y las predicciones de gestos
export function enableCam() {
  if (!gestureRecognizer) {
    alert("Please wait for gestureRecognizer to load");
    return;
  }
  webcamRunning = true;

  const constraints = {
    video: true,
  };

  // Activar el stream de la data de tu webcam al código
  navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
    video!.srcObject = stream;
    video.addEventListener("loadeddata", predictWebcam);
  });
}

Ahora, el codigo mas importante, el que predice que gesto tienen tus manos en base a la informacion de la imagen que da tu camara:

let lastVideoTime = -1;
let results: GestureRecognizerResult;
async function predictWebcam() {
  isPredictionsStarted = true;
  const webcamElement: HTMLVideoElement = document.getElementById(
    "webcam"
  ) as HTMLVideoElement;
  // Now let's start detecting the stream.
  if (runningMode === "IMAGE") {
    runningMode = "VIDEO";
    await gestureRecognizer.setOptions({ runningMode: "VIDEO" });
  }
  let nowInMs = Date.now();
  if (video.currentTime !== lastVideoTime) {
    lastVideoTime = video.currentTime;
    results = gestureRecognizer.recognizeForVideo(video, nowInMs);
  }

  canvasCtx?.save();
  canvasCtx?.clearRect(0, 0, canvasElement?.width, canvasElement?.height);
  const drawingUtils = new DrawingUtils(canvasCtx as CanvasRenderingContext2D);

  canvasElement.style.height = videoHeight;
  webcamElement.style.height = videoHeight;
  canvasElement.style.width = videoWidth;
  webcamElement.style.width = videoWidth;

  if (results.landmarks) {
    // Este fragmento de código es para dibujar los puntos rojos y las lineas en tu mano.
    for (const landmarks of results.landmarks) {
      drawingUtils.drawConnectors(
        landmarks,
        GestureRecognizer.HAND_CONNECTIONS,
        {
          color: "#00FF00",
          lineWidth: 2.5,
        }
      );
      drawingUtils.drawLandmarks(landmarks, {
        color: "#FF0000",
        lineWidth: 0.5,
      });
    }
  }

  canvasCtx?.restore();

  if (results.gestures.length > 0) {
    // Si hay una detección de gestos, osea se ven manos
    // Inicio de modificaciones en HTML para visualizar la detección de gestos
    gestureOutput.style.display = "block";
    gestureOutput.style.width = videoWidth;
    const categoryName = results.gestures[0][0].categoryName;
    const categoryScore = (results.gestures[0][0].score * 100).toFixed(2);
    gestureOutput.innerText = `Gesture: ${categoryName}\n Confidence: ${categoryScore}`;
    // Fin de modificaciones en HTML

    if (+categoryScore >= 50) {
      // Si la IA esta por lo menos 50% segura de que tienes un gesto, lo guardamos en la variable lastGesture
      lastGesture = categoryName.toLowerCase(); // Asignamos el gesto reconocido por la IA a una variable
    }
  } else {
    // Si no hay nada detectado, no hay gesto.
    gestureOutput.style.display = Gestures.NONE;
    lastGesture = Gestures.NONE;
  }
  // Esta función se llama en bucle para estar detectando continuamente tus manos.
  if (webcamRunning === true) {
    window.requestAnimationFrame(predictWebcam);
  }
}

Con esto listo, vamos a hacer el código para iniciar el motor de Excalibur, dentro del archivo game.ts ademas de usar el código para detección de manos:

import {
  Gestures,
  createGestureRecognizer,
  enableCam,
  hasGetUserMedia,
  isPredictionsStarted,
  lastGesture,
} from "./gesture";
import {
  Actor,
  CollisionType,
  Color,
  Engine,
  SolverStrategy,
  Vector,
  vec,
} from "excalibur";

const game = new Engine({
  width: 800,
  height: 600,
  canvasElementId: "game",
  fixedUpdateFps: 60,
  physics: {
    solver: SolverStrategy.Arcade,
    gravity: vec(0, 800),
  },
});

game.start().then(async () => {
  if (hasGetUserMedia()) {
    await createGestureRecognizer();
    enableCam();
  } else {
    alert("Tu navegador no soporte el uso de Webcam");
    console.warn("getUserMedia() no es soportado por tu navegador");
  }
});

Detección de manos en proyecto base

Al cargarse el motor y dar permisos de la webcam podemos ver lo trabajado hasta ahora en acción. Desde aquí ya podrías seguir por tu cuenta y crear un videojuego. O incluso aplicarlo a otro motor web como Phaser.

Voy a hacer una interacción de ejemplo, cuando hagas el gesto de roca salte un cuadrado, agregamos el siguiente código en nuestro game.ts justo al fondo para crear un piso.

const floor = new Actor({
  pos: vec(400, 560),
  width: 800,
  height: 80,
  color: Color.Green,
  collisionType: CollisionType.Fixed,
});

game.add(floor);

Y para el jugador justo debajo del fragmento anterior, ponemos este código:

class Player extends Actor {
  constructor(pos: Vector) {
    super({
      pos,
      width: 64,
      height: 96,
      collisionType: CollisionType.Active,
      color: Color.Red,
    });
  }

  onPreUpdate(_engine: Engine<any>, _delta: number): void {
    if (!isPredictionsStarted) return;

    if (lastGesture === Gestures.ROCK) {
      this.vel.y = -600;
    }
  }
}

const player = new Player(vec(300, 300));

game.add(player);

El resultado debería ser un cuadrado rojo que cada que tenemos la mano en puño o roca salga volando:

Resultado final del juego

Y bueno eso seria todo, mucho texto.

El CodeSandbox del resultado

El Repositorio en Github

Si algo anda mal, necesitas un consejo o apoyo con respecto a este tutorial, escribe por el server discord al canal de programación.