HTML5画布如何高质量调整(缩小)图像大小?

180

我在浏览器中使用html5画布元素来调整图像大小,但是发现质量很低。我发现了这个链接:Disable Interpolation when Scaling a <canvas>,但它并没有帮助我提高质量。

下面是我的CSS和JS代码以及用Photoshop缩放和用画布API缩放的图像。

在浏览器中缩放图像以获得最佳质量,该怎么做?

注意:我想将大图像缩小到小图像,在画布中修改颜色并将结果从画布发送到服务器。

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

使用Photoshop调整大小的图像:

enter image description here

在画布上调整大小的图像:

enter image description here

编辑:

我尝试按照在HTML5画布中调整图像大小Html5画布drawImage:如何应用抗锯齿中提出的方法进行多步缩小。

这是我使用的函数:

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


}

如果我使用2步缩小,则结果如下所示:

enter image description here

如果我使用3步缩小,则结果如下所示:

enter image description here

如果我使用4步缩小,则结果如下所示:

enter image description here

如果我使用20步缩小,则结果如下所示:

enter image description here

注意:从1步到2步,图像质量有很大提高,但是添加更多步骤会使图像变得更模糊。

有没有办法解决添加更多步骤会使图像变得更模糊的问题?

编辑2013-10-04:我尝试了GameAlchemist的算法。以下是与Photoshop相比的结果。

Photoshop图像:

PhotoShop Image

GameAlchemist的算法:

GameAlchemist's Algorithm


2
你可以尝试逐步缩放图像:https://dev59.com/jmMl5IYBdhLWcg3wBy_y - markE
1
可能是Html5 canvas drawImage: how to apply antialiasing的重复问题。看看是否有效。如果图像很大并且缩小到小尺寸,则需要分步进行(请参见链接中的示例图像)。 - user1693593
2
@confile 关闭插值会使情况变得更糟。您需要保持启用状态。请查看我提供的链接。我在那里展示了如何使用步骤缩小较大的图像并保持质量。正如Scott所说,您希望将质量放在速度之上。 - user1693593
1
相关:https://dev59.com/9HE95IYBdhLWcg3wdtmj - ViliusL
3
用HTML5复制昂贵的专业照片编辑软件的功能的机会肯定很小吧?你可能能够接近(大约),但想要完全像Photoshop一样工作,我想是不可能的! - Liam
显示剩余20条评论
14个回答

195

由于您的问题是缩小图像,所以谈论插值-即创建像素-是没有意义的。问题在于下采样。

要对图像进行下采样,我们需要将原始图像中每个p * p像素的正方形转换为目标图像中的单个像素。

出于性能考虑,浏览器执行非常简单的下采样:为构建较小的图像,它们将只从源图像中选择一个像素,并使用其值作为目标像素,这将“遗忘”一些细节并添加噪声。

然而,有一个例外:由于2倍图像下采样非常容易计算(平均4个像素),并且用于retina/HiDPI像素,因此此案例得到了适当处理-浏览器确实使用4个像素来制作一个像素。

但是……如果您多次使用2倍下采样,则会面临连续的舍入误差过多的问题。
更糟糕的是,您不总是按二的幂调整大小,而调整大小到最近的2的幂+最后调整大小会非常嘈杂。

您要寻求的是像素完美的下采样,也就是说:重新采样图像将考虑所有输入像素-无论缩放比例如何。

为此,我们必须计算每个输入像素对一个、两个或四个目标像素的贡献,具体取决于输入像素的缩放投影是否在目标像素内部,重叠X边界、Y边界还是两者都有。
(此处应该有一个示意图,但我没有。)

以下是画布缩放与我的像素完美缩放的1/3 zombat示例。

请注意,图片可能会在您的浏览器中缩放,并且被S.O.压缩成JPEG格式。
然而,我们可以看到,噪点明显减少了,尤其是在袋熊后面的草和右边的树枝上。毛皮上的噪音使其更加对比,但它看起来像是白色头发-与原始图片不同-。
右侧的图像不太引人注目,但绝对更好。

enter image description here

以下是进行像素完美缩小的代码:

fiddle结果: http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/
fiddle本身: 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;
}

这个算法在内存方面要求很高,因为需要一个浮点数缓冲区来存储目标图像的中间值(-> 如果我们计算结果画布,则在此算法中使用了源图像内存的6倍)。


它也相当昂贵,因为无论目标大小如何,每个源像素都被使用,并且我们不得不为getImageData / putImageData付出代价,速度也很慢。


但在这种情况下,没有比处理每个源值更快的方法,而且情况并不那么糟糕:对于我那张 740 * 556 的袋熊图像,处理需要花费 30 至 40 毫秒。


