Imagem de redimensionamento de tela HTML5 (escala reduzida) Alta qualidade?

149

Eu uso elementos de tela html5 para redimensionar imagens no meu navegador. Acontece que a qualidade é muito baixa. Encontrei o seguinte: Desativar interpolação ao dimensionar um <canvas> mas isso não ajuda a aumentar a qualidade.

Abaixo está o meu código css e js, bem como a imagem scalled com Photoshop e redimensionada na API do canvas.

O que devo fazer para obter a melhor qualidade ao dimensionar uma imagem no navegador?

Nota: Quero reduzir uma imagem grande para uma pequena, modificar a cor em uma tela e enviar o resultado da tela para o 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);
});

A imagem foi redimensionada com o photoshop:

insira a descrição da imagem aqui

A imagem redimensionada na tela:

insira a descrição da imagem aqui

Editar:

Tentei fazer o downscaling em mais de uma etapa, conforme proposto em:

Redimensionando uma imagem em uma tela HTML5 e Html5 drawImage: como aplicar antialiasing

Esta é a função que eu usei:

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);


}

Aqui está o resultado se eu usar um dimensionamento em 2 etapas:

insira a descrição da imagem aqui

Aqui está o resultado se eu usar um dimensionamento em três etapas:

insira a descrição da imagem aqui

Aqui está o resultado se eu usar um dimensionamento em 4 etapas:

insira a descrição da imagem aqui

Aqui está o resultado se eu usar um dimensionamento de 20 etapas:

insira a descrição da imagem aqui

Nota: Acontece que de 1 a 2 etapas há uma grande melhoria na qualidade da imagem, mas quanto mais etapas você adiciona ao processo, mais difusa fica a imagem.

Existe uma maneira de resolver o problema em que a imagem fica mais confusa quanto mais etapas você adicionar?

Edit 04/10/2013: Eu tentei o algoritmo do GameAlchemist. Aqui está o resultado comparado ao Photoshop.

Imagem do PhotoShop:

Imagem do PhotoShop

Algoritmo do GameAlchemist:

Algoritmo do GameAlchemist

confundir
fonte
2
Você pode tentar escalar a imagem de forma incremental: stackoverflow.com/questions/18761404/…
markE
1
possível duplicação do Html5 canvas drawImage: como aplicar o antialiasing . Veja se não funciona. Se as imagens são grandes e reduzido ao tamanho pequeno você terá que fazê-lo em etapas (ver imagens de exemplo no link)
2
O @confile desativando a interpolação tornará as coisas piores. Você deseja manter isso ativado. Veja o link que forneci acima. Eu mostro lá como usar as etapas para reduzir imagens maiores e manter a qualidade. E, como Scott diz, você quer priorizar a qualidade em detrimento da velocidade.
1
@ Ken-AbdiasSoftware Eu tentei abordar você, mas o problema é que piorará quanto mais rodadas eu usar para o dimensionamento gradual. Alguma idéia de como consertar isso?
confile 21/09/13
3
Certamente as chances de replicar a funcionalidade de um software de edição de fotos profissional caro usando HTML5 são muito pequenas? Você provavelmente pode se aproximar (ish), mas exatamente como funciona no Photoshop, eu imaginaria que seria impossível!
Liam

Respostas:

171

Como seu problema é reduzir a escala da imagem, não há sentido em falar sobre interpolação, que é sobre a criação de pixels. O problema aqui é a redução da amostra.

Para reduzir a amostra de uma imagem, precisamos transformar cada quadrado de p * p pixels na imagem original em um único pixel na imagem de destino.

Por motivos de desempenho, os navegadores fazem uma redução de escala muito simples: para criar uma imagem menor, eles apenas escolhem UM pixel na fonte e usam seu valor para o destino. que 'esquece' alguns detalhes e adiciona ruído.

