HTML5 Canvas Resize (Downscale) Image ¿Alta calidad?

149

Utilizo elementos de lienzo html5 para cambiar el tamaño de las imágenes en mi navegador. Resulta que la calidad es muy baja. Encontré esto: deshabilite la interpolación al escalar un <canvas> pero no ayuda a aumentar la calidad.

A continuación se muestra mi código css y js, así como la imagen escaldada con Photoshop y escalada en la API de lienzo.

¿Qué debo hacer para obtener una calidad óptima al escalar una imagen en el navegador?

Nota: Quiero reducir una imagen grande a una pequeña, modificar el color en un lienzo y enviar el resultado del lienzo al servidor.

CSS:

canvas, img {
    image-rendering: optimizeQuality;
    image-rendering: -moz-crisp-edges;
    image-rendering: -webkit-optimize-contrast;
    image-rendering: optimize-contrast;
    -ms-interpolation-mode: nearest-neighbor;
}

JS:

var $img = $('<img>');
var $originalCanvas = $('<canvas>');
$img.load(function() {


   var originalContext = $originalCanvas[0].getContext('2d');   
   originalContext.imageSmoothingEnabled = false;
   originalContext.webkitImageSmoothingEnabled = false;
   originalContext.mozImageSmoothingEnabled = false;
   originalContext.drawImage(this, 0, 0, 379, 500);
});

La imagen cambió de tamaño con Photoshop:

ingrese la descripción de la imagen aquí

La imagen redimensionada en lienzo:

ingrese la descripción de la imagen aquí

Editar:

Traté de reducir la escala en más de un paso como se propuso en:

Cambiar el tamaño de una imagen en un lienzo HTML5 y un lienzo Html5 drawImage: cómo aplicar el antialiasing

Esta es la función que he usado:

function resizeCanvasImage(img, canvas, maxWidth, maxHeight) {
    var imgWidth = img.width, 
        imgHeight = img.height;

    var ratio = 1, ratio1 = 1, ratio2 = 1;
    ratio1 = maxWidth / imgWidth;
    ratio2 = maxHeight / imgHeight;

    // Use the smallest ratio that the image best fit into the maxWidth x maxHeight box.
    if (ratio1 < ratio2) {
        ratio = ratio1;
    }
    else {
        ratio = ratio2;
    }

    var canvasContext = canvas.getContext("2d");
    var canvasCopy = document.createElement("canvas");
    var copyContext = canvasCopy.getContext("2d");
    var canvasCopy2 = document.createElement("canvas");
    var copyContext2 = canvasCopy2.getContext("2d");
    canvasCopy.width = imgWidth;
    canvasCopy.height = imgHeight;  
    copyContext.drawImage(img, 0, 0);

    // init
    canvasCopy2.width = imgWidth;
    canvasCopy2.height = imgHeight;        
    copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);


    var rounds = 2;
    var roundRatio = ratio * rounds;
    for (var i = 1; i <= rounds; i++) {
        console.log("Step: "+i);

        // tmp
        canvasCopy.width = imgWidth * roundRatio / i;
        canvasCopy.height = imgHeight * roundRatio / i;

        copyContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvasCopy.width, canvasCopy.height);

        // copy back
        canvasCopy2.width = imgWidth * roundRatio / i;
        canvasCopy2.height = imgHeight * roundRatio / i;
        copyContext2.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvasCopy2.width, canvasCopy2.height);

    } // end for


    // copy back to canvas
    canvas.width = imgWidth * roundRatio / rounds;
    canvas.height = imgHeight * roundRatio / rounds;
    canvasContext.drawImage(canvasCopy2, 0, 0, canvasCopy2.width, canvasCopy2.height, 0, 0, canvas.width, canvas.height);


}

Aquí está el resultado si uso un tamaño de 2 pasos:

ingrese la descripción de la imagen aquí

Aquí está el resultado si uso un tamaño de 3 pasos:

ingrese la descripción de la imagen aquí

Aquí está el resultado si uso un tamaño de 4 pasos:

ingrese la descripción de la imagen aquí

Aquí está el resultado si uso un tamaño reducido de 20 pasos:

ingrese la descripción de la imagen aquí

Nota: Resulta que de 1 paso a 2 pasos hay una gran mejora en la calidad de la imagen, pero cuantos más pasos agregue al proceso, más difusa se volverá la imagen.

¿Hay alguna manera de resolver el problema de que la imagen se vuelve más borrosa a medida que agrega más pasos?

Editar 04-10-2013: probé el algoritmo de GameAlchemist. Aquí está el resultado en comparación con Photoshop.

Imagen de PhotoShop:

Imagen de PhotoShop

Algoritmo de GameAlchemist:

Algoritmo de GameAlchemist

