首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >HTML5画布调整大小(缩小)图像高质量?

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

Stack Overflow用户
提问于 2013-09-21 01:48:59
回答 14查看 173K关注 0票数 168

我使用html5画布元素在浏览器中调整图像的大小。事实证明,质量非常低。我发现了这一点:Disable Interpolation when Scaling a ,但它对提高质量没有帮助。

下面是我的css和js代码,以及用Photoshop缩放和在canvas API中缩放的图像。

在浏览器中缩放图像时,如何获得最佳质量?

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

CSS:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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调整图像大小:

在画布上调整图像大小:

编辑:

我尝试按照下面的建议在多个步骤中进行缩容:

Resizing an image in an HTML5 canvasHtml5 canvas drawImage: how to apply antialiasing

这是我用过的函数:

代码语言:javascript
复制
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级缩容的结果:

下面是如果我使用3级缩容的结果:

下面是如果我使用4级缩容的结果:

下面是如果我使用20级缩容的结果:

注意:事实证明,从1步到2步,图像质量有了很大的提高,但添加到该过程中的步骤越多,图像就变得越模糊。

添加的步骤越多,图像越模糊,有没有办法解决这个问题?

编辑2013-10-04:我尝试了GameAlchemist的算法。这是与Photoshop进行比较的结果。

PhotoShop图像:

GameAlchemist的算法:

EN

回答 14

Stack Overflow用户

回答已采纳

发布于 2013-10-03 02:46:04

因为你的问题是缩小你的图像,所以没有必要谈论插值-which是关于创建像素的。这里的问题是下采样。

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

出于性能原因,浏览器执行非常简单的下采样:要构建较小的图像,它们只会在源中选择一个像素,并将其值用作目标。这会“忘记”一些细节,并增加噪音。

然而,有一个例外:由于2X图像下采样非常容易计算(平均4个像素来制作一个),并且用于视网膜/HiDPI像素,这种情况处理得当-the浏览器确实使用4个像素来制作一个像素。

但是..。如果你使用几次2倍的下采样,你将面临一个问题,即连续的舍入误差将增加太多的噪声。

更糟糕的是,你不会总是将大小调整为2的幂,并且调整到最近的幂+最后一次调整是非常嘈杂的。

你寻找的是像素完美的下采样,也就是:对图像进行重新采样,它将把所有输入像素都考虑到比例- -whatever。

要做到这一点,我们必须为每个输入像素计算其对一个、两个或四个目标像素的贡献,这取决于输入像素的缩放投影是在目标像素内部,还是与X边界、Y边界重叠,或者两者都有。

(一个方案在这里会很好,但我没有。)

这是一个画布比例和我的像素完美比例的例子,比例是僵尸的1/3。

请注意,图片可能会在您的浏览器中进行缩放,并且是S.O.的.jpegized。

然而,我们发现袋熊的噪音要小得多,特别是在袋熊后面的草地上,以及它右边的树枝上。毛皮中的杂音使其更具对比性,但看起来他有白色的头发-unlike源图片。

右边的图片不那么吸引人,但绝对更好。

下面是完成像素完美缩小的代码:

小提琴结果:http://jsfiddle.net/gamealchemist/r6aVp/embedded/result/

自欺欺人:http://jsfiddle.net/gamealchemist/r6aVp/

代码语言:javascript
复制
// 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 / putImageDate付费,这也是相当慢的。

但在这种情况下,没有办法比处理每个源值更快,而且情况也不是那么糟糕:对于我的740 * 556袋熊图像,处理时间在30到40毫秒之间。

票数 185
EN

Stack Overflow用户

发布于 2013-10-07 19:13:14

画布重采样速度快,质量好:http://jsfiddle.net/9g9Nv/442/

更新: 2.0版(更快,web workers +可转移对象)- https://github.com/viliusle/Hermite-resize

代码语言:javascript
复制
/**
 * 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);
}
票数 60
EN

Stack Overflow用户

发布于 2013-10-08 06:20:05

建议1-延长工艺管线

你可以像我在你提到的链接中描述的那样使用step-down,但是你似乎用错了方法。

将图像缩放到1:2以上(通常但不限于)时,不需要降级。这是您需要进行大幅缩小的地方,您需要根据图像的内容将其分成两个(很少是更多)步骤(特别是在出现高频(如细线)的地方)。

每次对图像进行下采样时,都会丢失细节和信息。你不能期望得到的图像和原始图像一样清晰。

如果您随后在许多步骤中缩小图像,您将丢失大量信息,并且结果将很差,正如您已经注意到的那样。

试着多走一步,或者最多走两步。

卷积

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

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

下面是添加锐化滤镜的代码(它基于一个通用的卷积滤镜--我把锐化的权重矩阵放在里面,还有一个混合因子来调整效果的发音):

使用:

代码语言:javascript
复制
sharpen(context, width, height, mixFactor);

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

函数(基于this snippet):

代码语言:javascript
复制
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);
}

使用此组合的结果将是:

根据你想要添加到混合中的锐化程度,你可以得到默认的“模糊”到非常锐化的结果:

建议2-低级算法实现

如果你想在质量方面获得最好的结果,你需要进入低级,并考虑实现这个全新的算法来做到这一点。

参见IEEE的Interpolation-Dependent Image Downsampling (2011)。

Here is a link to the paper in full (PDF)

目前在JavaScript的AFAIK中还没有这个算法的实现,所以如果你想全身心地投入到这项任务中,你就会手忙脚乱。

其实质是(摘自论文):

抽象

针对低码率图像编码,提出了一种面向插值的自适应下采样算法

。给定一幅图像,所提出的算法能够获得低分辨率的图像,从中可以插值出与输入图像具有相同分辨率的高质量图像。与传统的下采样算法独立于插值过程不同,本文提出的下采样算法将下采样与插值过程联系起来。因此,所提出的下采样算法能够最大程度地保持输入图像的原始信息。然后将下采样的图像送入JPEG。然后,将基于全变分(TV)的后处理应用于解压缩的低分辨率图像。最终,对处理后的图像进行插值以保持输入图像的原始分辨率。实验结果表明,利用该算法得到的下采样图像,可以获得更高质量的插值图像。此外,对于低比特率的图像编码,所提出的算法能够获得比JPEG更好的性能。

(有关所有详细信息、公式等,请参阅提供的链接)

票数 30
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/18922880

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档