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:

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

Valores impares: 1, 3, 5, 7, 9, 11, 13, 15

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>

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:

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

  1. Privacidad total: Las imágenes nunca salen del dispositivo del usuario.
  2. Baja latencia y velocidad: Todo se procesa localmente con WebAssembly, casi tan rápido como código nativo.
  3. Sin costos de servidor: No requiere backend, solo hosting estático.
  4. Multiplataforma: Funciona en cualquier navegador moderno.
  5. API similar a Python: Facilita la migración de proyectos existentes.
  6. Interactividad en tiempo real: Controles como sliders actualizan los resultados inmediatamente.

Limitaciones

  1. Tamaño del archivo: opencv.js tiene ~8 MB, la primera carga puede ser lenta.
  2. Memoria limitada: WebAssembly tiene restricciones; imágenes grandes pueden consumir muchos recursos.
  3. Operaciones avanzadas no disponibles: Machine learning, stitching, calibración, módulos contrib/third-party…
  4. Compatibilidad de navegador: Solo funciona en navegadores modernos con soporte WebAssembly.

Referencias


TODO