Filtros de imagen con lienzo

Ilmari Heikkinen

Introducción

El elemento lienzo HTML5 se puede usar para escribir filtros de imagen. Lo que debes hacer es dibujar una imagen en un lienzo, volver a leer los píxeles del lienzo y ejecutar el filtro en ellos. Luego, puedes escribir el resultado en un lienzo nuevo (o simplemente reutilizar el anterior).

¿Suena sencillo? Bien. ¡Manos a la orba!

La imagen de prueba original
La imagen de prueba original

Procesamiento de píxeles

Primero, recupera los píxeles de la imagen:

Filters = {};
Filters.getPixels = function(img) {
var c = this.getCanvas(img.width, img.height);
var ctx = c.getContext('2d');
ctx.drawImage(img);
return ctx.getImageData(0,0,c.width,c.height);
};

Filters.getCanvas = function(w,h) {
var c = document.createElement('canvas');
c.width = w;
c.height = h;
return c;
};

A continuación, necesitamos una forma de filtrar las imágenes. ¿Qué tal un método filterImage que tome un filtro y una imagen y muestre los píxeles filtrados?

Filters.filterImage = function(filter, image, var_args) {
var args = [this.getPixels(image)];
for (var i=2; i<arguments.length; i++) {
args.push(arguments[i]);
}
return filter.apply(null, args);
};

Cómo ejecutar filtros simples

Ahora que tenemos armada la canalización de procesamiento de píxeles, es hora de escribir algunos filtros simples. Para comenzar, convertiremos la imagen a escala de grises.

Filters.grayscale = function(pixels, args) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
// CIE luminance for the RGB
// The human eye is bad at seeing red and blue, so we de-emphasize them.
var v = 0.2126*r + 0.7152*g + 0.0722*b;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

Para ajustar el brillo, agrega un valor fijo a los píxeles:

Filters.brightness = function(pixels, adjustment) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
d[i] += adjustment;
d[i+1] += adjustment;
d[i+2] += adjustment;
}
return pixels;
};

Umbrar una imagen también es bastante simple. Solo debes comparar el valor de escala de grises de un píxel con el valor del umbral y establecer el color en función de eso:

Filters.threshold = function(pixels, threshold) {
var d = pixels.data;
for (var i=0; i<d.length; i+=4) {
var r = d[i];
var g = d[i+1];
var b = d[i+2];
var v = (0.2126*r + 0.7152*g + 0.0722*b >= threshold) ? 255 : 0;
d[i] = d[i+1] = d[i+2] = v
}
return pixels;
};

Convolución de imágenes

Los filtros de convolución son filtros genéricos muy útiles para el procesamiento de imágenes. La idea básica es que tomes la suma ponderada de un cuadrado de píxeles de la imagen de origen y la uses como valor de salida. Los filtros de convolución se pueden usar para desenfocar, enfocar, grabar en relieve, detectar bordes y muchas otras funciones.

Filters.tmpCanvas = document.createElement('canvas');
Filters.tmpCtx = Filters.tmpCanvas.getContext('2d');

Filters.createImageData = function(w,h) {
return this.tmpCtx.createImageData(w,h);
};

Filters.convolute = function(pixels, weights, opaque) {
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side/2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
// pad output by the convolution matrix
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
// go through the destination image pixels
var alphaFac = opaque ? 1 : 0;
for (var y=0; y<h; y++) {
for (var x=0; x<w; x++) {
  var sy = y;
  var sx = x;
  var dstOff = (y*w+x)*4;
  // calculate the weighed sum of the source image pixels that
  // fall under the convolution matrix
  var r=0, g=0, b=0, a=0;
  for (var cy=0; cy<side; cy++) {
    for (var cx=0; cx<side; cx++) {
      var scy = sy + cy - halfSide;
      var scx = sx + cx - halfSide;
      if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
        var srcOff = (scy*sw+scx)*4;
        var wt = weights[cy*side+cx];
        r += src[srcOff] * wt;
        g += src[srcOff+1] * wt;
        b += src[srcOff+2] * wt;
        a += src[srcOff+3] * wt;
      }
    }
  }
  dst[dstOff] = r;
  dst[dstOff+1] = g;
  dst[dstOff+2] = b;
  dst[dstOff+3] = a + alphaFac*(255-a);
}
}
return output;
};

