El álgebra lineal y el procesamiento digital de imágenes. Parte IV. Editor de imágenes.

El álgebra lineal y el procesamiento digital de imágenes. Parte IV. Editor de imágenes.
27 Octubre 2016

En los tres artículos anteriores explicamos algunos conceptos básicos sobre el uso del álgebra lineal en el procesamiento digital de imágenes: cómo una imagen se puede expresar como una matriz, el concepto de filtros digitales y su representación mediante operaciones matriciales, y finalmente las transformaciones afines, su interpretación geométrica y su representación matricial utilizando matrices de 3x3.

En este artículo, el último de esta seria, mostraremos una implementación en JavaScript que cubre toda la teoría vista hasta ahora. También explicaremos cómo utilizar el objeto canvas de HTML5 para que puedas hacer tu propia implementación de los conceptos de procesamiento de imágenes.

Un sencillo editor de imágenes usando JavaScript

Cuando se mueve el cursor sobre la imagen, se puede ver, debajo de la imagen, una matriz de 5x5 de los píxeles que rodean el píxel que se encuentra en la posición del cursor. Utiliza las opciones para aplicar filtros y realizar transformaciones. Si lo deseas, puedes seleccionar tu propia imagen y probar.

Brillo
Contraste
Corrección Gamma
Ajuste del Color Rojo
Ajuste del Color Verde
Ajuste del Color Azul
Escalar Horizontalmente
Escalar Verticalmente
Mover Horizontalmente
Mover Verticalmente
Rotar
Transvección Horizontal
Transvección Vertical
Desenfocar

HTML5 Canvas

El elemento <canvas> en HTML5 te permite manipular, dibujar y visualizar gráficos utilizando Javascript.

<canvas id="mycanvas" width="200" height="200">

El elemento canvas tiene un método en el DOM llamado getContext, utilizado para obtener el contexto a renderizar y sus funciones de dibujo.

var mycanvas = document.getElementById('mycanvas');
var context = mycanvas.getContext('2d');

Aunque otros contextos deberán proveer diferentes tipos de renderizaciones, como WebGL que usa un contexto 3D basado sobre OpenGL ES, aquí nos enfocaremos en renderización de contextos 2D.

Usando las propiedades del objeto que referencia el contexto a renderizar se puede, por ejemplo, establecer el ancho, estilo y color de las líneas; y utilizando sus métodos se puede dibujar líneas, curvas, arcos y rectángulos. También se pueden realizar transformaciones como escalado, rotación y traslación, o cualquier otra transformación afín utilizando su representación matricial.

Puedes encontrar la especificación para el contexto 2D del elemento canvas de HTML en https://www.w3.org/TR/2dcontext/ y un buen tutorial en https://developer.mozilla.org/es/docs/Web/Guide/HTML/Canvas_tutorial

Dibujando imágenes y manipulando píxeles

Para dibujar imágenes en el canvas, se provee el método drawImage. Este método tiene tres variantes dependiendo de sus parámetros. Veamos uno de ellos:

ctx.drawImage(image, dx, dy, dWidth, dHeight);

Aquí, image es el elemento que se va a dibujar en el contexto, el cual permite cualquier fuente de imágenes tales como un HTMLImageElement, un HTMLVideoElement, un HTMLCanvasElement o un ImageBitmap; dx y dy son las coordenadas X, Y en las cuales colocar la esquina superior izquierda de la imagen en el canvas de destino; y dWidth y dHeight son el ancho y alto que se utilizarán para dibujar la imagen en el canvas, lo cual permite escalar la imagen al dibujarla.

Para manipular los píxeles en un canvas, se provee el método getImageData, el cual retorna un objeto ImageData que representa la información de los píxeles en un área del canvas.

Este método requiere los siguientes parámetros: las coordenadas, en píxeles, de la esquina superior izquierda del área rectangular que se copiará, así como el ancho y alto de dicha área.

Usando la propiedad data del objeto ImageData, se obtiene un arreglo unidimensional que contiene valores enteros entre 0 y 255 en el orden RGBA (Rojo, Verde, Azul y el canal Alfa. Para el canal Alfa, 0 es transparente y 255 es completamente visible). Esto significa que cada 4 elementos del arreglo, se obtiene un píxel, donde el primer valor es el canal rojo, el segundo el canal verde, el tercero el canal azul, y el último es el canal alfa.

