Manipulando canvas pixel a pixel

Con la llegada del canvas al browser, la posibilidad de trabajar con gráficos creció exponencialmente, permitiendo transformar y editar imágenes de manera compleja directamente en el cliente de manera bastante sencilla.

Hay que recordar (y advertir), que estas tecnologías aún están en formación, por lo cual es de esperar que sufra cambios en el futuro. En particular, la iteración de pixels aquí expuesta puede ser optimizada (por el momento únicamente en Firefox) de la manera que exponen en esta entrada: Faster Canvas Pixel Manipulation with Typed Arrays (Mozilla Hacks).

Obteniendo y recorriendo los pixels

Dado un elemento <canvas>, podemos obtener un array que contiene todos pixels con el método getImageData del context2d:

var context = canvas.getContext('2d');

var imageData = context.getImageData(0, 0, canvas.width, canvas.height);

Que nos devuelve un objeto con la siguiente estructura:

Estructura del objeto imageData

El tipo de dato del objeto data variará de browser a browser, debiendo ser según la especificación Uint8ClampedArray (tal como se ve en la imagen superior). En los browsers que no estén al día con la especificación, vamos a encontrar un objeto del tipo CanvasPixelArray (que actualmente se encuentra deprecado por la especificación).

El array obtenido (independientemente de su tipo), es una estructura lineal que contiene cada píxel con su respectiva información (colores y canal alfa, RGBA) de la siguiente manera. Supongamos que tenemos un canvas de 3×3 (es decir, contiene 9 pixels), cuya vista ampliada a nivel píxel sería la siguiente:

Vista ampliada del canvasSi obtenemos el imageData del canvas en cuestión, tendríamos un array de 36 valores (pixels * 4) con los siguientes elementos (correspondiéndose con los pixels de izquierda a derecha y de arriba a abajo):

// Cada pixel ocupa 4 posiciones,
// que contienen la informacion de
// RGBA del mismo
[
    // Primer pixel (rojo)
    255, 0, 0, 0,
    // Segundo pixel (azul)
    0, 0, 255, 0,
    // Tercer pixel (verde)
    0, 255, 0, 0,
    // Cuarto pixel (azul)
    0, 0, 255, 0,
    // Quinto pixel (rojo)
    255, 0, 0, 0,
    // Sexto pixel (azul)
    0, 0, 255, 0,
    // Septimo pixel (verde)
    0, 255, 0, 0,
    // Octavo pixel (azul)
    0, 0, 255, 0,
    // Noveno pixel (rojo)
    255, 0, 0, 0,
]

De esta manera, si quisiéramos obtener un dato específico de un pixel en particular sabiendo su posición (x, y) (recordemos que tomamos el punto (0,0) en el vértice superior derecho), deberíamos utilizar la fórmula:

value = imageData.data[ y * (imageData.width * 4) + (x * 4) + componentIndex];

// O simplificado
value = imageData.data[ ( y * imageData.width + x ) * 4 + componentIndex];

Por ejemplo, obtenemos la información RGBA de un píxel en la posición (150, 100):

var data = imageData.data;

var x = 150;
var y = 100;

var components = [
    data[ ( y * imageData.width + x ) * 4 + 0],
    data[ ( y * imageData.width + x ) * 4 + 1],
    data[ ( y * imageData.width + x ) * 4 + 2],
    data[ ( y * imageData.width + x ) * 4 + 3]
]; // [Red, Green, Blue, Alpha] -> [226, 182, 155, 255]

De igual manera podemos recorrer todos los pixels del canvas:

var data = imageData.data;
var height = imageData.height;
var width = imageData.width;
var red, green, blue, alpha;

for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {

        red = data[( y * imageData.width + x ) * 4 + 0];
        green = data[( y * imageData.width + x ) * 4 + 1];
        blue = data[( y * imageData.width + x ) * 4 + 2];
        alpha = data[( y * imageData.width + x ) * 4 + 3];

        // ...
    }
}

Modificando imágenes

Suponiendo que tenemos una imagen, podríamos (entre otras cosas) modificarle el brillo (el filtro más sencillo), siguiendo los siguientes pasos:

  1. Crear un canvas del tamaño de la imagen.
  2. Dibujar la imagen en el canvas.
  3. Obtener todos los pixels del canvas.
  4. Recorrer los pixels y ajustarle los valores R, G y B a cada uno de ellos.
  5. Crear un canvas y llenarlo con la información modificada.
  6. Obtener una imagen del canvas.

Para los primeros tres pasos podemos implementar una simple función que devuelva el imageData correspondiente a una imagen dada:

/**
 * Crea y devuelve un imageData en base
 * a un elemento img dado
 *
 * @param img
 * @return Object
 */
