Html5 Canvas DrawImage: cómo aplicar antialiasing

81

Eche un vistazo al siguiente ejemplo:

http://jsfiddle.net/MLGr4/47/

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

img = new Image();
img.onload = function(){
    canvas.width = 400;
    canvas.height = 150;
    ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, 400, 150);
}
img.src = "http://openwalls.com/image/1734/colored_lines_on_blue_background_1920x1200.jpg";

Como puede ver, la imagen no tiene suavizado, aunque se dice que drawImage aplica el suavizado automáticamente. Intenté de muchas formas diferentes, pero no parece funcionar. ¿Podría decirme cómo puedo obtener una imagen suavizada? Gracias.

Dundar
fuente

Respuestas:

174

Porque

Algunas imágenes son muy difíciles de reducir e interpolar , como esta con curvas, cuando se desea pasar de un tamaño grande a uno pequeño.

Los navegadores suelen utilizar la interpolación bilineal (muestreo 2x2) con el elemento de lienzo en lugar de bi-cúbica (muestreo 4x4) por razones (probables) de rendimiento.

Si el paso es demasiado grande, simplemente no hay suficientes píxeles para muestrear, lo que se refleja en el resultado.

Desde una perspectiva de señal / DSP, podría ver esto como un valor de umbral de filtro de paso bajo establecido demasiado alto, lo que puede resultar en un alias si hay muchas frecuencias altas (detalles) en la señal.

Solución

Actualización 2018:

Aquí hay un buen truco que puede usar para los navegadores que admite la filterpropiedad en el contexto 2D. Esto desenfoca previamente la imagen, que en esencia es lo mismo que un remuestreo, luego se reduce. Esto permite pasos grandes pero solo necesita dos pasos y dos cajones.

Desenfoque previo usando el número de pasos (tamaño original / tamaño de destino / 2) como radio (es posible que deba ajustar esto heurísticamente según el navegador y los pasos impares / pares; aquí solo se muestra simplificado):

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

if (typeof ctx.filter === "undefined") {
 alert("Sorry, the browser doesn't support Context2D filters.")
}

const img = new Image;
img.onload = function() {

  // step 1
  const oc = document.createElement('canvas');
  const octx = oc.getContext('2d');
  oc.width = this.width;
  oc.height = this.height;

  // steo 2: pre-filter image using steps as radius
  const steps = (oc.width / canvas.width)>>1;
  octx.filter = `blur(${steps}px)`;
  octx.drawImage(this, 0, 0);

  // step 3, draw scaled
  ctx.drawImage(oc, 0, 0, oc.width, oc.height, 0, 0, canvas.width, canvas.height);

}
img.src = "//i.stack.imgur.com/cYfuM.jpg";
body{ background-color: ivory; }
canvas{border:1px solid red;}
<br/><p>Original was 1600x1200, reduced to 400x300 canvas</p><br/>
<canvas id="canvas" width=400 height=250></canvas>

Soporte para filtro como ogf Oct / 2018:

Actualización 2017: ahora hay una nueva propiedad definida en las especificaciones para configurar la calidad de remuestreo:

context.imageSmoothingQuality = "low|medium|high"

Actualmente solo es compatible con Chrome. Los métodos reales usados ​​por nivel se dejan a criterio del proveedor, pero es razonable asumir que Lanczos es "alto" o algo equivalente en calidad. Esto significa que el paso hacia abajo se puede omitir por completo, o se pueden usar pasos más grandes con menos redibujos, dependiendo del tamaño de la imagen y

Soporte para imageSmoothingQuality:

navegador. Hasta entonces ...:
Fin de transmisión

La solución es usar la reducción para obtener un resultado adecuado. Reducir significa que reduce el tamaño en pasos para permitir que el rango de interpolación limitado cubra suficientes píxeles para el muestreo.

Esto permitirá buenos resultados también con la interpolación bilineal (en realidad se comporta de forma muy parecida a la bicúbica al hacer esto) y la sobrecarga es mínima ya que hay menos píxeles para muestrear en cada paso.

El paso ideal es ir a la mitad de la resolución en cada paso hasta que establezca el tamaño objetivo (¡gracias a Joe Mabel por mencionar esto!).

Violín modificado

Usando escala directa como en la pregunta original:

IMAGEN NORMAL A ESCALA ABAJO

Usando step-down como se muestra a continuación:

IMAGEN BAJADA

En este caso, deberá renunciar en 3 pasos:

En el paso 1, reducimos la imagen a la mitad usando un lienzo fuera de la pantalla:

// step 1 - create off-screen canvas
var oc   = document.createElement('canvas'),
    octx = oc.getContext('2d');

oc.width  = img.width  * 0.5;
oc.height = img.height * 0.5;

octx.drawImage(img, 0, 0, oc.width, oc.height);

El paso 2 reutiliza el lienzo fuera de la pantalla y dibuja la imagen reducida a la mitad nuevamente:

// step 2
octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);

Y dibujamos una vez más en el lienzo principal, nuevamente reducido a la mitad pero al tamaño final:

// step 3
ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
                  0, 0, canvas.width,   canvas.height);

Propina:

Puede calcular el número total de pasos necesarios utilizando esta fórmula (incluye el paso final para establecer el tamaño objetivo):

steps = Math.ceil(Math.log(sourceWidth / targetWidth) / Math.log(2))

fuente
4
Trabajando con algunas imágenes iniciales muy grandes (8000 x 6000 y superiores), me resulta útil iterar básicamente el paso 2 hasta llegar a un factor de 2 del tamaño deseado.
Joe Mabel
¡Funciona de maravilla! ¡Gracias!
Vlad Tsepelev
1
Estoy confundido sobre la diferencia entre el segundo y el tercer paso ... ¿alguien puede explicarlo?
carinlynchin
1
@Carine es un poco complicado, pero Canvas intenta guardar un png tan rápido como puede. El archivo png admite 5 tipos de filtros diferentes internamente que pueden mejorar la compresión (gzip), pero para encontrar la mejor combinación, todos estos filtros deben probarse por línea de la imagen. Eso llevaría mucho tiempo para imágenes grandes y podría bloquear el navegador, por lo que la mayoría de los navegadores solo usan el filtro 0 y lo eliminan con la esperanza de obtener algo de compresión. Puede hacer este proceso manualmente, pero obviamente es un poco más de trabajo. O ejecútelo a través de API de servicio como la de tinypng.com.
1
@Kaiido no se olvida y la "copia" es muy lenta. Si necesita transparencia, es más rápido usar clearRect () y usar main o alt. lienzo como objetivo.
12

Recomiendo encarecidamente pica para tales tareas. Su calidad es superior a la de múltiples reducciones y, al mismo tiempo, es bastante rápida. Aquí hay una demostración .

avalancha1
fuente
4
    var getBase64Image = function(img, quality) {
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");

    //----- origin draw ---
    ctx.drawImage(img, 0, 0, img.width, img.height);

    //------ reduced draw ---
    var canvas2 = document.createElement("canvas");
    canvas2.width = img.width * quality;
    canvas2.height = img.height * quality;
    var ctx2 = canvas2.getContext("2d");
    ctx2.drawImage(canvas, 0, 0, img.width * quality, img.height * quality);

    // -- back from reduced draw ---
    ctx.drawImage(canvas2, 0, 0, img.width, img.height);

    var dataURL = canvas.toDataURL("image/png");
    return dataURL;
    // return dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
}
kamil
fuente
1
¿Cuál es el rango de valores del parámetro 'calidad'?
serup
entre cero y uno [0, 1]
Iván Rodríguez
4

Además de la respuesta de Ken, aquí hay otra solución para realizar el muestreo por la mitad (para que el resultado se vea bien usando el algoritmo del navegador):

  function resize_image( src, dst, type, quality ) {
     var tmp = new Image(),
         canvas, context, cW, cH;

     type = type || 'image/jpeg';
     quality = quality || 0.92;

     cW = src.naturalWidth;
     cH = src.naturalHeight;

     tmp.src = src.src;
     tmp.onload = function() {

        canvas = document.createElement( 'canvas' );

        cW /= 2;
        cH /= 2;

        if ( cW < src.width ) cW = src.width;
        if ( cH < src.height ) cH = src.height;

        canvas.width = cW;
        canvas.height = cH;
        context = canvas.getContext( '2d' );
        context.drawImage( tmp, 0, 0, cW, cH );

        dst.src = canvas.toDataURL( type, quality );

        if ( cW <= src.width || cH <= src.height )
           return;

        tmp.src = dst.src;
     }

  }
  // The images sent as parameters can be in the DOM or be image objects
  resize_image( $( '#original' )[0], $( '#smaller' )[0] );
Jesús Carrera
fuente
3

En caso de que alguien más esté buscando una respuesta todavía, hay otra forma en que puede usar la imagen de fondo en lugar de drawImage (). No perderá calidad de imagen de esta manera.

JS:

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
   var url = "http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

    img=new Image();
    img.onload=function(){

        canvas.style.backgroundImage = "url(\'" + url + "\')"

    }
    img.src="http://openwalls.com/image/17342/colored_lines_on_blue_background_1920x1200.jpg";

demostración de trabajo

Munkhjargal Narmandakh
fuente
2

Creé un servicio Angular reutilizable para manejar el cambio de tamaño de imágenes de alta calidad para cualquiera que esté interesado: https://gist.github.com/fisch0920/37bac5e741eaec60e983

El servicio incluye el enfoque de reducción gradual de Ken, así como una versión modificada del enfoque de convolución de lanczos que se encuentra aquí .

Incluí ambas soluciones porque ambas tienen sus propios pros y contras. El enfoque de convolución de lanczos es de mayor calidad a costa de ser más lento, mientras que el enfoque de reducción de escala escalonada produce resultados razonablemente antialias y es significativamente más rápido.

Uso de ejemplo:

angular.module('demo').controller('ExampleCtrl', function (imageService) {
  // EXAMPLE USAGE
  // NOTE: it's bad practice to access the DOM inside a controller, 
  // but this is just to show the example usage.

  // resize by lanczos-sinc filter
  imageService.resize($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })

  // resize by stepping down image size in increments of 2x
  imageService.resizeStep($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })
})
fisch2
fuente