如果在将图像放入画布之前对其进行缩放,速度会更快吗? - Michael
1
我不太明白...看起来这是我所做的。我创建的缓冲区和画布(resCV)都有缩放图像的大小。我认为更快的方法只有使用类似于Breshensam的整数计算。但是40毫秒对于绘图应用程序来说并不慢,只有对于视频游戏(25 fps)才算慢。 - GameAlchemist
你觉得有没有可能在保持质量的同时让算法更快? - Michael
1
我尝试使用0 | 而不是Mat.ceil来舍入缓冲区(算法的最新部分)。这样会稍微快一些。但无论如何,使用get/putImageData仍然存在相当大的开销,而且我们无法避免处理每个像素。 - GameAlchemist
4
好的,我看了代码:你离解决方案非常近。有两个错误:对于tX+1,你的索引偏差了一位(应该是+4,+5,+6,+7而不是+3,+4,+5,+6),同时修改rgba中的行需要乘以4而不是3。我只测试了4个随机值(0.1,0.15,0.33,0.8),看起来没问题。你更新的fiddle在这里:http://jsfiddle.net/gamealchemist/kpQyE/3/。 - GameAlchemist
显示剩余25条评论

67

使用高质量的快速画布重采样:http://jsfiddle.net/9g9Nv/442/

更新:版本2.0(更快,支持Web Workers和可传递对象) - 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);
}

我需要最高质量的。 - Michael
20
我把“好”改成了“最好”,现在可以了吗? :D。另一方面,如果您想要最佳的重新采样效果,请使用ImageMagick。 - ViliusL
4
@confile 你是正确的,在某些情况下透明图像在锐利区域会出现问题。我在测试中错过了这些情况。固定大小也修复了 JSFiddle 上远程图像支持的问题:http://jsfiddle.net/9g9Nv/49/。 - ViliusL
我在使用resample_hermite函数时遇到了一些麻烦:当我试图将图像缩小太多时,它会弯曲图像并改变颜色。我修改了一个jsfiddle来演示这个问题。原始fiddle错误的fiddle。当你尝试缩小太多时,似乎这个算法会失败。有人知道为什么吗? - jsarma
1
@newdeveloper http://jsfiddle.net/j8qe9ot7/ 但是你不应该在评论中问与此无关的问题,而应该创建一个新的单独的问题。 - ViliusL
显示剩余10条评论

31

建议一 - 扩展处理管道

您可以像您所提到的链接中描述的那样使用降采样,但似乎您使用它们的方式不正确。

在比例高于1:2(通常不限于此)时,不需要使用降采样来缩放图像。只有当您需要进行极端的缩小时,才需要根据图像的内容(特别是出现高频率,如细线条的地方)将其分成两个(很少情况下是多个)步骤。

每次缩小图像时,都会丢失细节和信息。您不能指望生成的图像与原始图像一样清晰。

如果您随后在许多步骤中缩小图像,则总共将丢失大量信息,结果将像您已经注意到的那样糟糕。

尝试仅使用一个额外的步骤,或最多两个。

卷积

在Photoshop的情况下,请注意它在重新采样图像后应用卷积,例如锐化。这不仅仅是双三次插值,因此为了完全模拟Photoshop,我们还需要添加Photoshop正在执行的步骤(使用默认设置)。

针对这个例子,我将使用你在帖子中提到的我的原始回答,但我已经添加了一个锐化卷积以作为后处理来提高质量(请参见底部的演示)。

以下是添加锐化滤镜的代码(它基于通用卷积滤镜-我将锐化的权重矩阵放入其中,并添加了一个混合因素以调整效果的发音):

使用方法:

sharpen(context, width, height, mixFactor);

mixFactor是一个介于[0.0, 1.0]之间的值,允许您减弱锐化效果-经验法则:尺寸越小,需要的效果就越少。

函数(基于this snippet):

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

使用这个组合的结果将会是: 在线演示在此处

Result downsample and sharpen convolution

根据您想要添加的锐化程度,您可以从默认的“模糊”到非常锐利的结果。

Variations of sharpen

建议2 - 低级算法实现

如果你想获得最佳的结果质量,你需要考虑低级算法的实现,例如使用这个全新的算法。

请参阅IEEE的Interpolation-Dependent Image Downsampling(2011)。
这是完整论文的链接(PDF)

据我所知,目前还没有JavaScript实现这个算法,因此如果你想尝试这个任务,你需要付出很大的努力。

其核心思想是(摘自该论文):

摘要

本文提出了一种基于插值的自适应降采样算法,用于低比特率图像编码。给定一幅图像,该算法能够获得一个低分辨率图像,从中可以插值出与输入图像相同分辨率的高质量图像。与传统的独立于插值过程的降采样算法不同,该算法将降采样与插值过程紧密联系在一起。因此,该算法能够最大程度地保留输入图像的原始信息。然后将降采样的图像输入JPEG进行编码。接着对解压缩后的低分辨率图像应用基于总变差(TV)的后处理。最终,处理后的图像被插值以保持输入图像的原始分辨率。实验结果验证了利用该算法的降采样图像可以获得更高质量的插值图像。此外,该算法能够在低比特率图像编码方面取得优越性能,胜过JPEG。