confile
fuente
2
Puede intentar escalar gradualmente su imagen: stackoverflow.com/questions/18761404/…
markE
1
posible duplicado de lienzo Html5 drawImage: cómo aplicar antialiasing . Mira si no funciona. Si las imágenes son grandes y reducidos a tamaño pequeño tendrá que hacerlo en los pasos (ver ejemplo imágenes en enlace)
2
@confile apagar la interpolación lo empeorará. Desea mantener eso habilitado. Mira el enlace que proporcioné arriba. Muestro allí cómo usar los pasos para reducir las imágenes más grandes y mantener la calidad. Y como Scott dice que quiere priorizar la calidad sobre la velocidad.
1
@ Ken-AbdiasSoftware Intenté que te acercaras, pero el problema es que empeorará a medida que más rondas utilizo para el escalado gradual. ¿Alguna idea de como arreglarlo?
Confile
3
¿Seguramente las posibilidades de replicar la funcionalidad de un costoso software profesional de edición de fotos usando HTML5 son bastante escasas? Probablemente pueda acercarse (ish), pero tal como funciona en Photoshop, ¡imagino que sería imposible!
Liam

Respuestas:

171

Como su problema es reducir la escala de su imagen, no tiene sentido hablar de interpolación, que es crear píxeles. El problema aquí es la disminución de la resolución.

Para reducir la muestra de una imagen, necesitamos convertir cada cuadrado de p * p píxeles en la imagen original en un solo píxel en la imagen de destino.

Por motivos de rendimiento, los navegadores hacen un muestreo muy simple: para construir la imagen más pequeña, solo elegirán UN píxel en la fuente y usarán su valor para el destino. que 'olvida' algunos detalles y agrega ruido.

Sin embargo, hay una excepción a eso: dado que el muestreo de imagen 2X es muy simple de calcular (promedio de 4 píxeles para hacer uno) y se usa para píxeles de retina / HiDPI, este caso se maneja correctamente: el navegador utiliza 4 píxeles para hacer uno-.

PERO ... si usa varias veces una disminución de muestreo 2X, enfrentará el problema de que los errores de redondeo sucesivos agregarán demasiado ruido.
Lo que es peor, no siempre cambiará el tamaño por una potencia de dos, y cambiar el tamaño a la potencia más cercana + un último cambio de tamaño es muy ruidoso.

Lo que busca es una disminución de resolución de píxeles perfecta, es decir: un nuevo muestreo de la imagen que tendrá en cuenta todos los píxeles de entrada, sea cual sea la escala.
Para hacerlo, debemos calcular, para cada píxel de entrada, su contribución a uno, dos o cuatro píxeles de destino, dependiendo de si la proyección escalada de los píxeles de entrada está justo dentro de los píxeles de destino, se superpone a un borde X, un borde Y o ambos .
(Un esquema sería bueno aquí, pero no tengo uno).

Aquí hay un ejemplo de escala de lienzo vs mi escala perfecta de píxeles en una escala 1/3 de un zombat.

Tenga en cuenta que la imagen se puede escalar en su navegador y está SO .jpegized.
Sin embargo, vemos que hay mucho menos ruido, especialmente en la hierba detrás del wombat y las ramas a su derecha. El ruido en el pelaje lo hace más contrastado, pero parece que tiene pelos blancos, a diferencia de la imagen original.
La imagen correcta es menos pegadiza pero definitivamente más agradable.

ingrese la descripción de la imagen aquí

Aquí está el código para hacer la reducción de escala perfecta de píxeles:

resultado del violín: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
mismo violín: http://jsfiddle.net/gamealchemist/r6aVp/

// scales the image by (float) scale < 1
// returns a canvas containing the scaled image.
function downScaleImage(img, scale) {
    var imgCV = document.createElement('canvas');
    imgCV.width = img.width;
    imgCV.height = img.height;
    var imgCtx = imgCV.getContext('2d');
    imgCtx.drawImage(img, 0, 0);
    return downScaleCanvas(imgCV, scale);
}

