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 filter
propiedad 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() {
const oc = document.createElement('canvas');
const octx = oc.getContext('2d');
oc.width = this.width;
oc.height = this.height;
const steps = (oc.width / canvas.width)>>1;
octx.filter = `blur(${steps}px)`;
octx.drawImage(this, 0, 0);
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:
CanvasRenderingContext2D.filter
api.CanvasRenderingContext2D.filter
On Standard Track, Experimental
https:
DESKTOP > |Chrome |Edge |Firefox |IE |Opera |Safari
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter ! | 52 | ? | 49 | - | - | -
MOBILE > |Chrome/A |Edge/mob |Firefox/A |Opera/A |Safari/iOS|Webview/A
:----------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------
filter ! | 52 | ? | 49 | - | - | 52
! = Experimental
Data from MDN - "npm i -g mdncomp" (c) epistemex
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
:
CanvasRenderingContext2D.imageSmoothingQuality
api.CanvasRenderingContext2D.imageSmoothingQuality
On Standard Track, Experimental
https:
DESKTOP > |Chrome |Edge |Firefox |IE |Opera |Safari
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !| 54 | ? | - | ? | 41 | Y
MOBILE > |Chrome/A |Edge/mob |Firefox/A |Opera/A |Safari/iOS|Webview/A
:----------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:
imageSmoothingQuality !| 54 | ? | - | 41 | Y | 54
! = Experimental
Data from MDN - "npm i -g mdncomp" (c) epistemex
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:
Usando step-down como se muestra a continuación:
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:
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:
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:
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))