OpenCV.js
Introducción
En este post vemos cómo ejecutar código de PdI y VpC en el navegador, lo que será muy útil por el perfil de este blog.
TL;DR:
- Se carga desde el CDN oficial al abrir la página.
- La API de OpenCV.js se usa en JavaScript, similar a
cv2en Python. - Todo el procesamiento ocurre en el cliente, sin necesidad de servidor backend.
Procesamiento de imágenes interactivo (web)
Esta demostración utiliza OpenCV.js, que es OpenCV compilado a JavaScript/WebAssembly, permitiendo ejecutar operaciones de procesamiento de imágenes directamente en el navegador sin necesidad de servidor backend. La API es muy similar a la de OpenCV en Python.
Carga una imagen
Controles
Resultado
Carga una imagen para comenzar
Código equivalente en Python
El código JavaScript que se ejecuta es equivalente a este código Python:
import cv2
import numpy as np
# Cargar imagen
img = cv2.imread('imagen.jpg')
# Convertir a escala de grises
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Aplicar desenfoque gaussiano
blur = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
# Binarización
_, binary = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)
# Detección de bordes (Canny)
edges = cv2.Canny(gray, low_threshold, high_threshold)
# Morfología
kernel = np.ones((5,5), np.uint8)
eroded = cv2.erode(binary, kernel, iterations=1)
dilated = cv2.dilate(binary, kernel, iterations=1)
# Ecualización de histograma
equalized = cv2.equalizeHist(gray)
Sintaxis utilizada y conceptos relacionados
¿Qué es OpenCV.js?
OpenCV.js es OpenCV compilado a WebAssembly, una tecnología que permite ejecutar código de bajo nivel en el navegador. Esto significa que podemos utilizar las mismas funciones de visión por computadora que en Python o C++, pero directamente en JavaScript, en la parte del cliente web (tú).
Cómo se carga OpenCV.js
<script async src="https://docs.opencv.org/4.8.0/opencv.js"
onload="onOpenCvReady();"
type="text/javascript"></script>
- async: Carga el script de forma asíncrona sin bloquear la página
- onload: Ejecuta una función cuando OpenCV.js se ha cargado completamente
- src: URL del CDN oficial de OpenCV
Estructura Básica del Código
1. Variables Globales y Estado
let cvReady = false; // Flag que indica si OpenCV está cargado
let srcMat = null; // Matriz de OpenCV (equivalente a numpy array)
let currentImage = null; // Información de la imagen actual
2. Función de Inicialización Robusta
La inicialización de OpenCV.js verifica múltiples mecanismos de carga para garantizar compatibilidad:
function initializeOpenCV() {
// Caso 1: cv es una Promesa (versiones recientes)
if (cv instanceof Promise) {
cv.then((resolvedCv) => {
window.cv = resolvedCv;
cvReady = true;
onOpenCVInitialized();
});
}
// Caso 2: Usar callback onRuntimeInitialized
else if (cv && cv.onRuntimeInitialized) {
cv['onRuntimeInitialized'] = () => {
cvReady = true;
onOpenCVInitialized();
};
}
// Caso 3: Fallback con verificación directa
else {
setTimeout(() => {
if (typeof cv !== 'undefined' && cv.Mat) {
cvReady = true;
onOpenCVInitialized();
}
}, 500);
}
}
Verificaciones críticas antes de usar OpenCV:
typeof cv !== 'undefined': Verifica que el objetocvexistacvReady: Bandera que confirma que OpenCV está completamente inicializadosrcMat && !srcMat.empty(): Verifica que haya una imagen válida cargada
Sintaxis de OpenCV.js
Es prácticamente idéntico a en otros lenguajes, usando cv.Mat y cuidando de la reserva de memoria para las variables, inicialización y su posterior liberación:
Mat (Matriz)
// Crear una nueva matriz
let mat = new cv.Mat();
// Leer una imagen desde un canvas
srcMat = cv.imread(canvas);
// Verificar si la matriz está vacía
if (!srcMat.empty()) {
// Procesar la imagen
}
// Liberar memoria (IMPORTANTE)
mat.delete();
Conversión de Espacios de Color
// RGBA a Grises
cv.cvtColor(srcMat, dst, cv.COLOR_RGBA2GRAY);
// Grises a RGBA (para mostrar en canvas)
cv.cvtColor(dst, dst, cv.COLOR_GRAY2RGBA);
// RGBA a RGB
cv.cvtColor(srcMat, dst, cv.COLOR_RGBA2RGB);
Operaciones de Procesamiento Explicadas
1. Desenfoque Gaussiano
case 'blur':
// Asegurar tamaño impar del kernel
const kernelSize = blurValue % 2 === 0 ? blurValue + 1 : blurValue;
// Convertir a RGB (OpenCV.js funciona mejor con RGB que RGBA)
cv.cvtColor(srcMat, dst, cv.COLOR_RGBA2RGB);
// Aplicar filtro Gaussiano
cv.GaussianBlur(
dst, // imagen de entrada
dst, // imagen de salida (puede ser la misma)
new cv.Size(kernelSize, kernelSize), // tamaño del kernel
0, 0, // desviación estándar en X e Y
cv.BORDER_DEFAULT // tipo de borde
);
// Convertir de vuelta a RGBA para mostrar
cv.cvtColor(dst, dst, cv.COLOR_RGB2RGBA);
break;
2. Binarización (Threshold)
case 'threshold':
// 1. Convertir a escala de grises
let gray = new cv.Mat();
cv.cvtColor(srcMat, gray, cv.COLOR_RGBA2GRAY);
// 2. Aplicar umbral
cv.threshold(
gray, // imagen en grises
dst, // imagen binaria de salida
thresholdValue, // valor del umbral (0-255)
255, // valor máximo
cv.THRESH_BINARY // tipo de umbralización
);
// 3. Convertir a RGBA para visualización
cv.cvtColor(dst, dst, cv.COLOR_GRAY2RGBA);
// 4. Limpiar memoria
gray.delete();
break;
3. Detección de Bordes (Canny)
case 'canny':
// Convertir a grises primero
let grayCanny = new cv.Mat();
cv.cvtColor(srcMat, grayCanny, cv.COLOR_RGBA2GRAY);
// Aplicar algoritmo de Canny
cv.Canny(
grayCanny, // imagen en grises
dst, // imagen de bordes
cannyLow, // umbral bajo
cannyHigh, // umbral alto
3, // tamaño del kernel de Sobel
false // usar L2 gradient?
);
// Convertir a RGBA
cv.cvtColor(dst, dst, cv.COLOR_GRAY2RGBA);
grayCanny.delete();
break;
4. Operaciones Morfológicas
case 'morphology':
// 1. Convertir a grises y binarizar
let grayMorph = new cv.Mat();
let binaryMorph = new cv.Mat();
cv.cvtColor(srcMat, grayMorph, cv.COLOR_RGBA2GRAY);
cv.threshold(grayMorph, binaryMorph, 127, 255, cv.THRESH_BINARY);
// 2. Crear kernel para operaciones morfológicas
let kernel = cv.getStructuringElement(
cv.MORPH_RECT, // forma del kernel (rectangular)
new cv.Size(5, 5) // tamaño del kernel
);
// 3. Aplicar dilatación
cv.dilate(
binaryMorph, // imagen binaria
dst, // resultado
kernel, // kernel estructural
new cv.Point(-1, -1), // punto de anclaje (centro)
1 // número de iteraciones
);
// 4. Limpiar y convertir
cv.cvtColor(dst, dst, cv.COLOR_GRAY2RGBA);
grayMorph.delete();
binaryMorph.delete();
kernel.delete();
break;
5. Ecualización de Histograma
case 'histogram':
let grayHist = new cv.Mat();
// Convertir a grises
cv.cvtColor(srcMat, grayHist, cv.COLOR_RGBA2GRAY);
// Ecualizar histograma para mejorar contraste
cv.equalizeHist(grayHist, dst);
// Convertir de vuelta a color
cv.cvtColor(dst, dst, cv.COLOR_GRAY2RGBA);
grayHist.delete();
break;
Manejo de Imágenes del Usuario
document.getElementById('imageInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
const img = new Image();
img.onload = function() {
// Crear canvas temporal
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Dibujar imagen en canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// Procesar imagen
processImageFromCanvas(canvas);
};
img.src = event.target.result;
};
reader.readAsDataURL(file); // Leer archivo como URL de datos
}
});
Gestión de Memoria
OpenCV.js usa WebAssembly, que maneja memoria manualmente. Siempre se debería liberar la memoria:
// ALWAYS delete Mat objects when done
let tempMat = new cv.Mat();
// ... usar tempMat ...
tempMat.delete(); // ← IMPORTANTE
// En caso de error, limpiar todo
try {
// operaciones...
} catch (e) {
// Limpiar todas las matrices
if (tempMat) tempMat.delete();
throw e;
}
// Limpiar al salir de la página
window.addEventListener('beforeunload', function() {
if (srcMat) {
srcMat.delete();
}
});
Configuración del Canvas de Salida
// Mostrar imagen procesada en canvas
cv.imshow(outputCanvas, dst);
// El canvas debe tener las dimensiones correctas
outputCanvas.width = dst.cols;
outputCanvas.height = dst.rows;
Optimizaciones Importantes
// 1. Reutilizar matrices cuando sea posible
let reusableMat = new cv.Mat();
// 2. Redimensionar imágenes grandes
if (srcMat.cols > 1000 || srcMat.rows > 1000) {
let resized = new cv.Mat();
cv.resize(srcMat, resized, new cv.Size(800, 600));
srcMat.delete();
srcMat = resized;
}
// 3. Usar requestAnimationFrame para actualizaciones suaves
function updateProcessing() {
requestAnimationFrame(() => {
applyProcessing();
});
}
Ventajas y limitaciones
Ventajas
- Privacidad total: Las imágenes nunca salen del dispositivo del usuario.
- Baja latencia y velocidad: Todo se procesa localmente con WebAssembly, casi tan rápido como código nativo.
- Sin costos de servidor: No requiere backend, solo hosting estático.
- Multiplataforma: Funciona en cualquier navegador moderno.
- API similar a Python: Facilita la migración de proyectos existentes.
- Interactividad en tiempo real: Controles como sliders actualizan los resultados inmediatamente.
Limitaciones
- Tamaño del archivo:
opencv.jstiene ~8 MB, la primera carga puede ser lenta. - Memoria limitada: WebAssembly tiene restricciones; imágenes grandes pueden consumir muchos recursos.
- Operaciones avanzadas no disponibles: Machine learning, stitching, calibración, módulos contrib/third-party…
- Compatibilidad de navegador: Solo funciona en navegadores modernos con soporte WebAssembly.
Referencias
- OpenCV.js Documentation
- WebAssembly Documentation
- Ejemplos de visualizaciones interactivas en Jekyll
- Trucos con Knitr y Plotly
- cv.Mat is not a constructor opencv
TODO
- La idea es pensar a futuro cómo poder incrustar código Python, por ejemplo, en snippets dentro de estos posts, para extender funcionalidades de librerías como Opencv.js, que pueden estar limitadas. Es un tema a estudiar, porque no se puedes ejecutar Python directamente en Jekyll, ya que genera sitios estáticos y el servidor solo sirve HTML, CSS y JS, pero se podría desplegar el código Python en un backend (Railway, Render, Streamlit Cloud…) y luego incrustarlo o consumirlo desde el sitio estático mediante <iframe> o llamadas API (fetch), extendiendo las librerías en cliente con funcionalidades avanzadas o incluso GPU.