// scales the canvas by (float) scale < 1
// returns a new canvas containing the scaled image.
function downScaleCanvas(cv, scale) {
    if (!(scale < 1) || !(scale > 0)) throw ('scale must be a positive number <1 ');
    var sqScale = scale * scale; // square scale = area of source pixel within target
    var sw = cv.width; // source image width
    var sh = cv.height; // source image height
    var tw = Math.floor(sw * scale); // target image width
    var th = Math.floor(sh * scale); // target image height
    var sx = 0, sy = 0, sIndex = 0; // source x,y, index within source array
    var tx = 0, ty = 0, yIndex = 0, tIndex = 0; // target x,y, x,y index within target array
    var tX = 0, tY = 0; // rounded tx, ty
    var w = 0, nw = 0, wx = 0, nwx = 0, wy = 0, nwy = 0; // weight / next weight x / y
    // weight is weight of current source point within target.
    // next weight is weight of current source point within next target's point.
    var crossX = false; // does scaled px cross its current px right border ?
    var crossY = false; // does scaled px cross its current px bottom border ?
    var sBuffer = cv.getContext('2d').
    getImageData(0, 0, sw, sh).data; // source buffer 8 bit rgba
    var tBuffer = new Float32Array(3 * tw * th); // target buffer Float32 rgb
    var sR = 0, sG = 0,  sB = 0; // source's current point r,g,b
    /* untested !
    var sA = 0;  //source alpha  */    

    for (sy = 0; sy < sh; sy++) {
        ty = sy * scale; // y src position within target
        tY = 0 | ty;     // rounded : target pixel's y
        yIndex = 3 * tY * tw;  // line index within target array
        crossY = (tY != (0 | ty + scale)); 
        if (crossY) { // if pixel is crossing botton target pixel
            wy = (tY + 1 - ty); // weight of point within target pixel
            nwy = (ty + scale - tY - 1); // ... within y+1 target pixel
        }
        for (sx = 0; sx < sw; sx++, sIndex += 4) {
            tx = sx * scale; // x src position within target
            tX = 0 |  tx;    // rounded : target pixel's x
            tIndex = yIndex + tX * 3; // target pixel index within target array
            crossX = (tX != (0 | tx + scale));
            if (crossX) { // if pixel is crossing target pixel's right
                wx = (tX + 1 - tx); // weight of point within target pixel
                nwx = (tx + scale - tX - 1); // ... within x+1 target pixel
            }
            sR = sBuffer[sIndex    ];   // retrieving r,g,b for curr src px.
            sG = sBuffer[sIndex + 1];
            sB = sBuffer[sIndex + 2];

            /* !! untested : handling alpha !!
               sA = sBuffer[sIndex + 3];
               if (!sA) continue;
               if (sA != 0xFF) {
                   sR = (sR * sA) >> 8;  // or use /256 instead ??
                   sG = (sG * sA) >> 8;
                   sB = (sB * sA) >> 8;
               }
            */
            if (!crossX && !crossY) { // pixel does not cross
                // just add components weighted by squared scale.
                tBuffer[tIndex    ] += sR * sqScale;
                tBuffer[tIndex + 1] += sG * sqScale;
                tBuffer[tIndex + 2] += sB * sqScale;
            } else if (crossX && !crossY) { // cross on X only
                w = wx * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tX+1) px                
                nw = nwx * scale
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
            } else if (crossY && !crossX) { // cross on Y only
                w = wy * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // add weighted component for next (tY+1) px                
                nw = nwy * scale
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
            } else { // crosses both x and y : four target points involved
                // add weighted component for current px
                w = wx * wy;
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                // for tX + 1; tY px
                nw = nwx * wy;
                tBuffer[tIndex + 3] += sR * nw;
                tBuffer[tIndex + 4] += sG * nw;
                tBuffer[tIndex + 5] += sB * nw;
                // for tX ; tY + 1 px
                nw = wx * nwy;
                tBuffer[tIndex + 3 * tw    ] += sR * nw;
                tBuffer[tIndex + 3 * tw + 1] += sG * nw;
                tBuffer[tIndex + 3 * tw + 2] += sB * nw;
                // for tX + 1 ; tY +1 px
                nw = nwx * nwy;
                tBuffer[tIndex + 3 * tw + 3] += sR * nw;
                tBuffer[tIndex + 3 * tw + 4] += sG * nw;
                tBuffer[tIndex + 3 * tw + 5] += sB * nw;
            }
        } // end for sx 
    } // end for sy

    // create result canvas
    var resCV = document.createElement('canvas');
    resCV.width = tw;
    resCV.height = th;
    var resCtx = resCV.getContext('2d');
    var imgRes = resCtx.getImageData(0, 0, tw, th);
    var tByteBuffer = imgRes.data;
    // convert float32 array into a UInt8Clamped Array
    var pxIndex = 0; //  
    for (sIndex = 0, tIndex = 0; pxIndex < tw * th; sIndex += 3, tIndex += 4, pxIndex++) {
        tByteBuffer[tIndex] = Math.ceil(tBuffer[sIndex]);
        tByteBuffer[tIndex + 1] = Math.ceil(tBuffer[sIndex + 1]);
        tByteBuffer[tIndex + 2] = Math.ceil(tBuffer[sIndex + 2]);
        tByteBuffer[tIndex + 3] = 255;
    }
    // writing result to canvas.
    resCtx.putImageData(imgRes, 0, 0);
    return resCV;
}

Es bastante codicioso de memoria, ya que se requiere un búfer flotante para almacenar los valores intermedios de la imagen de destino (-> si contamos el lienzo de resultados, usamos 6 veces la memoria de la imagen de origen en este algoritmo).
También es bastante costoso, ya que cada píxel de origen se usa sea cual sea el tamaño de destino, y tenemos que pagar por getImageData / putImageDate, también bastante lento.
Pero no hay forma de ser más rápido que procesar cada valor de origen en este caso, y la situación no es tan mala: para mi imagen 740 * 556 de un wombat, el procesamiento tarda entre 30 y 40 ms.

