Vimos en otro
artículo cómo convertir una imagen a escala de grises con javascript, utilizando el objeto canvasPixelArray. Sin embargo, la conclusión a la que habíamos llegado en aquel momento era que no era muy recomendable usar javascript para este tipo de manipulaciones debido a que el tiempo de proceso era poco aceptable, sobre todo comparándolo con el que insume Explorer, que al usar código de máquina hace lo mismo pero en mucho menos tiempo.
Pero
José Antonio Pérez, a quien tuve la suerte de conocer gracias a
anieto2k, me hizo ver que los tiempos de conversión pueden mejorarse notablemente corrigiendo el código que mostramos en aquella oportunidad. En concreto, lo que me señaló José Antonio es lo siguiente:
... la lentitud que muestra el procedimiento puede corregirse de forma notable. Cada vez que tocas un pixel realizas la siguiente llamada:
context.putImageData(iData, 0, 0);
Esto implica hacer copiar todo el array de pixels cada vez que tocas uno, es decir, a lo largo del procedimiento se realizan whith*height "repintados" del Canvas.
Totalmente cierto. Podemos comparar el funcionamiento de ambos códigos y veremos que la diferencia, luego de aplicar esta sugerencia, es más que notable:
Código sin optimizar:
Código optimizado:
El código del ejemplo optimizado (no aún el propuesto por José Antonio, que veremos luego y que es mucho más elegante y brinda más posibilidades) es el siguiente:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Escala de grises con javascript</title>
<script>
function toGrayScale(im){
var inicio=new Date().getTime();
if(document.createElement("canvas").getContext){
var ref = document.createElement("canvas");
ref.width = im.width || im.offsetWidth;
ref.height =im.height || im.offsetHeight;
var context = ref.getContext("2d");
context.drawImage(im,0,0);
var iData=context.getImageData(0,0, ref.width, ref.height);
im.parentNode.replaceChild(ref,im);
var data = iData.data;
var length = data.length;
document.getElementById('log').innerHTML='procesando...';
for (var i = 0;i<length;i+=4) {
var average = (data[i]+data[i+1]+data[i+2])/3;
data[i] = average;
data[i+1] = average;
data[i+2] = average;
}
context.putImageData(iData, 0, 0);
document.getElementById('log').innerHTML=-(inicio-new Date().getTime())/1000;
}else{
im.style.filter = 'progid:DXImageTransform.Microsoft.BasicImage(grayscale=1)';
document.getElementById('log').innerHTML=-(inicio-new Date().getTime())/1000;
}
}
</script>
</head>
<body>
<img style="cursor:pointer" onclick="toGrayScale(this)" id="im" src="4389e6d.jpg" />
<div id="log" style="font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px">click sobre la imagen para pasar a escala de grises</div>
</body>
</html>
Debo confesar que en el momento en que escribí el anterior artículo había usado estas imágenes tan pequeñas para que los ejemplos demoraran un tiempo razonable, pero ahora podemos ver que el experimento puede realizarse con imágenes más grandes sin problemas:
El código propuesto por José es bastante más completo, ya que permite dividir el proceso en lotes de dimensiones variables, lo cual brinda otras posibilidades:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Escala de grises con javascript</title>
<script>
var log = function(texto){
var logElement = document.getElementById('log');
logElement.innerHTML = texto;
}
var modifyPixel = function(data,i) {
var average = (data[i]+data[i+1]+data[i+2])/3;
data[i] = average;
data[i+1] = average;
data[i+2] = average;
}
var rectangleToGray = function(context,left,top,width,height){
var imageData = context.getImageData(left,top,width,height);
var data = imageData.data;
var length = data.length;
for (var i = 0;i<length;i+=4) {
modifyPixel(data,i);
}
context.putImageData(imageData, left, top);
}
var canvasToGray = function(canvas,deltay) {
var context = canvas.getContext('2d');
var width = canvas.width;
var height = canvas.height;
var y = 0;
deltay = deltay || height;
var step = function() {
rectangleToGray(context,0,y,width,deltay);
y += deltay;
log(Math.round(100*y/height)+'%');
if (y<height) {
var rest = height-y;
if ( rest < deltay) deltay = rest;
setTimeout(step,0);
}
else {
t = new Date().getTime()-t;
log('Proceso completado en '+t/1000+' segundos');
}
}
var t = new Date().getTime();
step();
}
var imageToGray = function(im) {
var canvas = document.createElement("canvas");
if(canvas.getContext){
canvas.width = im.width || im.offsetWidth;
canvas.height = im.height || im.offsetHeight;
canvas.getContext('2d').drawImage(im,0,0);
im.parentNode.replaceChild(canvas,im);
canvasToGray(canvas,2);
}
else {
var nit = 100;
var t = new Date().getTime();
for (var j = 0;j<nit;j++)
im.style.filter = 'progid:DXImageTransform.Microsoft.BasicImage(grayscale=1)';
t = new Date().getTime()-t;
log('Proceso completado en '+t/(1000*nit)+' segundos');
}
}
</script>
</head>
<body>
<img style="cursor:pointer" onclick="imageToGray(this)" id="im" src="tigre2.jpg" />
<div id="log" style="font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px">Click sobre la imagen para pasar a escala de grises</div>
</body>
</html>
Nótese que cuando se invoca la función canvasToGray en el código mostrado, se usa "2" como segundo argumento. Esto hace que el lienzo se vaya pintando por partes (partes rectangulares del mismo ancho del canvas por los 2 pixeles de altura, que indica ese segundo argumento), lo que produce el siguiente resultado:
Si en lugar de parcializar el pintado en rectángulos de 2 ó n px de altura quisiéramos pintar todo de una vez (un único rectángulo), bastaría con invocar la función canvasToGray sin un segundo argumento. Con ello obtendríamos el siguiente efecto:
Aunque los tiempos de Explorer siguen siendo mucho mejores, la velocidad obtenida cuando empleamos correctamente el objeto imageData es más que aceptable.
Además, como vimos anteriormente (pero ahora de manera óptima), podemos lograr, usando este mecanismo, cosas que en Explorer sólo pueden soñarse:
Escala de rojos:
Código usado:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Escala de grises con javascript</title>
<script>
function toGrayScale(im){
var inicio=new Date().getTime();
if(document.createElement("canvas").getContext){
var ref = document.createElement("canvas");
ref.width = im.width || im.offsetWidth;
ref.height =im.height || im.offsetHeight;
var context = ref.getContext("2d");
context.drawImage(im,0,0);
var iData=context.getImageData(0,0, ref.width, ref.height);
im.parentNode.replaceChild(ref,im);
var data = iData.data;
var length = data.length;
document.getElementById('log').innerHTML='procesando...';
for (var i = 0;i<length;i+=4) {
var average = (data[i]+data[i+1]+data[i+2])/3;
data[i] = 255;
data[i+1] = average;
data[i+2] = average;
}
context.putImageData(iData, 0, 0);
document.getElementById('log').innerHTML=-(inicio-new Date().getTime())/1000;
}else{
im.style.filter = 'progid:DXImageTransform.Microsoft.BasicImage(grayscale=1)';
document.getElementById('log').innerHTML=-(inicio-new Date().getTime())/1000;
}
}
</script>
</head>
<body>
<img style="cursor:pointer" onclick="toGrayScale(this)" id="im" src="sp.jpg" />
<div id="log" style="font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px">click sobre la imagen para pasar a escala de rojos</div>
</body>
</html>
Y de la misma manera podemos jugar con el resto de los canales para obtener:
Escala de azules:
Escala de verdes:
También podemos jugar de diferentes maneras con los valores RGB para obtener resultados disitintos. Por ejemplo:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Escala de grises con javascript</title>
<script>
function toGrayScale(im){
var inicio=new Date().getTime();
if(document.createElement("canvas").getContext){
var ref = document.createElement("canvas");
ref.width = im.width || im.offsetWidth;
ref.height =im.height || im.offsetHeight;
var context = ref.getContext("2d");
context.drawImage(im,0,0);
var iData=context.getImageData(0,0, ref.width, ref.height);
im.parentNode.replaceChild(ref,im);
var data = iData.data;
var length = data.length;
document.getElementById('log').innerHTML='procesando...';
for (var i = 0;i<length;i+=4) {
var average = (data[i]+data[i+1]+data[i+2])/3;
data[i] = average/2;
data[i+1] = average;
data[i+2] = average/2;
}
context.putImageData(iData, 0, 0);
document.getElementById('log').innerHTML=-(inicio-new Date().getTime())/1000;
}else{
im.style.filter = 'progid:DXImageTransform.Microsoft.BasicImage(grayscale=1)';
document.getElementById('log').innerHTML=-(inicio-new Date().getTime())/1000;
}
}
</script>
</head>
<body>
<img style="cursor:pointer" onclick="toGrayScale(this)" id="im" src="sp.jpg" />
<div id="log" style="font-family:Verdana, Arial, Helvetica, sans-serif; font-size:10px">click sobre la imagen para transformarla</div>
</body>
</html>
Cuyo resultado sería:
RollOver:
Para obtener un efecto de rollOver o sustitución de imágenes necesitamos invocar el método toDataUrl, el cual retorna una imagen instantánea del canvas codificada en base 64.
Ejemplo:
Código utilizado:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Rollover</title>
<script>
function toGrayScale(im){
im.style.cursor='pointer';
if(document.createElement("canvas").getContext){
if(im.gs){
im.src=im.gs;
return;
}
var ref = document.createElement("canvas");
ref.width = im.width || im.offsetWidth;
ref.height =im.height || im.offsetHeight;
var context = ref.getContext("2d");
context.drawImage(im,0,0);
var iData=context.getImageData(0,0, ref.width, ref.height);
var data = iData.data;
var length = data.length;
for (var i = 0;i<length;i+=4) {
var average = (data[i]+data[i+1]+data[i+2])/3;
data[i] = average;
data[i+1] = average;
data[i+2] = average;
}
context.putImageData(iData, 0, 0);
im.src=im.gs=ref.toDataURL();
}else{
im.style.filter = 'progid:DXImageTransform.Microsoft.BasicImage(grayscale=1)';
}
}
</script>
</head>
<body>
<img onmouseout="this.src='arwen.jpg';this.style.filter=''" onmouseover="toGrayScale(this)" src="arwen.jpg" />
</body>
</html>
Veamos otro uso que podemos darle al objeto imageData (y de paso, a ver si identifican a este personaje, que merece nuestro máximo reconocimiento):
Código utilizado:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>ASCII ART</title>
<style type="text/css">
<!--
body{
font-family: "Courier New";
font-size: 9px;
line-height: 5px;
}
-->
</style>
<script>
function toASCIIArt(im){
var paleta='@80GCLft1i;:,. ';
var lp=paleta.length;
var out='';
if(document.createElement("canvas").getContext){
var ref = document.createElement("canvas");
ref.width = im.width || im.offsetWidth;
ref.height =im.height || im.offsetHeight;
var context = ref.getContext("2d");
context.drawImage(im,0,0);
var iData=context.getImageData(0,0, ref.width, ref.height);
var data = iData.data;
var length = data.length;
for (var i = 0,l=1;i<length;i+=4,l++) {
var average = (data[i]+data[i+1]+data[i+2])/3;
out+=paleta.charAt(Math.round(average*lp/255));
if(!(l%ref.width))
out+='\n';
}
document.getElementById('lienzo').innerHTML=out;
}
}
onload=function(){toASCIIArt(document.getElementById('im'));}
</script>
</head>
<body>
<img style=" position:absolute; top:-1500px" id="im" src="an.jpg" />
<pre id="lienzo"></pre>
</body>
</html>
Conclusión:
Bien usado, el objeto imageData es una herramienta poderosa, que nos permite diferentes transformaciones sobre los canales rgba de una imagen sin quedarse atrás de otras técnicas del lado del servidor, como GD o Imagemagick, o del lado del cliente, como la clase BitmapData de Flash.
Agradecimiento:
Nuevamente quiero agradecer a
José Antonio por mostrarme cómo mejorar el uso de esta herramienta.