29 de Julio de 2010

Notas Espacio Programación

galería de imágenesvideo
AUTOR: Andrés Fernández
FECHA: 2/1/2010
LECTURAS:2445
Buscar Notas
volver
Optimizando GrayScale con javascript

Grayscale Javascript Optimizado

Tiempo atrás mostramos cómo trabajar con el objeto CanvasPixelArray para manipular los pixeles de una imagen y convertirla a escala de grises, pero el resultado era demasiado lento. Gracias a José Antonio Pérez, que me hizo notar algunas deficiencias en mi código, veremos cómo obtener un resultado mucho más aceptable.
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.widthref.height);
        
im.parentNode.replaceChild(ref,im);
        var 
data   iData.data;
        var 
length data.length;
        
document.getElementById('log').innerHTML='procesando...';
          for (var 
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(iData00); 
        
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 
0;i<length;i+=4) {
    
modifyPixel(data,i);
  }
  
context.putImageData(imageDatalefttop); 
}

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); 
    
+= deltay;
    
log(Math.round(100*y/height)+'%');
    if (
y<height) {
      var 
rest height-y;
      if ( 
rest deltaydeltay rest;
      
setTimeout(step,0);
    }
    else {
      
= new Date().getTime()-t;
      
log('Proceso completado en '+t/1000+' segundos');
    }
  }
  var 
= 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 
= new Date().getTime();
    for (var 
0;j<nit;j++)
       
im.style.filter 'progid:DXImageTransform.Microsoft.BasicImage(grayscale=1)'
    
= 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.widthref.height);
        
im.parentNode.replaceChild(ref,im);
        var 
data   iData.data;
        var 
length data.length;
        
document.getElementById('log').innerHTML='procesando...';
          for (var 
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(iData00); 
        
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.widthref.height);
        
im.parentNode.replaceChild(ref,im);
        var 
data   iData.data;
        var 
length data.length;
        
document.getElementById('log').innerHTML='procesando...';
          for (var 
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(iData00); 
        
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.widthref.height);
        var 
data   iData.data;
        var 
length data.length;
          for (var 
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(iData00);
          
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.widthref.height);
        var 
data   iData.data;
        var 
length data.length;
          for (var 
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.

Home - Quiénes Somos - Portfolio - Espacio Diseño - Espacio Programación - Capacitación - Contacto - RSS - XHTML 1.0