GameAlchemist
fuente
¿Podría ser más rápido si escala la imagen antes de ponerla en el lienzo?
Confile
no lo entiendo ... parece que es lo que hago. El búfer y el lienzo que creo (resCV) tienen el tamaño de la imagen a escala. Creo que la única forma de hacerlo más rápido sería usar el cálculo de enteros tipo breshensam. Pero 40 ms solo es lento para un videojuego (25 fps), no para una aplicación de sorteo.
GameAlchemist
¿Ves alguna posibilidad de hacer tu algoritmo más rápido manteniendo la calidad?
Confile
1
Traté de redondear el búfer (última parte del algoritmo) usando 0 | en lugar de Mat.ceil. Es un poco más rápido Pero de todos modos, hay bastante sobrecarga con get / putImageData y, de nuevo, no podemos evitar procesar cada píxel.
GameAlchemist
44
Ok, entonces vi el código: estabas muy cerca de la solución. Dos errores: sus índices estaban apagados en uno para tX + 1 (eran + 3, + 4, + 5, + 6 en lugar de +4, +5, +6, +7), y cambiar la línea en rgba es un mul por 4, no 3. Acabo de probar 4 valores aleatorios para verificar (0.1, 0.15, 0.33, 0.8) parecía estar bien. su violín actualizado está aquí: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist
51

Nuevo muestreo de lienzo rápido con buena calidad: http://jsfiddle.net/9g9Nv/442/

Actualización: versión 2.0 (más rápido, trabajadores web + objetos transferibles) - https://github.com/viliusle/Hermite-resize

/**
 * Hermite resize - fast image resize/resample using Hermite filter. 1 cpu version!
 * 
 * @param {HtmlElement} canvas
 * @param {int} width
 * @param {int} height
 * @param {boolean} resize_canvas if true, canvas will be resized. Optional.
 */
function resample_single(canvas, width, height, resize_canvas) {
    var width_source = canvas.width;
    var height_source = canvas.height;
    width = Math.round(width);
    height = Math.round(height);

    var ratio_w = width_source / width;
    var ratio_h = height_source / height;
    var ratio_w_half = Math.ceil(ratio_w / 2);
    var ratio_h_half = Math.ceil(ratio_h / 2);

    var ctx = canvas.getContext("2d");
    var img = ctx.getImageData(0, 0, width_source, height_source);
    var img2 = ctx.createImageData(width, height);
    var data = img.data;
    var data2 = img2.data;

    for (var j = 0; j < height; j++) {
        for (var i = 0; i < width; i++) {
            var x2 = (i + j * width) * 4;
            var weight = 0;
            var weights = 0;
            var weights_alpha = 0;
            var gx_r = 0;
            var gx_g = 0;
            var gx_b = 0;
            var gx_a = 0;
            var center_y = (j + 0.5) * ratio_h;
            var yy_start = Math.floor(j * ratio_h);
            var yy_stop = Math.ceil((j + 1) * ratio_h);
            for (var yy = yy_start; yy < yy_stop; yy++) {
                var dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
                var center_x = (i + 0.5) * ratio_w;
                var w0 = dy * dy; //pre-calc part of w
                var xx_start = Math.floor(i * ratio_w);
                var xx_stop = Math.ceil((i + 1) * ratio_w);
                for (var xx = xx_start; xx < xx_stop; xx++) {
                    var dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
                    var w = Math.sqrt(w0 + dx * dx);
                    if (w >= 1) {
                        //pixel too far
                        continue;
                    }
                    //hermite filter
                    weight = 2 * w * w * w - 3 * w * w + 1;
                    var pos_x = 4 * (xx + yy * width_source);
                    //alpha
                    gx_a += weight * data[pos_x + 3];
                    weights_alpha += weight;
                    //colors
                    if (data[pos_x + 3] < 255)
                        weight = weight * data[pos_x + 3] / 250;
                    gx_r += weight * data[pos_x];
                    gx_g += weight * data[pos_x + 1];
                    gx_b += weight * data[pos_x + 2];
                    weights += weight;
                }
            }
            data2[x2] = gx_r / weights;
            data2[x2 + 1] = gx_g / weights;
            data2[x2 + 2] = gx_b / weights;
            data2[x2 + 3] = gx_a / weights_alpha;
        }
    }
    //clear and resize canvas
    if (resize_canvas === true) {
        canvas.width = width;
        canvas.height = height;
    } else {
        ctx.clearRect(0, 0, width_source, height_source);
    }

    //draw
    ctx.putImageData(img2, 0, 0);
}
ViliusL
fuente
Necesito la mejor calidad
Confile
18
arreglado, cambié "bueno" a "mejor", ¿está bien ahora? :RE. Por otro lado, si desea la mejor muestra posible, use imagemagick.
ViliusL
@confile imgur.com era seguro de usar en jsfiddle, pero ¿los administradores hicieron algo mal? No ves buena calidad, porque tu navegador le da a CORS un error fatal. (no se puede usar la imagen de sitios remotos)
ViliusL
Está bien, puedes usar cualquier otra imagen PNG con áreas transparentes. ¿Alguna idea sobre esto?
Confile
44
@confile tenía razón, en algunos casos las imágenes transparentes tenían problemas en áreas nítidas. Me perdí estos casos con mi prueba. El cambio de tamaño fijo también corrigió el soporte de imagen remota en el violín: jsfiddle.net/9g9Nv/49
ViliusL
28