No entanto, há uma exceção: como a redução de tamanho da imagem 2X é muito simples de calcular (média de 4 pixels para criar um) e é usada para pixels retina / HiDPI, esse caso é tratado adequadamente - o navegador usa 4 pixels para fazer 1-.

MAS ... se você usar várias vezes uma downsampling 2X, enfrentará o problema de que os sucessivos erros de arredondamento adicionarão muito ruído.
O que é pior: você nem sempre será redimensionado com uma potência de dois e redimensionar para a potência mais próxima + um último redimensionamento é muito barulhento.

O que você procura é uma redução de tamanho de pixel perfeita, ou seja: uma nova amostragem da imagem que levará em consideração todos os pixels de entrada, independentemente da escala.
Para fazer isso, devemos calcular, para cada pixel de entrada, sua contribuição para um, dois ou quatro pixels de destino, dependendo de a projeção em escala dos pixels de entrada estar dentro dos pixels de destino, sobrepor uma borda X, Y ou ambas .
(Um esquema seria bom aqui, mas eu não tenho um.)

Aqui está um exemplo de escala de tela versus a escala perfeita de meu pixel em uma escala 1/3 de um zombat.

Observe que a imagem pode ser redimensionada no seu navegador e é .jpegized pelo SO.
No entanto, vemos que há muito menos barulho, especialmente na grama atrás do wombat e nos galhos à direita. O barulho no pêlo o torna mais contrastado, mas parece que ele tem cabelos brancos - uma imagem de fonte parecida -.
A imagem certa é menos cativante, mas definitivamente melhor.

insira a descrição da imagem aqui

Aqui está o código para fazer o downscaling perfeito de pixels:

resultado do violino: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
próprio do violino: 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;
}

É bastante ganancioso na memória, pois é necessário um buffer flutuante para armazenar os valores intermediários da imagem de destino (-> se contarmos a tela de resultado, usamos 6 vezes a memória da imagem de origem nesse algoritmo).
Também é bastante caro, já que cada pixel de origem é usado independentemente do tamanho do destino, e temos que pagar pelo getImageData / putImageDate, bem lento também.
Mas não há como ser mais rápido do que processar cada valor de origem nesse caso, e a situação não é tão ruim: para minha imagem de um wombat de 740 * 556, o processamento leva entre 30 e 40 ms.

GameAlchemist
fonte
Poderia ser mais rápido se você dimensionasse a imagem antes de colocá-la na tela?
Confile4 de
eu não entendo ... parece que é o que eu faço. O buffer e a tela que eu criei (resCV) têm o tamanho da imagem em escala. Eu acho que a única maneira de obtê-lo mais rápido seria usar o cálculo de número inteiro do tipo breshensam. Mas 40ms é lento apenas para um videogame (25 fps), não para um aplicativo de desenho.
GameAlchemist
você vê alguma chance de acelerar seu algoritmo enquanto mantém a qualidade?
confile
1
Eu tentei arredondar o buffer (parte mais recente do algoritmo) usando 0 | em vez de Mat.ceil. É um pouco mais rápido. Mas, de qualquer maneira, há alguma sobrecarga com o get / putImageData e, novamente, não podemos evitar processar cada pixel.
GameAlchemist
4
Ok, então eu assisti o código: você estava muito perto da solução. Dois erros: seus índices foram reduzidos em um para tX + 1 (eram + 3, + 4, + 5, + 6 em vez de +4, +5, +6, +7), e a mudança de linha em rgba é uma multa por 4, não por 3. Acabei de testar 4 valores aleatórios para verificar (0,1, 0,15, 0,33, 0,8) que parecia ok. seu violino atualizado está aqui: jsfiddle.net/gamealchemist/kpQyE/3
GameAlchemist
51

Reamostragem rápida de tela com boa qualidade: http://jsfiddle.net/9g9Nv/442/

