Filtros de imagen con lienzo

Ilmari Heikkinen

Introducción

El elemento lienzo de HTML5 se puede usar para escribir filtros de imágenes. Lo que debes hacer es dibujar una imagen en un lienzo, 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 simple? ¡Bien! ¡Manos a la orba!

La imagen de prueba original
La imagen de prueba original

Píxeles de procesamiento

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 manera de filtrar las imágenes. ¿Y 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);
};

Ejecuta filtros simples

Ahora que tenemos la canalización de procesamiento de píxeles armada, es hora de escribir algunos filtros simples. Para comenzar, convirtamos 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 en 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;
};

Imágenes con rotación

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 rectángulo de píxeles de la imagen de origen y la uses como valor de salida. Estos filtros se pueden usar para difuminar, mejorar la nitidez, realizar grabados 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 x 3. Observa cómo 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 del cuadro. El desenfoque del cuadro genera el promedio de los valores de píxeles dentro de la matriz de convolución. La forma de hacerlo es crear una matriz de convolución de tamaño NxN en la que cada uno de los pesos sea 1 / (NxN). De esa manera, cada uno de los píxeles dentro de la matriz contribuye en 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 de Sobel calcula los gradientes verticales y horizontales de la imagen, y combina las imágenes calculadas para encontrar bordes en la imagen. Implementamos el filtro Sobel en este caso con la escala de grises de la imagen, luego, mediante el uso de los gradientes horizontales y verticales, y, por último, la combinación de las imágenes de gradientes para crear la imagen final.

Con respecto a la terminología, “gradiente” significa aquí el cambio en el valor de píxeles en una posición de imagen. Si un píxel tiene un vecino izquierdo con un valor de 20 y un vecino derecho con un valor de 50, el gradiente horizontal del píxel sería 30. El gradiente vertical tiene la misma idea, pero usa los vecinos 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
}

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

Conclusión

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

Si necesitas un mejor rendimiento de los filtros, por lo general, puedes transferirlos para que usen sombreadores de fragmentos WebGL que realicen el procesamiento de las 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.