Sugerencia 1: extienda la tubería de proceso

Puede usar el paso hacia abajo como lo describo en los enlaces a los que se refiere, pero parece que los usa de manera incorrecta.

No es necesario reducir la escala para escalar imágenes a relaciones superiores a 1: 2 (normalmente, pero no limitado a). Es donde necesita hacer una reducción drástica drástica , debe dividirla en dos (y raramente, más) pasos dependiendo del contenido de la imagen (en particular, donde ocurren frecuencias altas como líneas finas).

Cada vez que muestrea una imagen, perderá detalles e información. No puede esperar que la imagen resultante sea tan clara como la original.

Si luego está reduciendo las imágenes en muchos pasos, perderá mucha información en total y el resultado será pobre como ya lo notó.

Intente con solo un paso adicional, o como máximo dos.

Convoluciones

En el caso de Photoshop, observe que aplica una convolución después de que la imagen haya sido muestreada nuevamente, como la nitidez. No se trata solo de una interpolación bicúbica, por lo que, para emular completamente Photoshop, también debemos agregar los pasos que Photoshop está realizando (con la configuración predeterminada).

Para este ejemplo, usaré mi respuesta original a la que se refiere en su publicación, pero le agregué una convolución más precisa para mejorar la calidad como un proceso posterior (vea la demostración al final).

Aquí hay un código para agregar un filtro de enfoque (se basa en un filtro de convolución genérico; puse la matriz de peso para enfocar dentro de él y un factor de mezcla para ajustar la pronunciación del efecto):

Uso:

sharpen(context, width, height, mixFactor);

los mixFactor es un valor entre [0.0, 1.0] y le permite minimizar el efecto de nitidez - regla general: cuanto menor sea el tamaño, menor será el efecto necesario.

Función (basada en este fragmento ):

function sharpen(ctx, w, h, mix) {

    var weights =  [0, -1, 0,  -1, 5, -1,  0, -1, 0],
        katet = Math.round(Math.sqrt(weights.length)),
        half = (katet * 0.5) |0,
        dstData = ctx.createImageData(w, h),
        dstBuff = dstData.data,
        srcBuff = ctx.getImageData(0, 0, w, h).data,
        y = h;
        
    while(y--) {

        x = w;

        while(x--) {

            var sy = y,
                sx = x,
                dstOff = (y * w + x) * 4,
                r = 0, g = 0, b = 0, a = 0;

            for (var cy = 0; cy < katet; cy++) {
                for (var cx = 0; cx < katet; cx++) {

                    var scy = sy + cy - half;
                    var scx = sx + cx - half;

                    if (scy >= 0 && scy < h && scx >= 0 && scx < w) {

                        var srcOff = (scy * w + scx) * 4;
                        var wt = weights[cy * katet + cx];

                        r += srcBuff[srcOff] * wt;
                        g += srcBuff[srcOff + 1] * wt;
                        b += srcBuff[srcOff + 2] * wt;
                        a += srcBuff[srcOff + 3] * wt;
                    }
                }
            }

            dstBuff[dstOff] = r * mix + srcBuff[dstOff] * (1 - mix);
            dstBuff[dstOff + 1] = g * mix + srcBuff[dstOff + 1] * (1 - mix);
            dstBuff[dstOff + 2] = b * mix + srcBuff[dstOff + 2] * (1 - mix)
            dstBuff[dstOff + 3] = srcBuff[dstOff + 3];
        }
    }

    ctx.putImageData(dstData, 0, 0);
}

El resultado de usar esta combinación será:

DEMO EN LÍNEA AQUÍ

Resultado descendente y agudización de convolución

Dependiendo de la cantidad de nitidez que desee agregar a la mezcla, puede obtener el resultado de "borroso" predeterminado a muy nítido:

Variaciones de afilar

Sugerencia 2: implementación de algoritmos de bajo nivel

Si desea obtener el mejor resultado en cuanto a calidad, deberá ir a un nivel bajo y considerar implementar, por ejemplo, este nuevo algoritmo para hacerlo.

Ver Downsampling de imagen dependiente de interpolación (2011) de IEEE.
Aquí hay un enlace al documento completo (PDF) .

No hay implementaciones de este algoritmo en JavaScript AFAIK de en este momento, por lo que te espera una mano completa si quieres lanzarte a esta tarea.

La esencia es (extractos del artículo):

Resumen