function getImageDataFromAnImageElement(img) {
    var canvas, context, width, height, imageData;

    canvas = document.createElement('canvas');
    context = canvas.getContext('2d');

    canvas.width = img.width;
    canvas.height = img.height;

    context.drawImage(img, 0, 0);

    return context.getImageData(0, 0, canvas.width, canvas.height);
}

Salteando el cuarto, los pasos 5 y 6 también pueden agruparse en una función:

/**
 * Crea y devuelve una imagen en base
 * a un imageData
 *
 * @param imageData
 * @return Image
 */
function getImageFromImageData(imageData) {
    var canvas, context, result;

    canvas = document.createElement('canvas');
    canvas.width = imageData.width;
    canvas.height = imageData.height;
    context = canvas.getContext('2d');

    context.putImageData(imageData, 0, 0);

    result = new Image;
    result.src = canvas.toDataURL('image/png');

    return result;
}

Y finalmente, el paso 4 es el puntual del filtro en cuestión (en este caso, brillo):

/**
 * Aplica brillo a la imagen
 *
 * @param img
 * @param level
 */
function brightness(img, level) {
    var width, height, imageData, data;

    width = img.width;
    height = img.height;

    // Obtenemos la informacion con la funcion correspondiente
    imageData = getImageDataFromAnImageElement(img);
    data = imageData.data;

    // Recorremos los pixels y aumentamos su valor
    // RGB (no tocamos el alpha) en el nivel especificado
    for (var y = 0; y < height; y++) {
        for (var x = 0; x < width; x++) {
            data[(y * width + x) * 4 + 0] += level;
            data[(y * width + x) * 4 + 1] += level;
            data[(y * width + x) * 4 + 2] += level;
        }
    }

    // Generamos la imagen con la funcion correspondiente
    return getImageFromImageData(imageData);
}

Entonces, la siguiente implementación:

var body = document.getElementsByTagName('body')[0];
var img = document.getElementsByTagName('img')[0];

body.appendChild(brightness(img, -100));
body.appendChild(brightness(img, 100));

Daría como resultado (imagen original, con brillo -100 y con brillo 100):

Aplicacion de brillo

Esta implementación puede verse en vivo en este link (requiere browsers modernos por supuesto).

Otro ejemplo: Reflejo

Aquí se puede ver la aplicación de un reflejo parcial de una imagen utilizando canvas.

Aplicacion de canvas para generar reflejo sobre una imagen

Leer más:



5 views shared on this article. Join in...

  1. _cronos2 dice:

    Hey, qué bueno el artículo, pero creo que tiene dos erratas :3
    “Si obtenemos el imageData del canvas en cuestión, tendríamos un array de 32 valores (pixels * 4)”
    Si son 9 píxeles, ¿no serán 36 elementos?
    Y en el snippet justo debajo de eso te saltaste el 4º píxel en el array.
    Y una duda, si el brillo se modifica aumentando/disminuyendo el RGB del píxel, podría quedar con valores por encima de 255, o por debajo de 0. ¿Cómo soluciona eso el browser?
    Saludos 😀

    • Gracias por las correcciones 🙂

      Con respecto a la duda que planteás, habría que revisar la especificación, pero por lo que probé hasta el momento resulta que RGB >= 255 es blanco, sin importar cuanto te pases.

      Si querés probar, en la página de ejemplo (link) podes ejecutar:

      add(brightness(img(), -240));

  2. Gaby dice:

    Hola! Muy bueno tu post, aunque tengo un problema: ¿cómo puedo hacer eso mismo pero para traer una imágen desde otro dominio? Porque si hago ésto, me arroja error:

    var img = document.createElement(“img”);
    img.src = ‘http://www.google.com.ar/intl/es_ALL/images/logos/images_logo_lg.gif’;

    [11:58:41.811] uncaught exception: [Exception… “Component returned failure code: 0x80040111 (NS_ERROR_NOT_AVAILABLE) [nsIDOMCanvasRenderingContext2D.drawImage]” nsresult: “0x80040111 (NS_ERROR_NOT_AVAILABLE)” location: “JS frame :: resource://greasemonkey/runScript.js :: getImageDataFromAnImageElement :: line 46” data: no]

    • Lucas Alonso dice:

      Ante todo muy bien explicado gracias.

      En cuanto a tu pregunta Gaby.

      Yo yo que conozco y creo no estar equivocado es que javascript no permite acceder a datos de otro dominio por seguridad.

      Si no es así corregidme.

      Un Saludo



Pings to this post

  1. […] técnica, permite generar resultados muy vistosos con muy poco código, tal como puede verse en un artículo que he desarrollado previamente (o en este gran artículo de Stoyan […]


Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Comment

You may use these tags : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>