Para que se tenga un mejor entendimiento de este array, en el siguiente código JavaScript se crea una matriz bidimensional de píxeles a partir de la propiedad data del objeto ImageData:

function getImgColorMatrix(pxlData, width, height){
  var matrix = new Array(height);
  for (var i = 0; i < height; i++){
    matrix[i] = new Array(width);
    for (var j = 0; j < width; j++){
      matrix[i][j] = GetPixelColor(pxlData, i, j, width);
    }
  }
  return matrix;
}

function GetPixelColor(pxlData, x, y, width){
  var index = (x * width + y) * 4;
  var pixel = {
    r : pxlData[index],
    g : pxlData[index + 1],
    b : pxlData[index + 2]
  };
  return pixel;
}

Programando el Editor de Imágenes

En el editor de imágenes se usaron dos funciones básicas, una para filtrar la imagen y otra para realizar transformaciones afines (aun cuando el objeto que referencia el contexto a renderizar provee métodos para realizar transformaciones, decidimos hacerlas manipulando los píxeles, para demostrar la teoría explicada en los artículos previos).

La función para transformar las imágenes recibe el contexto a renderizar (ctx), el ancho (w) y la altura (h) de la imagen, y la matriz de la transformación como un arreglo.

Como se explicó en el artículo anterior, las transformaciones afines pueden ser representadas utilizando una matriz de 3x3:

T =   
a c e
b d f
0 0 1
 

Como la última fila es fija, un forma común de representar esta matriz es utilizando un vector de 6 componentes [a, b, c, d, e, f] (esta es la forma en la matriz de transformaciones se expresa cuando se utiliza el método transform del objeto que referencia el contexto a renderizar).

Usando los conceptos de coordenadas proyectadas explicados en el artículo anterior, los filtros lineales se pueden expresar como el producto de una matriz de 4x4 por un vector de 4 componentes, que representa el vector RGB. De esta forma, las siguientes operaciones:

 
R'
G'
B'
   =   
a d g
b e h
c f i
   •   
R
G
B
   +   
j
k
l
 

Se pueden expresar como:

 
R'
G'
B'
1
   =   
a d g j
b e h k
c f i l
0 0 0 1
   •   
R
G
B
1
 

Utilizamos este concepto para implementar la función filter, la cual recibe el contexto a renderizar (ctx), el ancho (w) y la altura (h) de la imagen, y la matriz del filtro como un vector unidimensional [a, b, c, d, e, f, g, h, i, j, k, l].

var filter = function(ctx, w, h, matrix){
  var imageData = ctx.getImageData( 0, 0, w, h);
  var pxlData = imageData.data;
  for (var i = 0; i < pxlData.length; i += 4){
    var r = pxlData[i];
    var g = pxlData[i + 1];
    var b = pxlData[i + 2];
    pxlData[i]     = matrix[0]*r + matrix[3]*g + matrix[6]*b + matrix[9];
    pxlData[i + 1] = matrix[1]*r + matrix[4]*g + matrix[7]*b + matrix[10];
    pxlData[i + 2] = matrix[2]*r + matrix[5]*g + matrix[8]*b + matrix[11];
  }
  ctx.putImageData(imageData, 0, 0);
}

var transform = function(ctx, w, h, matrix){
  var pxlData = ctx.getImageData( 0, 0, w, h).data;
  var imageData = ctx.createImageData(w, h);
  var newPxlData = imageData.data;
  for (var x = 0; x < w; x++){
    for (var y = 0; y < h; y++){
      var nx = Math.floor(matrix[0]*x + matrix[2]*y + matrix[4]);
      var ny = Math.floor(matrix[1]*x + matrix[3]*y + matrix[5]);
      if (nx >= 0 && nx < w && ny>=0 && ny < h){
        newPxlData[(y*w + x)*4]     = pxlData[(ny*w + nx)*4];
        newPxlData[(y*w + x)*4 + 1] = pxlData[(ny*w + nx)*4 + 1];
        newPxlData[(y*w + x)*4 + 2] = pxlData[(ny*w + nx)*4 + 2];
        newPxlData[(y*w + x)*4 + 3] = pxlData[(ny*w + nx)*4 + 3];
      }
    }
  }
  ctx.putImageData(imageData, 0, 0);
}

Una vez que los píxeles son manipulados, se utiliza el método putImageData para copiar los datos de los nuevos píxeles de la imagen de vuelta al canvas.