Se propone un algoritmo de muestreo descendente adaptativo orientado a la interpolación para la codificación de imágenes de baja tasa de bits en este documento. Dada una imagen, el algoritmo propuesto puede obtener una imagen de baja resolución, a partir de la cual se puede interpolar una imagen de alta calidad con la misma resolución que la imagen de entrada. A diferencia de los algoritmos tradicionales de muestreo descendente, que son independientes del proceso de interpolación, el algoritmo de muestreo descendente propuesto articula el muestreo descendente al proceso de interpolación. En consecuencia, el algoritmo de muestreo descendente propuesto puede mantener la información original de la imagen de entrada en la mayor medida. La imagen muestreada hacia abajo se alimenta a JPEG. Luego se aplica un procesamiento posterior basado en la variación total (TV) a la imagen descomprimida de baja resolución. Por último,Los resultados experimentales verifican que utilizando la imagen muestreada por el algoritmo propuesto, se puede lograr una imagen interpolada con una calidad mucho mayor. Además, el algoritmo propuesto puede lograr un rendimiento superior al JPEG para la codificación de imágenes de baja velocidad de bits.

Instantánea de papel

(vea el enlace provisto para todos los detalles, fórmulas, etc.)

Comunidad
fuente
Esta también es una gran solución. ¡Gracias!
Confile
Esta es una gran solución. Lo probé en archivos png con áreas transparentes. Aquí está el resultado: jsfiddle.net/confile/5CD4N ¿Tiene alguna idea de qué hacer para que funcione?
Confile
1
esto es GENIO! pero por favor, ¿puedes explicar qué estás haciendo exactamente? jajaja .. estoy totalmente queriendo saber los entresijos ... tal vez recursos para aprender?
carinlynchin
1
@Carine que puede ser demasiado para un campo de comentario pobre :) pero, reducir la escala muestra un grupo de píxeles para promediar uno nuevo que represente ese grupo. Esto es en efecto un filtro de paso bajo que introduce algo de desenfoque en general. Para compensar la pérdida de nitidez, simplemente aplique una convolución de afilado. Como el enfoque puede ser muy pronunciado, podemos mezclarlo con la imagen para controlar el nivel de enfoque. Espero que te dé una idea.
21

Si desea utilizar solo el lienzo, el mejor resultado será con múltiples pasos hacia abajo. Pero eso aún no es suficiente. Para una mejor calidad, necesita una implementación js pura. Acabamos de lanzar pica : reductor de alta velocidad con calidad / velocidad variable. En resumen, redimensiona 1280 * 1024px en ~ 0.1s, y 5000 * 3000px en 1s, con la más alta calidad (filtro lanczos con 3 lóbulos). Pica tiene demo , donde puedes jugar con tus imágenes, niveles de calidad e incluso probarlo en dispositivos móviles.

Pica aún no tiene máscara de enfoque, pero eso se agregará muy pronto. Eso es mucho más fácil que implementar un filtro de convolución de alta velocidad para cambiar el tamaño.

Vitalia
fuente
16

¿Por qué usar el lienzo para cambiar el tamaño de las imágenes? Todos los navegadores modernos usan interpolación bicúbica, el mismo proceso utilizado por Photoshop (si lo estás haciendo bien), y lo hacen más rápido que el proceso de lienzo. Simplemente especifique el tamaño de imagen que desea (use solo una dimensión, altura o ancho, para cambiar el tamaño proporcionalmente).

Esto es compatible con la mayoría de los navegadores, incluidas las versiones posteriores de IE. Las versiones anteriores pueden requerir CSS específico del navegador .

Una función simple (usando jQuery) para cambiar el tamaño de una imagen sería así:

function resizeImage(img, percentage) {
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return {"width": width*coeff, "height": height*coeff}           
}

Luego, solo use el valor devuelto para cambiar el tamaño de la imagen en una o ambas dimensiones.

Obviamente, hay diferentes mejoras que podría hacer, pero esto hace el trabajo.

Pegue el siguiente código en la consola de esta página y observe lo que les sucede a los gravatares:

function resizeImage(img, percentage) {
    var coeff = percentage/100,
        width = $(img).width(),
        height = $(img).height();

    return {"width": width*coeff, "height": height*coeff}           
}

$('.user-gravatar32 img').each(function(){
  var newDimensions = resizeImage( this, 150);
  this.style.width = newDimensions.width + "px";
  this.style.height = newDimensions.height + "px";
});
Robusto
fuente
2
También tenga en cuenta que si solo especifica una dimensión, el navegador (moderno) mantendrá automáticamente la relación de aspecto natural de la imagen.
André Dion
38
Quizás necesite enviar la imagen redimensionada a un servidor.
Sergiu Paraschiv
2
@Sergiu: No es necesario, pero tenga en cuenta que si va de una imagen muy pequeña a una muy grande, no obtendrá grandes resultados incluso de un servidor.
Robusto
2
@Robusto Necesito poner la imagen en el lienzo después y enviarla al servidor más adelante. Quiero reducir una imagen grande a una pequeña, modificar el color en un lienzo y enviar el resultado al servidor. ¿Que crees que deberia hacer?
Confile el
9
@Robusto Este es el problema. Mostrar una pequeña imagen en el cliente es fácil. img.width nad img.height es muy trivial. Quiero reducirlo solo una vez y no de nuevo en el servidor.
Confile
8

No es la respuesta correcta para las personas que realmente necesitan cambiar el tamaño de la imagen en sí, sino solo para reducir el tamaño del archivo .