Snapshot from paper

(请查看提供的链接获取所有细节、公式等。)

这是一个很好的解决方案。我已经尝试在具有透明区域的png文件上使用它。这是结果:http://jsfiddle.net/confile/5CD4N/ 你有任何想法如何使它工作吗? - Michael
1
这太厉害了!但是你能解释一下你具体在做什么吗?哈哈,我非常想知道其中的细节...也许有学习资源可以推荐吗? - carinlynchin
1
@Carine 对于一个简单的评论区来说,这可能有点过分了 :) 但是,缩小比例会重新采样一组像素以平均表示该组的新像素。这实际上是一种低通滤波器,会在整体上引入一些模糊。为了补偿锐度损失,只需应用锐化卷积即可。由于锐化可能非常明显,因此我们可以将其与图像混合,以便控制锐化程度。希望这能提供一些见解。 - user1693593

22
如果您只想使用画布,多次降采样将获得最佳效果。但这仍然不够好。为了获得更好的质量,您需要使用纯JS实现。我们刚刚发布了 pica - 具有可变质量/速度的高速缩小图像工具。简而言之,它以最高质量(3个洛伯兹Lanczos滤波器)在 ~0.1秒内调整1280 x 1024像素大小的图像,以及在1秒内调整5000 x 3000像素大小的图像。Pica拥有演示版,您可以在其中处理自己的图片、质量水平,甚至尝试在移动设备上使用。
Pica目前还没有非锐化掩蔽功能,但很快就会添加。这比实现高速卷积过滤器来调整大小要容易得多。

16

为什么要使用画布来调整图像大小?现代浏览器都使用双三次插值——与Photoshop相同的处理过程(如果你做得对的话)——而且它们比画布过程快。只需指定您想要的图像尺寸(仅使用一个维度,高度或宽度,进行等比缩放)。

这受到大多数浏览器的支持,包括较晚版本的IE。早期版本可能需要特定于浏览器的CSS

使用jQuery调整图像大小的简单函数示例如下:

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

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

然后只需使用返回的值来调整图像的一个或两个维度大小。

显然,您可以进行不同的改进,但这样就可以完成任务。

将以下代码粘贴到此页面的控制台中,并观察gravatars会发生什么:

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

2
还要注意,如果您只指定一个维度,(现代)浏览器将自动保持图像的自然纵横比。 - André Dion
43
也许他需要将调整大小后的图像发送到服务器。 - Sergiu Paraschiv
2
@Sergiu:不是必须的,但请注意,即使使用服务器,如果您从非常小的图像转换为非常大的图像,也不会得到很好的结果。 - Robusto
3
我需要将图像放入画布中,并在稍后将其发送到服务器。我想将大图缩小为小图,在画布中修改颜色并将结果发送到服务器。你认为我应该怎么做? - Michael
10
这是问题所在。在客户端上显示小图像很容易,img.width和img.height非常简单。我想只缩小一次,而不是在服务器上再次缩小。 - Michael
显示剩余4条评论

10

对于真正需要调整图像大小的人来说,这并不是正确的答案,但可以缩小文件大小。

我遇到了一个问题,那就是我的客户经常上传“未压缩”的JPEG格式照片,直接从相机中获取。

很少有人知道,画布在大多数浏览器(截至2017年)支持更改JPEG的质量。

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

借助这个技巧,我可以将大小为4k x 3k、大小超过10Mb的图片压缩到1或2Mb,当然这取决于您的需求。

看这里


6
这是改进后的Hermite调整大小滤镜,利用1个工作器使窗口不会冻结。 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 blitz({...})

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

5
我找到了一种解决方案,不需要直接访问像素数据并循环执行降采样操作。根据图像的大小,这可能会非常耗费资源,最好使用浏览器的内部算法。
drawImage()函数使用线性插值、最近邻重采样方法。当您将其缩小的比原始大小还少时,它可以很好地工作。
如果您循环调整大小最多只缩小一半,则结果会相当不错,并且比访问像素数据更快。
该函数每次减半降低分辨率,直到达到所需大小:
  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] );

请问您能否发布一个 jsfiddle 和一些相关的图片? - Michael
在底部的链接中,您可以找到使用此技术生成的图像。 - Jesús Carrera

4
这里有一个可重复使用的Angular服务,用于高质量的图像/画布调整大小:https://gist.github.com/fisch0920/37bac5e741eaec60e983 该服务支持Lanczos卷积和逐步缩小。卷积方法具有更高的质量,但速度较慢,而逐步缩小方法则产生合理的抗锯齿效果,并且速度明显更快。
示例用法:
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
    })
})

2
也许你可以尝试这个方法,在我的项目中我经常使用。这样做不仅可以获得高质量的图像,还可以获取画布上的任何其他元素。
/* 
 * @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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接