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))
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 .
fuente
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,/, ""); }
fuente
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] );
fuente
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
fuente
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 }) })
fuente