Tuve un problema con las imágenes "directamente desde la cámara", que mis clientes a menudo cargaban en JPEG "sin comprimir".

No tan conocido es que el lienzo admite (en la mayoría de los navegadores 2017) cambiar la calidad de JPEG

data=canvas.toDataURL('image/jpeg', .85) # [1..0] default 0.92

Con este truco podría reducir las fotos de 4k x 3k con> 10Mb a 1 o 2Mb, seguro que depende de sus necesidades.

mira aquí

medio bit
fuente
4

Aquí hay un servicio angular reutilizable para cambiar el tamaño de la imagen / lienzo de alta calidad: https://gist.github.com/fisch0920/37bac5e741eaec60e983

El servicio admite la convolución de lanczos y la reducción gradual gradual. El enfoque de convolución es de mayor calidad a costa de ser más lento, mientras que el enfoque de reducción gradual gradual produce resultados razonablemente antialias y es significativamente más rápido.

Ejemplo de uso:

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
4

Este es el filtro de cambio de tamaño Hermite mejorado que utiliza 1 trabajador para que la ventana no se congele.

https://github.com/calvintwr/blitz-hermite-resize

const blitz = Blitz.create()

/* Promise */
blitz({
    source: DOM Image/DOM Canvas/jQuery/DataURL/File,
    width: 400,
    height: 600
}).then(output => {
    // handle output
})catch(error => {
    // handle error
})

/* Await */
let resized = await blizt({...})

/* Old school callback */
const blitz = Blitz.create('callback')
blitz({...}, function(output) {
    // run your callback.
})
Calvintwr
fuente
3

Encontré una solución que no necesita acceder directamente a los datos de píxeles y recorrerlos para realizar el muestreo. Dependiendo del tamaño de la imagen, esto puede requerir muchos recursos y sería mejor utilizar los algoritmos internos del navegador.

La función drawImage () está utilizando un método de remuestreo de interpolación lineal y vecino más cercano. Eso funciona bien cuando no redimensiona más de la mitad del tamaño original .

Si realiza un bucle para cambiar el tamaño solo la mitad a la vez, los resultados serían bastante buenos y mucho más rápidos que acceder a los datos de píxeles.

Esta función reduce la muestra a la mitad a la vez hasta alcanzar el tamaño deseado:

  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
¿Podría publicar un jsfiddle y algunas imágenes resultantes?
Confile
En el enlace en la parte inferior puedes encontrar imágenes resultantes usando esta técnica
Jesús Carrera
1

Tal vez hombre, puedes probar esto, que siempre uso en mi proyecto. De esta manera, no solo puedes obtener imágenes de alta calidad, sino cualquier otro elemento en tu lienzo.

/* 
 * @parame canvas => canvas object
 * @parame rate => the pixel quality
 */
function setCanvasSize(canvas, rate) {
    const scaleRate = rate;
    canvas.width = window.innerWidth * scaleRate;
    canvas.height = window.innerHeight * scaleRate;
    canvas.style.width = window.innerWidth + 'px';
    canvas.style.height = window.innerHeight + 'px';
    canvas.getContext('2d').scale(scaleRate, scaleRate);
}
RandomYang
fuente
0

en lugar de .85 , si agregamos 1.0 . Obtendrá la respuesta exacta.

data=canvas.toDataURL('image/jpeg', 1.0);

Puede obtener una imagen clara y brillante. por favor, compruebe

Fénix
fuente
0

Realmente trato de evitar ejecutar datos de imagen, especialmente en imágenes más grandes. Por lo tanto, se me ocurrió una forma bastante simple de reducir decentemente el tamaño de la imagen sin restricciones ni limitaciones mediante unos pocos pasos adicionales. Esta rutina se reduce al medio paso más bajo posible antes del tamaño objetivo deseado. Luego lo escala hasta el doble del tamaño objetivo y luego la mitad nuevamente. Suena divertido al principio, pero los resultados son asombrosamente buenos y llegan rápidamente.

