Construyendo una aplicación de reconocimiento de objetos enfocado a los navegadores web con TensorFlow.js y Keras
Bienvenido a esta tercera parte de este tutorial.
- Parte 1: Reconocimiento de objetos usando TensorFlow.js y Keras
- Parte 2: Inteligencia Artificial en la Web: TensorFlow.js y Kera
Prueba aqui la aplicacion: Este es el resultado de la version 1.0.0, pruébala en tu pc online aquí: IA de reconocimiento de objetos: TensorFlow.js y Keras
Antes de comenzar con el tutorial de todo el código JavaScript quiero explicar algunas cosas que se deben de tener en cuenta para el correcto funcionamiento de la aplicación en su versión 1.0.0.
- Para entrenar mi modelo de inteligencia artificial utilice juguetes Pokémon.

- Utilice una base giratoria y un fondo blanco. De esta manera podria capturar la mayor cantidad de fotografias posibles en una vista de 360 Grados de cada juguete.

- Cada modelo tiene un total de 1400 Fotografías para maximizar el reconocimiento.
- Recuerda que estás entrenando una inteligencia artificial. Para una máquina, es imposible saber qué objeto estás tratando de enseñarle. Por eso, es crucial evitar usar tus manos para sostener el objeto que estás fotografiando.Si utilizas tus manos, el modelo de IA aprenderá a asociar tus manos con el objeto, en lugar de reconocer el objeto por sí solo. Esto terminará en un conjunto de datos con cientos de fotos de tus manos, lo que confundirá a la IA y afectará gravemente su precisión.
Para obtener los mejores resultados, coloca el objeto sobre una superficie o utiliza un soporte para que la IA se concentre únicamente en las características del objeto.
- Si capturas un mínimo de 20 fotografías por objeto, es probable que la IA no logre un reconocimiento preciso y los resultados no sean los esperados. El éxito del entrenamiento no solo depende de la cantidad, sino también de la calidad y variedad de los datos. A menudo subestimamos lo complejo que es para una máquina interpretar el mundo visual, y el entrenamiento de IA es un claro ejemplo de ello.
- Quizás te preguntes cómo logré capturar 1400 imágenes por cada modelo. La respuesta es que no lo hice manualmente. En lugar de hacer clic 1400 veces, programé un sistema para que automatizara el proceso. Este programa simuló clics a intervalos regulares, con un límite de 1400 capturas, facilitando enormemente la recolección de datos y demostrando cómo la automatización es clave para un entrenamiento de IA eficiente.
En esta sección del tutorial vamos a explicar el JavaScript utilizado para el reconocimiento de objetos.
¡¡¡Comencemos!!!
Inclusion de librerias
El codigo Javascript inicia importando las librerias necesarias.
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-layers@latest"></script> <script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
@tensorflow/tfjs y @tensorflow/tfjs-layers: Estas son las librerías principales de Google que permiten construir y ejecutar modelos de aprendizaje automático directamente en el navegador web usando JavaScript. tfjs-layers es una extensión que proporciona una API de alto nivel para crear modelos más complejos como redes neuronales convolucionales.
file-saver.js: Esta librería facilita el guardado de archivos generados dinámicamente en el navegador, como el archivo ZIP de nuestro modelo.
jszip.js: Permite comprimir y descomprimir archivos en formato ZIP en JavaScript. Es crucial para empaquetar el modelo de TensorFlow.js y los datos de entrenamiento en un solo archivo descargable.
Declaración de Variables y Elementos del DOM
Esta sección inicializa las variables globales que se usarán en todo el programa y establece las conexiones a los elementos HTML de la página, como el video de la cámara, los botones y los campos de texto.
// Declaración de variables globales
let trainingModel;
let webcam;
let trainingData = [];
let classLabels = []; // Nombres de las clases para el reconocimiento
let isPredicting = false;
// La dimensión de entrada para el modelo. Todas las imágenes serán redimensionadas a este tamaño.
const IMAGE_SIZE = 64;
// Referencias a los elementos del DOM
const videoElement = document.getElementById('videoElement');
const statusElement = document.getElementById('status');
const classNameInput = document.getElementById('classNameInput');
const addClassButton = document.getElementById('addClassButton');
const captureButtonsContainer = document.getElementById('captureButtonsContainer');
const trainButton = document.getElementById('trainButton');
const predictButton = document.getElementById('predictButton');
const saveModelButton = document.getElementById('saveModelButton');
const loadModelInput = document.getElementById('loadModelInput');
trainingModel: Aquí se almacenará el modelo de red neuronal una vez que sea entrenado o cargado.
webcam: Un objeto de TensorFlow.js que simplifica la captura de imágenes desde la cámara web.
trainingData: Un array que guardará las imágenes capturadas para cada clase de objeto que queramos reconocer.
classLabels: Un array de cadenas de texto que almacena los nombres de los objetos (ej. «taza», «teléfono», «llaves»).
IMAGE_SIZE: Una constante que determina el tamaño al que se redimensionarán todas las imágenes capturadas antes de ser procesadas por el modelo, optimizando el rendimiento.
Los elementos anteriormente nombrados son los más importantes. Existen otros elementos que hacen parte del HTML como lo son botones y demás.
Inicialización de la Aplicación (init)
Esta función es el punto de partida. Intenta acceder a la cámara del usuario para iniciar el flujo de video en la página. Si tiene éxito, crea un objeto webcam a partir del video y actualiza el estado de la aplicación.
async function init() {
try {
statusElement.textContent = 'Inicializando cámara...';
// Inicializa el objeto de la cámara web
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
videoElement.srcObject = stream;
await new Promise(resolve => {
videoElement.onloadedmetadata = () => {
resolve();
};
});
// Ajusta el tamaño del contenedor del video para mantener la relación de aspecto
const aspectRatio = videoElement.videoWidth / videoElement.videoHeight;
const videoContainer = document.getElementById('video-container');
videoContainer.style.paddingTop = `${100 / aspectRatio}%`;
webcam = await tf.data.webcam(videoElement);
statusElement.textContent = 'Cámara lista. Añade un objeto para empezar.';
} else {
statusElement.textContent = 'Error: Tu navegador no soporta la cámara web.';
}
} catch (error) {
console.error('Error al inicializar la aplicación:', error);
statusElement.textContent = 'Error: No se pudo cargar la cámara.';
}
}
Utiliza navigator.mediaDevices.getUserMedia para solicitar permiso al usuario y acceder a su cámara.
tf.data.webcam() es una función de conveniencia de TensorFlow.js que facilita el uso de la cámara web como fuente de datos para el modelo.
Captura de Datos (addClass y createCaptureButton)
Estas funciones permiten al usuario definir nuevas clases de objetos y capturar imágenes para cada una de ellas.
/**
* Añade un nuevo botón de clase para la captura de imágenes.
*/
function addClass() {
const className = classNameInput.value.trim();
if (className === '') {
statusElement.textContent = 'Por favor, escribe un nombre de objeto válido.';
return;
}
// Evita duplicados
if (classLabels.includes(className)) {
statusElement.textContent = `El objeto "${className}" ya existe.`;
return;
}
// Asigna un índice numérico para la clase
const classIndex = classLabels.length;
classLabels.push(className);
trainingData.push({ name: className, tensors: [] });
createCaptureButton(className, classIndex);
classNameInput.value = '';
}
/**
* Función auxiliar para crear un botón de captura de clase.
* Se usa para añadir nuevas clases y al cargar un modelo existente.
*/
function createCaptureButton(className, classIndex, imageCount = 0) {
const classContainer = document.createElement('div');
classContainer.className = 'flex flex-col items-center p-2 bg-gray-200 rounded-lg';
const button = document.createElement('button');
button.className = 'button bg-blue-500 hover:bg-blue-600 w-full';
button.textContent = `Capturar ${className}`;
const imageCounterElement = document.createElement('p');
imageCounterElement.className = 'text-sm font-medium mt-1 text-gray-600';
imageCounterElement.textContent = `Imágenes: ${imageCount}`;
button.dataset.images = imageCount;
button.addEventListener('click', async () => {
const imgTensor = await webcam.capture();
// Procesa la imagen: redimensiona, normaliza y la convierte a un tensor 4D
const processedTensor = tf.tidy(() => {
return imgTensor.resizeBilinear([IMAGE_SIZE, IMAGE_SIZE])
.expandDims(0)
.toFloat()
.div(255);
});
trainingData[classIndex].tensors.push({ tensor: processedTensor, label: classIndex });
imgTensor.dispose();
const currentCount = parseInt(button.dataset.images) + 1;
button.dataset.images = currentCount;
imageCounterElement.textContent = `Imágenes: ${currentCount}`;
statusElement.textContent = `Imagen de "${className}" capturada. Total: ${currentCount}`;
});
classContainer.appendChild(button);
classContainer.appendChild(imageCounterElement);
captureButtonsContainer.appendChild(classContainer);
}
addClass() agrega un nuevo nombre de objeto al array classLabels y prepara una entrada vacía en trainingData para guardar las imágenes.
createCaptureButton() genera un botón por cada clase de objeto. Cuando se hace clic, el botón captura una imagen de la cámara (webcam.capture()), la procesa (redimensiona y normaliza los píxeles) y la almacena como un tensor de TensorFlow.js en trainingData.
Entrenamiento del Modelo (trainModel)
Esta es la sección más importante del código. Construye una red neuronal y la entrena con los datos capturados.
/**
* Construye y entrena el modelo de clasificación.
*/
async function trainModel() {
const totalImages = trainingData.reduce((sum, cls) => sum + cls.tensors.length, 0);
if (trainingData.length < 2 || totalImages < 10) {
statusElement.textContent = 'Necesitas al menos 2 objetos y un total de 10 imágenes para entrenar.';
return;
}
trainButton.disabled = true;
predictButton.disabled = true;
statusElement.textContent = 'Entrenando modelo... Por favor, espera.';
// Concatenar todos los tensores de imágenes y etiquetas
const allTensors = tf.concat(trainingData.flatMap(cls => cls.tensors.map(t => t.tensor)));
const allLabels = tf.tidy(() => {
const labels = trainingData.flatMap(cls => cls.tensors.map(t => t.label));
return tf.oneHot(tf.tensor1d(labels, 'int32'), trainingData.length);
});
// Crea el modelo de clasificación con una arquitectura Keras personalizada
if (trainingModel) {
trainingModel.dispose();
}
// Define la arquitectura de la red neuronal convolucional
trainingModel = tf.sequential();
trainingModel.add(tf.layers.conv2d({
inputShape: [IMAGE_SIZE, IMAGE_SIZE, 3], // 64x64 pixeles, 3 canales de color (RGB)
kernelSize: 5,
filters: 8,
activation: 'relu',
}));
trainingModel.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));
trainingModel.add(tf.layers.conv2d({
kernelSize: 5,
filters: 16,
activation: 'relu',
}));
trainingModel.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));
// Aplanar la salida de las capas convolucionales antes de las capas densas
trainingModel.add(tf.layers.flatten());
// Capa de salida densa con una unidad por cada clase
trainingModel.add(tf.layers.dense({
units: classLabels.length,
activation: 'softmax'
}));
trainingModel.summary();
// Compila el modelo
trainingModel.compile({
optimizer: 'rmsprop',
loss: 'categoricalCrossentropy',
metrics: ['accuracy'],
});
// Entrena el modelo con los datos capturados
await trainingModel.fit(allTensors, allLabels, {
epochs: 20, // Aumenta para mayor precisión, o reduce para un entrenamiento más rápido
shuffle: true,
callbacks: {
onEpochEnd: (epoch, logs) => {
console.log(`Epoch ${epoch + 1}: Loss = ${logs.loss.toFixed(4)}, Accuracy = ${logs.acc.toFixed(4)}`);
}
}
});
allTensors.dispose();
allLabels.dispose();
statusElement.textContent = 'Entrenamiento completado. ¡Puedes iniciar el reconocimiento!';
predictButton.disabled = false;
saveModelButton.disabled = false;
}
El modelo es una red neuronal convolucional (CNN), ideal para el procesamiento de imágenes. Las capas convolucionales (conv2d) extraen características de la imagen, como bordes y formas, mientras que las capas de agrupación (maxPooling2d) reducen la dimensionalidad de los datos.
La capa final (dense) clasifica las características extraídas, asignando una probabilidad a cada clase de objeto.
El método trainingModel.fit() inicia el proceso de entrenamiento, donde el modelo aprende a asociar las imágenes capturadas con sus respectivas etiquetas.
Reconocimiento en Tiempo Real (startPrediction)
Una vez entrenado, el modelo puede ser usado para reconocer objetos continuamente en el video de la cámara.
/**
* Inicia el bucle de predicción continua.
*/
async function startPrediction() {
if (!trainingModel) {
statusElement.textContent = 'Primero entrena o carga un modelo para poder reconocer objetos.';
return;
}
isPredicting = true;
predictButton.textContent = 'Detener Reconocimiento';
predictButton.classList.remove('bg-purple-500', 'hover:bg-purple-600');
predictButton.classList.add('bg-red-500', 'hover:bg-red-600');
trainButton.disabled = true;
addClassButton.disabled = true;
saveModelButton.disabled = true;
document.querySelectorAll('#captureButtonsContainer button').forEach(btn => btn.disabled = true);
while (isPredicting) {
const imgTensor = await webcam.capture();
const processedTensor = tf.tidy(() => {
return imgTensor.resizeBilinear([IMAGE_SIZE, IMAGE_SIZE])
.expandDims(0)
.toFloat()
.div(255);
});
const prediction = await trainingModel.predict(processedTensor).as1D();
const predictionData = prediction.dataSync(); // Obtiene los datos de probabilidad
const predictedClassIndex = prediction.argMax().dataSync()[0];
const confidence = predictionData[predictedClassIndex];
if (confidence < 0.50) {
statusElement.innerHTML = 'No hay nada para reconocer';
} else if (confidence >= 0.50 && confidence < 0.60) {
statusElement.innerHTML = 'Reconociendo...';
} else {
const predictedClassName = classLabels[predictedClassIndex];
statusElement.innerHTML = `Detectado: <span class="text-blue-600 font-bold">${predictedClassName}</span>`;
}
// Libera la memoria
imgTensor.dispose();
processedTensor.dispose();
prediction.dispose();
await tf.nextFrame();
}
predictButton.textContent = 'Iniciar Reconocimiento';
predictButton.classList.remove('bg-red-500', 'hover:bg-red-600');
predictButton.classList.add('bg-purple-500', 'hover:bg-purple-600');
trainButton.disabled = false;
addClassButton.disabled = false;
saveModelButton.disabled = false;
document.querySelectorAll('#captureButtonsContainer button').forEach(btn => btn.disabled = false);
}
/**
* Convierte un tensor de imagen a un Data URL (string de imagen comprimida).
* @param {tf.Tensor} tensor El tensor 4D que contiene la imagen (ej. [1, 64, 64, 3]).
* @returns {Promise<string>} La promesa que resuelve con la URL de datos comprimida.
*/
async function tensorToDataUrl(tensor) {
// Un-normaliza el tensor (escala de 0-1 a 0-255)
const imgTensor = tf.tidy(() => tensor.squeeze().mul(255).asType('int32'));
const canvas = document.createElement('canvas');
canvas.width = IMAGE_SIZE;
canvas.height = IMAGE_SIZE;
return new Promise((resolve) => {
tf.browser.toPixels(imgTensor, canvas).then(() => {
// Exporta la imagen como JPEG comprimido
resolve(canvas.toDataURL('image/jpeg', 0.8)); // 0.8 es un buen equilibrio entre tamaño y calidad
imgTensor.dispose();
});
});
}
El bucle while(isPredicting) se ejecuta repetidamente. En cada iteración, captura un nuevo fotograma, lo procesa y lo alimenta al modelo para obtener una predicción.
trainingModel.predict() devuelve un tensor con las probabilidades de cada clase. El código toma la clase con la probabilidad más alta y la muestra al usuario.
Las llamadas a dispose() son cruciales para liberar la memoria de la GPU, evitando que la aplicación se ralentice con el tiempo.
Guardar y Cargar el Modelo (saveModel y loadModel)
Estas funciones añaden una funcionalidad de persistencia. El modelo y los datos de entrenamiento pueden ser guardados en un archivo ZIP y luego cargados en una sesión posterior.
/**
* Guarda el modelo entrenado y los datos de entrenamiento en un archivo ZIP.
*/
async function saveModel() {
if (!trainingModel) {
statusElement.textContent = 'No hay un modelo entrenado para guardar.';
return;
}
statusElement.textContent = 'Generando archivos y empaquetando en ZIP...';
try {
// Guarda el modelo en un formato de archivo que puede ser cargado por TF.js
const artifacts = await trainingModel.save(tf.io.withSaveHandler(async artifacts => {
return artifacts;
}));
const zip = new JSZip();
// Archivos estándar del modelo TF.js
const modelJsonContent = JSON.stringify({
modelTopology: artifacts.modelTopology,
weightsManifest: artifacts.weightSpecs,
format: artifacts.format,
generatedBy: artifacts.generatedBy,
});
const weightsBinBlob = new Blob([artifacts.weightData], { type: 'application/octet-stream' });
zip.file('model.json', modelJsonContent);
zip.file('group1-shard1of1.bin', weightsBinBlob);
// Nuevo: Guarda las etiquetas y los datos de entrenamiento
const trainingDataToSave = [];
for (const cls of trainingData) {
const savedTensors = [];
for (const t of cls.tensors) {
const dataUrl = await tensorToDataUrl(t.tensor);
savedTensors.push({
data: dataUrl,
label: t.label
});
}
trainingDataToSave.push({ name: cls.name, tensors: savedTensors });
}
zip.file('training_data.json', JSON.stringify({
classLabels,
trainingData: trainingDataToSave
}));
const zipBlob = await zip.generateAsync({ type: "blob" });
saveAs(zipBlob, 'modelo_entrenamiento.zip');
statusElement.textContent = 'Modelo y datos de entrenamiento guardados como .zip.';
} catch (error) {
console.error('Error al guardar el modelo:', error);
statusElement.textContent = 'Error al guardar el modelo. Por favor, revisa la consola.';
}
}
saveModel() utiliza tf.io.withSaveHandler para obtener los archivos del modelo de TensorFlow.js. Luego, usa JSZip para empaquetar estos archivos junto con un archivo JSON que contiene las etiquetas y los datos de entrenamiento. Finalmente, file-saver.js inicia la descarga.
/**
* Carga un modelo y sus etiquetas desde un archivo ZIP.
*/
async function loadModel() {
const file = loadModelInput.files[0];
if (!file) {
statusElement.textContent = 'Por favor, selecciona un archivo .zip.';
return;
}
statusElement.textContent = 'Cargando y descomprimiendo archivos...';
try {
const zip = new JSZip();
const content = await zip.loadAsync(file);
const modelJsonFile = content.file('model.json');
const weightsBinFile = content.file('group1-shard1of1.bin');
const trainingDataFile = content.file('training_data.json');
if (!modelJsonFile || !weightsBinFile || !trainingDataFile) {
throw new Error("El archivo ZIP no contiene los archivos necesarios: model.json, group1-shard1of1.bin y training_data.json.");
}
if (isPredicting) {
isPredicting = false;
await tf.nextFrame();
}
// Carga los datos de entrenamiento para reconstruir el conjunto de datos
const trainingDataText = await trainingDataFile.async('string');
const loadedTrainingData = JSON.parse(trainingDataText);
classLabels = loadedTrainingData.classLabels;
// Limpia los datos de entrenamiento actuales y reconstruye a partir del archivo cargado
trainingData = [];
for (const cls of loadedTrainingData.trainingData) {
const tensors = [];
for (const t of cls.tensors) {
// Crea un nuevo tensor a partir de la imagen comprimida
const img = new Image();
img.src = t.data;
await new Promise(resolve => img.onload = resolve);
const tensor = tf.browser.fromPixels(img)
.resizeBilinear([IMAGE_SIZE, IMAGE_SIZE])
.expandDims(0)
.toFloat()
.div(255);
tensors.push({ tensor, label: t.label });
}
trainingData.push({ name: cls.name, tensors });
}
// Limpia y reconstruye los botones de captura con el conteo correcto
captureButtonsContainer.innerHTML = '';
trainingData.forEach((cls, index) => {
createCaptureButton(cls.name, index, cls.tensors.length);
});
// Carga el modelo TF.js
if (trainingModel) {
trainingModel.dispose();
}
const modelJsonText = await modelJsonFile.async('string');
const weightsBinData = await weightsBinFile.async('arraybuffer');
const handler = {
load: async () => {
const modelTopology = JSON.parse(modelJsonText);
const weights = new Float32Array(weightsBinData);
return {
modelTopology: modelTopology.modelTopology,
weightSpecs: modelTopology.weightsManifest,
weightData: weights.buffer
};
}
};
trainingModel = await tf.loadLayersModel(handler);
statusElement.textContent = 'Modelo y datos de entrenamiento cargados. ¡Puedes entrenar y reconocer!';
predictButton.disabled = false;
saveModelButton.disabled = false;
trainButton.disabled = false;
} catch (error) {
console.error('Error al cargar el modelo:', error);
statusElement.textContent = `Error al cargar el modelo: ${error.message}`;
}
}
loadModel() realiza el proceso inverso: descomprime el archivo ZIP, lee los archivos del modelo y los datos de entrenamiento, reconstruye el modelo y recrea la interfaz de usuario con los datos previamente guardados.
Manejadores de Eventos y Ejecución
Esta última parte del código se encarga de conectar todas las funciones anteriores a los botones y elementos de la interfaz, y de iniciar el programa cuando la página termina de cargar.
// Manejadores de eventos de los botones principales
addClassButton.addEventListener('click', addClass);
trainButton.addEventListener('click', trainModel);
predictButton.addEventListener('click', () => {
if (isPredicting) {
isPredicting = false;
} else {
startPrediction();
}
});
saveModelButton.addEventListener('click', saveModel);
loadModelInput.addEventListener('change', loadModel);
// Inicia la aplicación al cargar la página.
window.onload = init;
Este es todo el JavaScript utilizando en la aplicación de inteligencia artificial que reconoce objetos con TensorFlow.js y Keras
Gracias por estar atento a esta serie de tutoriales para aprender a hacer una aplicación web de reconocimientos de objetos utilizando TensorFlow.js y Keras.