Atualização: versão 2.0 (mais rápido, trabalhadores da Web + objetos transferíveis) - 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
fonte
Eu preciso da melhor qualidade
confile
18
corrigido, mudei "bom" para "melhor", está tudo bem agora? : D. Por outro lado, se você quiser a melhor amostra possível - use imagemagick.
precisa
@confile imgur.com era seguro para usar no jsfiddle, mas os administradores fizeram algo errado? Você não vê boa qualidade, porque o seu navegador dá erro fatal ao CORS. (não pode usar imagem de sites remotos)
ViliusL
Ok, você pode usar qualquer outra imagem PNG com áreas transparentes. Alguma idéia disso?
confile
4
@confile você estava certo, em alguns casos imagens transparentes tiveram problemas em áreas nítidas. Perdi esses casos com meu teste. O redimensionamento fixo também corrigiu o suporte à imagem remota no fiddle: jsfiddle.net/9g9Nv/49
#
28

Sugestão 1 - estenda a tubulação do processo

Você pode usar o abandono conforme descrito nos links a que você se refere, mas parece usá-los de maneira errada.

Não é necessário reduzir o tamanho da imagem para proporções acima de 1: 2 (normalmente, mas não se limitando a). É onde você precisa fazer uma drástica redução de escala, é necessário dividi-la em duas (e raramente, mais) etapas, dependendo do conteúdo da imagem (em particular onde ocorrem altas frequências, como linhas finas).

Toda vez que você baixa uma amostra da imagem, perde detalhes e informações. Você não pode esperar que a imagem resultante seja tão nítida quanto a original.

Se você reduzir as imagens em várias etapas, perderá muitas informações no total e o resultado será ruim, como você já notou.

Tente com apenas um passo extra, ou no máximo dois.

Convoluções

No caso do Photoshop, observe que ele aplica uma convolução após a nova amostra da imagem, como a nitidez. Não é apenas a interpolação bicúbica que ocorre; para emular completamente o Photoshop, precisamos adicionar também as etapas que o Photoshop está executando (com a configuração padrão).

Neste exemplo, usarei minha resposta original a que você se refere em sua postagem, mas adicionei uma convolução mais nítida para melhorar a qualidade como um processo de postagem (consulte a demonstração na parte inferior).

Aqui está o código para adicionar filtro de nitidez (é baseado em um filtro de convolução genérico - eu coloquei a matriz de peso para nitidez dentro dela, bem como um fator de mistura para ajustar a pronúncia do efeito):

Uso:

sharpen(context, width, height, mixFactor);

o mixFactor valor é entre [0.0, 1.0] e permite subestimar o efeito de nitidez - regra geral: quanto menor o tamanho, menor é o efeito.

Função (com base neste trecho ):

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);
}

O resultado do uso dessa combinação será:

DEMO ONLINE AQUI

Reduzir a amostra do resultado e aprimorar a convolução

Dependendo da quantidade de nitidez que você deseja adicionar à mistura, é possível obter o resultado de "desfocado" padrão a muito nítido:

Variações de nitidez

Sugestão 2 - implementação de algoritmo de baixo nível

Se você deseja obter o melhor resultado em termos de qualidade, precisará diminuir o nível e considerar a implementação, por exemplo, deste novo algoritmo para fazer isso.

Consulte Downsampling de imagem dependente de interpolação (2011) do IEEE.
Aqui está um link para o artigo na íntegra (PDF) .

No momento, não há implementações desse algoritmo no JavaScript AFAIK de JavaScript, portanto, você terá uma mão cheia se quiser se dedicar a essa tarefa.

A essência é (trechos do artigo):

Resumo