function resizeCanvas(canvas, newWidth, newHeight) {
  let ctx = canvas.getContext('2d');
  let buffer = document.createElement('canvas');
  buffer.width = ctx.canvas.width;
  buffer.height = ctx.canvas.height;
  let ctxBuf = buffer.getContext('2d');
  

  let scaleX = newWidth / ctx.canvas.width;
  let scaleY = newHeight / ctx.canvas.height;

  let scaler = Math.min(scaleX, scaleY);
  //see if target scale is less than half...
  if (scaler < 0.5) {
    //while loop in case target scale is less than quarter...
    while (scaler < 0.5) {
      ctxBuf.canvas.width = ctxBuf.canvas.width * 0.5;
      ctxBuf.canvas.height = ctxBuf.canvas.height * 0.5;
      ctxBuf.scale(0.5, 0.5);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      ctx.canvas.width = ctxBuf.canvas.width;
      ctx.canvas.height = ctxBuf.canvas.height;
      ctx.drawImage(buffer, 0, 0);

      scaleX = newWidth / ctxBuf.canvas.width;
      scaleY = newHeight / ctxBuf.canvas.height;
      scaler = Math.min(scaleX, scaleY);
    }
    //only if the scaler is now larger than half, double target scale trick...
    if (scaler > 0.5) {
      scaleX *= 2.0;
      scaleY *= 2.0;
      ctxBuf.canvas.width = ctxBuf.canvas.width * scaleX;
      ctxBuf.canvas.height = ctxBuf.canvas.height * scaleY;
      ctxBuf.scale(scaleX, scaleY);
      ctxBuf.drawImage(canvas, 0, 0);
      ctxBuf.setTransform(1, 0, 0, 1, 0, 0);
      scaleX = 0.5;
      scaleY = 0.5;
    }
  } else
    ctxBuf.drawImage(canvas, 0, 0);

  //wrapping things up...
  ctx.canvas.width = newWidth;
  ctx.canvas.height = newHeight;
  ctx.scale(scaleX, scaleY);
  ctx.drawImage(buffer, 0, 0);
  ctx.setTransform(1, 0, 0, 1, 0, 0);
}
Timur Baysal
fuente
-1

context.scale(xScale, yScale)

<canvas id="c"></canvas>
<hr/>
<img id="i" />

<script>
var i = document.getElementById('i');

i.onload = function(){
    var width = this.naturalWidth,
        height = this.naturalHeight,
        canvas = document.getElementById('c'),
        ctx = canvas.getContext('2d');

    canvas.width = Math.floor(width / 2);
    canvas.height = Math.floor(height / 2);

    ctx.scale(0.5, 0.5);
    ctx.drawImage(this, 0, 0);
    ctx.rect(0,0,500,500);
    ctx.stroke();

    // restore original 1x1 scale
    ctx.scale(2, 2);
    ctx.rect(0,0,500,500);
    ctx.stroke();
};

i.src = 'https://static.md/b70a511140758c63f07b618da5137b5d.png';
</script>
moldcraft
fuente
-1

DEMO : Cambiar el tamaño de las imágenes con JS y HTML Canvas Demo fiddler.

Puede encontrar 3 métodos diferentes para hacer este cambio de tamaño, que lo ayudarán a comprender cómo funciona el código y por qué.

https://jsfiddle.net/1b68eLdr/93089/

En el proyecto GitHub se puede encontrar el código completo de la demostración y el método TypeScript que quizás desee utilizar en su código.

https://github.com/eyalc4/ts-image-resizer

Este es el código final:

export class ImageTools {
base64ResizedImage: string = null;

constructor() {
}

ResizeImage(base64image: string, width: number = 1080, height: number = 1080) {
    let img = new Image();
    img.src = base64image;

    img.onload = () => {

        // Check if the image require resize at all
        if(img.height <= height && img.width <= width) {
            this.base64ResizedImage = base64image;

            // TODO: Call method to do something with the resize image
        }
        else {
            // Make sure the width and height preserve the original aspect ratio and adjust if needed
            if(img.height > img.width) {
                width = Math.floor(height * (img.width / img.height));
            }
            else {
                height = Math.floor(width * (img.height / img.width));
            }

            let resizingCanvas: HTMLCanvasElement = document.createElement('canvas');
            let resizingCanvasContext = resizingCanvas.getContext("2d");

            // Start with original image size
            resizingCanvas.width = img.width;
            resizingCanvas.height = img.height;


            // Draw the original image on the (temp) resizing canvas
            resizingCanvasContext.drawImage(img, 0, 0, resizingCanvas.width, resizingCanvas.height);

            let curImageDimensions = {
                width: Math.floor(img.width),
                height: Math.floor(img.height)
            };

            let halfImageDimensions = {
                width: null,
                height: null
            };

            // Quickly reduce the size by 50% each time in few iterations until the size is less then
            // 2x time the target size - the motivation for it, is to reduce the aliasing that would have been
            // created with direct reduction of very big image to small image
            while (curImageDimensions.width * 0.5 > width) {
                // Reduce the resizing canvas by half and refresh the image
                halfImageDimensions.width = Math.floor(curImageDimensions.width * 0.5);
                halfImageDimensions.height = Math.floor(curImageDimensions.height * 0.5);

                resizingCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                    0, 0, halfImageDimensions.width, halfImageDimensions.height);

                curImageDimensions.width = halfImageDimensions.width;
                curImageDimensions.height = halfImageDimensions.height;
            }

            // Now do final resize for the resizingCanvas to meet the dimension requirments
            // directly to the output canvas, that will output the final image
            let outputCanvas: HTMLCanvasElement = document.createElement('canvas');
            let outputCanvasContext = outputCanvas.getContext("2d");

            outputCanvas.width = width;
            outputCanvas.height = height;

            outputCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                0, 0, width, height);

            // output the canvas pixels as an image. params: format, quality
            this.base64ResizedImage = outputCanvas.toDataURL('image/jpeg', 0.85);

            // TODO: Call method to do something with the resize image
        }
    };
}}
Eyal c
fuente