Note que en la función transform, un nuevo objeto ImageData es creado utilizando el método createImageData(). En ese método creamos el objeto se crea con las dimensiones especificadas, pero se puede utilizar otro objeto ImageData como parámetro de manera que el nuevo objeto tenga las mismas dimensiones que éste.

Como la corrección gamma no puede ser expresado como el producto de matrices, ella tiene su propia función, donde ctx, x y y tienen el mismo significado que en las funciones previas, y factor es un parámetro con el grado de corrección:

var gamma = function(ctx, w, h, factor){
  var imageData = ctx.getImageData( 0, 0, w, h);
  var pxlData = imageData.data;
  for (var i = 0; i < pxlData.length; i += 4){
    pxlData[i]     =  255*Math.pow(pxlData[i] / 255, 1/factor);
    pxlData[i + 1] = 255*Math.pow(pxlData[i + 1] / 255, 1/factor);
    pxlData[i + 2] = 255*Math.pow(pxlData[i + 2] / 255, 1/factor);
  }
  ctx.putImageData(imageData, 0, 0);
}

A continuación, las lista de filtros y transformaciones implementadas, así como la matriz utilizada. El parámetro value es el valor obtenido del control correspondiente en la interfaz.

Escala de grises

filter(ctx, w, h, [1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 1/3, 0, 0, 0]);

Invertir colores

filter(ctx, w, h, [-1, 0, 0, 0, -1, 0, 0, 0, -1, 255, 255, 255]);

Sepia

filter(ctx, w, h, [0.393, 0.349, 0.272, 0.769, 0.686, 0.534, 0.189, 0.168, 0.131, 0, 0, 0]);

Brillo

filter(ctx, w, h, [1, 0, 0, 0, 1, 0, 0, 0, 1, value, value, value]);

Contraste

var f = (259 * (value + 255)) / (255 * (259 - value));
filter(ctx, w, h, [f, 0, 0, 0, f, 0, 0, 0, f, 128*(1-f), 128*(1-f), 128*(1-f)]);

Corrección Gamma

gamma(ctx, w, h, value);

Ajuste del color rojo

filter(ctx, w, h, [1, 0, 0, 0, 1, 0, 0, 0, 1, value, 0, 0]);

Ajuste del color verde

filter(ctx, w, h, [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, value, 0]);

Ajuste del color azul

filter(ctx, w, h, [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, value]);

Volteo vertical

transform(ctx, w, h, [1, 0, 0, -1, 0, h-1]);

Volteo horizontal

transform(ctx, w, h, [-1, 0, 0, 1, w-1, 0]);

Escalar horizontalmente (scale and centre)

if (value != 0){
  transform(ctx, w, h, [1/value, 0, 0, 1, w*(1 - 1/value)/2, 0]);
}

Escalar verticalmente (scale and centre)

if (value != 0) {
  transform(ctx, w, h, [1, 0, 0, 1/value, 0, h*(1 - 1/value)/2]);
}

Mover horizontalmente

transform(ctx, w, h, [1, 0, 0, 1, -w*value/100, 0]);

Mover verticalmente

transform(ctx, w, h, [1, 0, 0, 1, 0, -h*value/100]);

Rotar (alrededor del centro de la imagen)

var cos = Math.cos(value*Math.PI/180);
var sin = Math.sin(value*Math.PI/180);
transform(ctx, w, h, [cos, sin, -sin, cos, -cos*w/2 + sin*h/2 + w/2, -sin*w/2 - cos*h/2 + h/2]);

Transvección horizontal (alrededor del centro de la imagen)

var tan = Math.tan(value*Math.PI/180);
transform(ctx, w, h, [1, tan, 0, 1, 0, -tan*w/2]);

Transvección vertical (alrededor del centro de la imagen)

var tan = Math.tan(value*Math.PI/180);
transform(ctx, w, h, [1, 0, tan, 1, -tan*h/2, 0]);

El desenfoque fue incluido como un ejemplo de un filtro no lineal, usando una implementación obtenida de:

http://www.quasimondo.com/BoxBlurForCanvas/FastBlurDemo.html

Entradas relacionadas

Aún no hay comentarios


Tu comentario

Nunca compartiremos su dirección de correo.
Los comentarios son moderados antes de hacerlos visible para todos. La foto de perfil se obtiene de Gravatar usando su correo electrónico.