Um algoritmo de amostragem adaptativa orientada para interpolação é proposto para codificação de imagens com baixa taxa de bits neste artigo. Dada uma imagem, o algoritmo proposto é capaz de obter uma imagem de baixa resolução, a partir da qual uma imagem de alta qualidade com a mesma resolução que a imagem de entrada pode ser interpolada. Diferente dos algoritmos tradicionais de baixa amostragem, que são independentes do processo de interpolação, o algoritmo proposto de baixa amostragem articula a baixa amostragem ao processo de interpolação. Consequentemente, o algoritmo de amostragem proposto é capaz de manter a informação original da imagem de entrada na maior extensão possível. A imagem amostrada para baixo é então alimentada em JPEG. Um pós-processamento baseado em variação total (TV) é então aplicado à imagem de baixa resolução descomprimida. Por fim,Resultados experimentais verificam que, utilizando a imagem reduzida por amostragem pelo algoritmo proposto, é possível obter uma imagem interpolada com qualidade muito mais alta. Além disso, o algoritmo proposto é capaz de obter desempenho superior ao JPEG para codificação de imagens com baixa taxa de bits.

Instantâneo do papel

(consulte o link fornecido para todos os detalhes, fórmulas etc.)

Comunidade
fonte
Essa também é uma ótima solução. Obrigado!
Confile 17/04
Esta é uma otima soluçao. Eu tentei em arquivos png com áreas transparentes. Aqui está o resultado: jsfiddle.net/confile/5CD4N Você tem alguma idéia do que fazer para fazer funcionar?
Confile 17/04
1
isso é GÊNIO! mas por favor, você pode explicar o que exatamente está fazendo? lol .. estou totalmente querendo saber os prós e contras ... talvez recursos para aprender?
Carllynchin
1
@Carine que pode ser um pouco demais para um campo de comentário ruim :) mas, redimensionar uma nova amostra de um grupo de pixels para obter a média de um novo representando esse grupo. Este é, de fato, um filtro passa-baixo que introduz um pouco de desfoque geral. Para compensar a perda de nitidez, basta aplicar uma convolução de nitidez. Como a nitidez pode ser muito acentuada, podemos misturá-la com a imagem, para controlar o nível de nitidez. Espero que isso aconteça.
21

Se você deseja usar apenas a tela, o melhor resultado será com várias etapas descendentes. Mas isso não é bom o suficiente ainda. Para uma melhor qualidade, você precisa de uma implementação js pura. Acabamos de lançar o pica - downscaler de alta velocidade com qualidade / velocidade variável. Em resumo, ele redimensiona 1280 * 1024px em ~ 0,1s e 5000 * 3000px em 1s, com a mais alta qualidade (filtro lanczos com 3 lobos). O Pica possui demonstração , onde você pode brincar com suas imagens, níveis de qualidade e até experimentá-lo em dispositivos móveis.

O Pica ainda não tem uma máscara de nitidez, mas isso será adicionado muito em breve. Isso é muito mais fácil do que implementar o filtro de convolução de alta velocidade para redimensionar.

Vitaly
fonte
16

Por que usar a tela para redimensionar imagens? Todos os navegadores modernos usam interpolação bicúbica - o mesmo processo usado pelo Photoshop (se você estiver fazendo certo) - e eles fazem isso mais rapidamente do que o processo de tela. Basta especificar o tamanho da imagem que você deseja (use apenas uma dimensão, altura ou largura, para redimensionar proporcionalmente).

Isso é suportado pela maioria dos navegadores, incluindo versões posteriores do IE. Versões anteriores podem exigir CSS específico do navegador .

Uma função simples (usando jQuery) para redimensionar uma imagem seria assim:

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

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

Em seguida, basta usar o valor retornado para redimensionar a imagem em uma ou ambas as dimensões.

Obviamente, existem diferentes refinamentos que você pode fazer, mas isso faz o trabalho.