Este es un filtro de nitidez de 3 × 3. Observa cómo se enfoca el peso en el píxel central. Para mantener el brillo de la imagen, la suma de los valores de la matriz debe ser uno.

Filters.filterImage(Filters.convolute, image,
[  0, -1,  0,
-1,  5, -1,
  0, -1,  0 ]
);

Este es otro ejemplo de un filtro de convolución, el desenfoque de cuadro. El desenfoque de caja genera el promedio de los valores de píxeles dentro de la matriz de convolución. Para ello, crea una matriz de convolución de tamaño NxN en la que cada uno de los pesos sea 1 / (NxN). De esta manera, cada uno de los píxeles dentro de la matriz contribuye con una cantidad igual a la imagen de salida, y la suma de los pesos es uno.

Filters.filterImage(Filters.convolute, image,
[ 1/9, 1/9, 1/9,
1/9, 1/9, 1/9,
1/9, 1/9, 1/9 ]
);

Podemos crear filtros de imagen más complejos combinando los filtros existentes. Por ejemplo, escribamos un filtro de Sobel. Un filtro Sobel calcula los gradientes verticales y horizontales de la imagen y combina las imágenes calculadas para encontrar los bordes en la imagen. Para implementar el filtro de Sobel, primero se aplica el ajuste de escala de grises de la imagen, se toman los gradientes horizontales y verticales y, por último, se combinan las imágenes de gradiente para formar la imagen final.

En cuanto a la terminología, "gradiente" en este caso hace referencia al cambio en el valor de píxeles en la posición de una imagen. Si un píxel tiene un vecino izquierdo con el valor 20 y un vecino derecho con el valor 50, el gradiente horizontal en el píxel sería de 30. El gradiente vertical tiene la misma idea, pero usa los vecinos de arriba y abajo.

var grayscale = Filters.filterImage(Filter.grayscale, image);
// Note that ImageData values are clamped between 0 and 255, so we need
// to use a Float32Array for the gradient values because they
// range between -255 and 255.
var vertical = Filters.convoluteFloat32(grayscale,
[ -1, 0, 1,
-2, 0, 2,
-1, 0, 1 ]);
var horizontal = Filters.convoluteFloat32(grayscale,
[ -1, -2, -1,
  0,  0,  0,
  1,  2,  1 ]);
var final_image = Filters.createImageData(vertical.width, vertical.height);
for (var i=0; i<final_image.data.length; i+=4) {
// make the vertical gradient red
var v = Math.abs(vertical.data[i]);
final_image.data[i] = v;
// make the horizontal gradient green
var h = Math.abs(horizontal.data[i]);
final_image.data[i+1] = h;
// and mix in some blue for aesthetics
final_image.data[i+2] = (v+h)/4;
final_image.data[i+3] = 255; // opaque alpha
}

Y hay muchos otros filtros de convolución geniales que están esperando que los descubras. Por ejemplo, intenta implementar un filtro de Laplace en el juguete de convolución anterior y mira qué hace.

Conclusión

Espero que este pequeño artículo te haya resultado útil para presentar los conceptos básicos de la escritura de filtros de imágenes en JavaScript con la etiqueta HTML de lienzo. Te recomiendo que implementes algunos filtros de imagen más, es muy divertido.

Si necesitas un mejor rendimiento de tus filtros, por lo general, puedes adaptarlos para usar sombreadores de fragmentos de WebGL y realizar el procesamiento de imágenes. Con los sombreadores, puedes ejecutar la mayoría de los filtros simples en tiempo real, lo que te permite usarlos para el procesamiento posterior de videos y animaciones.