Cole o seguinte código no console desta página e veja o que acontece com os 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
fonte
2
Observe também que, se você especificar apenas uma dimensão, o navegador (moderno) manterá automaticamente a proporção natural da imagem.
André Dion
38
Talvez ele precise enviar a imagem redimensionada para um servidor.
Sergiu Paraschiv
2
@Sergiu: Não é necessário, mas observe que se você estiver passando de uma imagem muito pequena para uma muito grande, não obterá ótimos resultados, mesmo em um servidor.
Robusto
2
@ Robusto Eu preciso colocar a imagem na tela depois e enviá-la ao servidor mais tarde. Quero reduzir uma imagem grande para uma pequena, modificar cores em uma tela e enviar o resultado ao servidor. O que você acha que eu deveria fazer?
Confile2
9
@ Robusto Este é o problema. É fácil mostrar uma imagem pequena no cliente. img.width nad img.height é tão trivial. Eu quero reduzi-lo apenas uma vez e não novamente no servidor.
confile
8

Não é a resposta certa para pessoas que realmente precisam redimensionar a imagem, mas apenas para diminuir o tamanho do arquivo .

Eu tive um problema com as fotos "diretamente da câmera", que meus clientes frequentemente carregavam em JPEG "não compactado".

Não é tão conhecido que o canvas suporta (na maioria dos navegadores 2017) para alterar a qualidade do JPEG

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

Com esse truque, eu poderia reduzir fotos de 4k x 3k com> 10Mb para 1 ou 2Mb, com certeza depende de suas necessidades.

olhe aqui

halfbit
fonte
4

Aqui está um serviço Angular reutilizável para redimensionamento de imagem / tela de alta qualidade: https://gist.github.com/fisch0920/37bac5e741eaec60e983

O serviço suporta a convolução de lanczos e o downscaling gradual. A abordagem de convolução é de qualidade mais alta, ao custo de ser mais lenta, enquanto a abordagem de redução de escala por etapas produz resultados razoavelmente antialias e é significativamente mais rápida.

Exemplo 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
fonte
4

Este é o filtro de redimensionamento Hermite aprimorado que utiliza 1 trabalhador para que a janela não 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
fonte
3

Encontrei uma solução que não precisa acessar diretamente os dados de pixel e percorrê-los para realizar a redução da amostragem. Dependendo do tamanho da imagem, isso pode consumir muitos recursos, e seria melhor usar os algoritmos internos do navegador.

A função drawImage () está usando um método de reamostragem de interpolação linear e vizinho mais próximo. Isso funciona bem quando você não está redimensionando mais da metade do tamanho original .

Se você fizer um loop para redimensionar no máximo metade de cada vez, os resultados serão muito bons e muito mais rápidos do que acessar dados de pixel.

Esta função reduz a amostra para metade de cada vez até atingir o tamanho desejado:

  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
fonte
Você poderia postar um jsfiddle e algumas imagens resultantes?
confile 27/08/14
No link na parte inferior pode encontrar resultando imagens usando esta técnica
Jesús Carrera
1

Talvez cara, você pode tentar isso, o que eu sempre uso no meu projeto. Dessa forma, você pode obter não apenas imagens de alta qualidade, mas qualquer outro elemento em sua tela.

/* 
 * @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
fonte
0

em vez de 0,85 , se adicionarmos 1,0 . Você receberá uma resposta exata.

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

Você pode obter uma imagem clara e brilhante. por favor, verifique

Fénix
fonte
0

Eu realmente tento evitar percorrer dados de imagem, especialmente em imagens maiores. Assim, criei uma maneira bastante simples de reduzir decentemente o tamanho da imagem sem restrições ou limitações, usando algumas etapas extras. Essa rotina desce para a metade mais baixa possível antes do tamanho desejado. Em seguida, ele aumenta o tamanho até o dobro do tamanho desejado e depois metade novamente. Parece engraçado no começo, mas os resultados são surpreendentemente bons e vão para lá rapidamente.

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
fonte
-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>
artesanato
fonte
-1

DEMO : Redimensionando imagens com o violinista JS e HTML Canvas Demo.

Você pode encontrar três métodos diferentes para redimensionar, que ajudarão a entender como o código está funcionando e por quê.

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

O código completo da demonstração e o método TypeScript que você pode usar no seu código podem ser encontrados no projeto GitHub.

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

Este é o 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
fonte