简体   繁体   English

HTML5 Canvas Resize (Downscale) Image 高质量?

[英]HTML5 Canvas Resize (Downscale) Image High Quality?

I use html5 canvas elements to resize images im my browser.我使用 html5 canvas 元素在浏览器中调整图像大小。 It turns out that the quality is very low.事实证明,质量非常低。 I found this: Disable Interpolation when Scaling a <canvas> but it does not help to increase the quality.我发现了这一点: 缩放 <canvas> 时禁用插值,但这无助于提高质量。

Below is my css and js code as well as the image scalled with Photoshop and scaled in the canvas API.下面是我的 css 和 js 代码以及用 Photoshop 调用并在画布 API 中缩放的图像。

What do I have to do to get optimal quality when scaling an image in the browser?在浏览器中缩放图像时,我该怎么做才能获得最佳质量?

Note: I want to scale down a large image to a small one, modify color in a canvas and send the result from the canvas to the server.注意:我想将大图像缩小为小图像,修改画布中的颜色并将结果从画布发送到服务器。

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

The image resized with photoshop:使用photoshop调整图像大小:

在此处输入图片说明

The image resized on canvas:图像在画布上调整大小:

在此处输入图片说明

Edit:编辑:

I tried to make downscaling in more than one steps as proposed in:我尝试按照以下建议的多个步骤进行缩减:

Resizing an image in an HTML5 canvas and Html5 canvas drawImage: how to apply antialiasing 在 HTML5 画布Html5 画布 drawImage 中调整图像大小:如何应用抗锯齿

This is the function I have used:这是我使用过的功能:

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


}

Here is the result if I use a 2 step down sizing:如果我使用 2 步缩小尺寸,结果如下:

在此处输入图片说明

Here is the result if I use a 3 step down sizing:如果我使用 3 步缩小尺寸,结果如下:

在此处输入图片说明

Here is the result if I use a 4 step down sizing:如果我使用 4 步缩小尺寸,结果如下:

在此处输入图片说明

Here is the result if I use a 20 step down sizing:如果我使用 20 步缩小尺寸,结果如下:

在此处输入图片说明

Note: It turns out that from 1 step to 2 steps there is a large improvement in image quality but the more steps you add to the process the more fuzzy the image becomes.注意:事实证明,从 1 步到 2 步,图像质量有很大提高,但添加到过程中的步骤越多,图像变得越模糊。

Is there a way to solve the problem that the image gets more fuzzy the more steps you add?有没有办法解决添加的步骤越多图像越模糊的问题?

Edit 2013-10-04: I tried the algorithm of GameAlchemist.编辑 2013-10-04:我尝试了 GameAlchemist 的算法。 Here is the result compared to Photoshop.这是与 Photoshop 相比的结果。

PhotoShop Image:图片:

照相馆图片

GameAlchemist's Algorithm:游戏炼金术士的算法:

游戏炼金术士算法

Since your problem is to downscale your image, there is no point in talking about interpolation -which is about creating pixel-.由于您的问题是缩小图像的尺寸,因此谈论插值(即创建像素)毫无意义。 The issue here is downsampling.这里的问题是下采样。

To downsample an image, we need to turn each square of p * p pixels in the original image into a single pixel in the destination image.要对图像进行下采样,我们需要将原始图像中每个 p * p 像素的正方形转换为目标图像中的单个像素。

For performances reasons Browsers do a very simple downsampling : to build the smaller image, they will just pick ONE pixel in the source and use its value for the destination.出于性能原因,浏览器做了一个非常简单的下采样:为了构建更小的图像,他们将只在源中选取一个像素并将其值用于目标。 which 'forgets' some details and adds noise.它“忘记”了一些细节并增加了噪音。

Yet there's an exception to that : since the 2X image downsampling is very simple to compute (average 4 pixels to make one) and is used for retina/HiDPI pixels, this case is handled properly -the Browser does make use of 4 pixels to make one-.然而,有一个例外:由于 2X 图像下采样非常容易计算(平均 4 个像素来制作一个)并且用于视网膜/HiDPI 像素,这种情况得到了正确处理 - 浏览器确实使用了 4 个像素来制作一-。

BUT... if you use several time a 2X downsampling, you'll face the issue that the successive rounding errors will add too much noise.但是...如果您多次使用 2X 下采样,您将面临连续舍入误差会增加太多噪音的问题。
What's worse, you won't always resize by a power of two, and resizing to the nearest power + a last resizing is very noisy.更糟糕的是,您不会总是以 2 的幂调整大小,并且调整到最接近的幂 + 最后一次调整大小非常嘈杂。

What you seek is a pixel-perfect downsampling, that is : a re-sampling of the image that will take all input pixels into account -whatever the scale-.您寻求的是像素完美的下采样,即:对图像进行重新采样,将所有输入像素都考虑在内 - 无论比例如何 -。
To do that we must compute, for each input pixel, its contribution to one, two, or four destination pixels depending wether the scaled projection of the input pixels is right inside a destination pixels, overlaps an X border, an Y border, or both.为此,我们必须为每个输入像素计算其对一个、两个或四个目标像素的贡献,具体取决于输入像素的缩放投影是否正好位于目标像素内部、与 X 边界、Y 边界重叠,或两者兼而有之.
( A scheme would be nice here, but i don't have one. ) (这里有一个计划会很好,但我没有。)

Here's an example of canvas scale vs my pixel perfect scale on a 1/3 scale of a zombat.这是画布比例与我的像素完美比例在 zombat 的 1/3 比例上的示例。

Notice that the picture might get scaled in your Browser, and is .jpegized by SO.请注意,图片可能会在您的浏览器中缩放,并由 SO 进行 .jpegized。
Yet we see that there's much less noise especially in the grass behind the wombat, and the branches on its right.然而,我们看到噪音要小得多,尤其是在袋熊身后的草地和它右边的树枝上。 The noise in the fur makes it more contrasted, but it looks like he's got white hairs -unlike source picture-.皮毛中的噪点使其对比更加鲜明,但看起来他有白发——与源图片不同——。
Right image is less catchy but definitively nicer.正确的图像不那么吸引人,但绝对更好。

在此处输入图片说明

Here's the code to do the pixel perfect downscaling :这是进行像素完美缩小的代码:

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

It is quite memory greedy, since a float buffer is required to store the intermediate values of the destination image (-> if we count the result canvas, we use 6 times the source image's memory in this algorithm).这是相当内存贪婪的,因为需要一个浮点缓冲区来存储目标图像的中间值(-> 如果我们计算结果画布,我们在此算法中使用源图像内存的 6 倍)。
It is also quite expensive, since each source pixel is used whatever the destination size, and we have to pay for the getImageData / putImageDate, quite slow also.它也非常昂贵,因为无论目标大小如何,都会使用每个源像素,而且我们必须为 getImageData / putImageDate 付费,而且速度也很慢。
But there's no way to be faster than process each source value in this case, and situation is not that bad : For my 740 * 556 image of a wombat, processing takes between 30 and 40 ms.但是在这种情况下,没有比处理每个源值更快的方法,而且情况也不错:对于我的 740 * 556 袋熊图像,处理需要 30 到 40 毫秒。

Fast canvas resample with good quality: http://jsfiddle.net/9g9Nv/442/质量好的快速画布重采样: http : //jsfiddle.net/9g9Nv/442/

Update: version 2.0 (faster, web workers + transferable objects) - https://github.com/viliusle/Hermite-resize更新: 2.0 版(更快,网络工作者 + 可转移对象) - 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);
}

Suggestion 1 - extend the process pipe-line建议 1 - 延长工艺管线

You can use step-down as I describe in the links you refer to but you appear to use them in a wrong way.您可以使用我在您引用的链接中描述的降压,但您似乎以错误的方式使用它们。

Step down is not needed to scale images to ratios above 1:2 (typically, but not limited to).不需要降级将图像缩放到 1:2 以上的比率(通常但不限于)。 It is where you need to do a drastic down-scaling you need to split it up in two (and rarely, more) steps depending on content of the image (in particular where high-frequencies such as thin lines occur).这是您需要进行大幅缩小的地方,您需要根据图像的内容将其分成两个(很少,更多)步骤(特别是在出现细线等高频的情况下)。

Every time you down-sample an image you will loose details and information.每次对图像进行下采样时,都会丢失细节和信息。 You cannot expect the resulting image to be as clear as the original.您不能期望生成的图像与原始图像一样清晰。

If you are then scaling down the images in many steps you will loose a lot of information in total and the result will be poor as you already noticed.如果您随后在多个步骤中按比例缩小图像,您总共会丢失大量信息,并且结果会很差,正如您已经注意到的那样。

Try with just one extra step, or at tops two.尝试只做一个额外的步骤,或者最多两步。

Convolutions卷积

In case of Photoshop notice that it applies a convolution after the image has been re-sampled, such as sharpen.如果是 Photoshop,请注意它在图像重新采样后应用卷积,例如锐化。 It's not just bi-cubic interpolation that takes place so in order to fully emulate Photoshop we need to also add the steps Photoshop is doing (with the default setup).这不仅仅是发生双三次插值,因此为了完全模拟 Photoshop,我们还需要添加 Photoshop 正在执行的步骤(使用默认设置)。

For this example I will use my original answer that you refer to in your post, but I have added a sharpen convolution to it to improve quality as a post process (see demo at bottom).对于此示例,我将使用您在帖子中引用的原始答案,但我已为其添加了锐化卷积以提高后期处理质量(请参阅底部的演示)。

Here is code for adding sharpen filter (it's based on a generic convolution filter - I put the weight matrix for sharpen inside it as well as a mix factor to adjust the pronunciation of the effect):这是添加锐化过滤器的代码(它基于通用卷积过滤器 - 我将锐化的权重矩阵放在其中,以及用于调整效果发音的混合因子):

Usage:用法:

sharpen(context, width, height, mixFactor);

The mixFactor is a value between [0.0, 1.0] and allow you do downplay the sharpen effect - rule-of-thumb: the less size the less of the effect is needed. mixFactor是 [0.0, 1.0] 之间的值,允许您淡化锐化效果 - 经验法则:尺寸越小,需要的效果越少。

Function (based on 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);
}

The result of using this combination will be:使用这种组合的结果将是:

ONLINE DEMO HERE在线演示在这里

结果下采样和锐化卷积

Depending on how much of the sharpening you want to add to the blend you can get result from default "blurry" to very sharp:根据您想要添加到混合中的锐化程度,您可以获得从默认“模糊”到非常清晰的结果:

锐化的变化

Suggestion 2 - low level algorithm implementation建议 2 - 低级算法实现

If you want to get the best result quality-wise you'll need to go low-level and consider to implement for example this brand new algorithm to do this.如果你想在质量方面获得最好的结果,你需要低级并考虑实现例如这个全新的算法来做到这一点。

See Interpolation-Dependent Image Downsampling (2011) from IEEE.请参阅 IEEE 的Interpolation-Dependent Image Downsampling (2011)。
Here is a link to the paper in full (PDF) . 这是该论文的全文链接 (PDF)

There are no implementations of this algorithm in JavaScript AFAIK of at this time so you're in for a hand-full if you want to throw yourself at this task.目前在 JavaScript AFAIK 中没有此算法的实现,因此如果您想全身心地投入到这项任务中,您将需要全力以赴。

The essence is (excerpts from the paper):实质是(论文节选):

Abstract抽象的

An interpolation oriented adaptive down-sampling algorithm is proposed for low bit-rate image coding in this paper.该文针对低码率图像编码提出了一种面向插值的自适应下采样算法。 Given an image, the proposed algorithm is able to obtain a low resolution image, from which a high quality image with the same resolution as the input image can be interpolated.给定图像,所提出的算法能够获得低分辨率图像,从中可以插入与输入图像具有相同分辨率的高质量图像。 Different from the traditional down-sampling algorithms, which are independent from the interpolation process, the proposed down-sampling algorithm hinges the down-sampling to the interpolation process.与独立于插值过程的传统下采样算法不同,所提出的下采样算法将下采样与插值过程结合起来。 Consequently, the proposed down-sampling algorithm is able to maintain the original information of the input image to the largest extent.因此,所提出的下采样算法能够最大程度地保持输入图像的原始信息。 The down-sampled image is then fed into JPEG.然后将下采样的图像输入 JPEG。 A total variation (TV) based post processing is then applied to the decompressed low resolution image.然后将基于总变化 (TV) 的后处理应用于解压缩的低分辨率图像。 Ultimately, the processed image is interpolated to maintain the original resolution of the input image.最终,对处理过的图像进行插值以保持输入图像的原始分辨率。 Experimental results verify that utilizing the downsampled image by the proposed algorithm, an interpolated image with much higher quality can be achieved.实验结果证明,利用所提出的算法下采样的图像,可以获得更高质量的插值图像。 Besides, the proposed algorithm is able to achieve superior performance than JPEG for low bit rate image coding.此外,所提出的算法在低比特率图像编码方面能够实现优于JPEG的性能。

纸上的快照

(see provided link for all details, formulas etc.) (有关所有详细信息、公式等,请参阅提供的链接)

If you wish to use canvas only, the best result will be with multiple downsteps.如果您只想使用画布,最好的结果将是多个步骤。 But that's not good enougth yet.但这还不够好。 For better quality you need pure js implementation.为了获得更好的质量,您需要纯 js 实现。 We just released pica - high speed downscaler with variable quality/speed.我们刚刚发布了pica - 具有可变质量/速度的高速降频器。 In short, it resizes 1280*1024px in ~0.1s, and 5000*3000px image in 1s, with highest quality (lanczos filter with 3 lobes).简而言之,它在大约 0.1 秒内调整 1280*1024 像素的大小,并在 1 秒内调整 5000*3000 像素的图像,并具有最高质量(具有 3 个波瓣的 lanczos 过滤器)。 Pica has demo , where you can play with your images, quality levels, and even try it on mobile devices. Pica 有演示,您可以在其中使用您的图像、质量级别,甚至可以在移动设备上试用。

Pica does not have unsharp mask yet, but that will be added very soon. Pica 还没有不清晰的蒙版,但很快就会添加。 That's much more easy than implement high speed convolution filter for resize.这比实现高速卷积滤波器来调整大小要容易得多。

Why use the canvas to resize images?为什么要使用画布来调整图像大小? Modern browsers all use bicubic interpolation — the same process used by Photoshop (if you're doing it right) — and they do it faster than the canvas process.现代浏览器都使用双三次插值——与 Photoshop 使用的过程相同(如果你做得对)——并且它们比画布过程更快。 Just specify the image size you want (use only one dimension, height or width, to resize proportionally).只需指定您想要的图像大小(仅使用一个维度、高度或宽度,按比例调整大小)。

This is supported by most browsers, including later versions of IE.大多数浏览器都支持这一点,包括更高版本的 IE。 Earlier versions may require browser-specific CSS .早期版本可能需要特定于浏览器的 CSS

A simple function (using jQuery) to resize an image would be like this:调整图像大小的简单函数(使用 jQuery)如下所示:

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

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

Then just use the returned value to resize the image in one or both dimensions.然后只需使用返回的值在一个或两个维度上调整图像大小。

Obviously there are different refinements you could make, but this gets the job done.显然,您可以进行不同的改进,但这可以完成工作。

Paste the following code into the console of this page and watch what happens to the gravatars:将以下代码粘贴到此页面的控制台中,然后观察 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";
});

Not the right answer for people who really need to resize the image itself, but just to shrink the file size .对于真正需要调整图像大小的人来说,这不是正确的答案,而只是缩小文件大小

I had a problem with "directly from the camera" pictures, that my customers often uploaded in "uncompressed" JPEG.我遇到了“直接来自相机”的图片问题,我的客户经常以“未压缩”的 JPEG 格式上传这些图片。

Not so well known is, that the canvas supports (in most browsers 2017) to change the quality of JPEG不太为人所知的是,画布支持(在大多数浏览器 2017 中)更改 JPEG 的质量

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

With this trick I could reduce 4k x 3k pics with >10Mb to 1 or 2Mb, sure it depends on your needs.使用这个技巧,我可以将大于 10Mb 的 4k x 3k 图片减少到 1 或 2Mb,当然这取决于您的需要。

look here 看这里

I found a solution that doesn't need to access directly the pixel data and loop through it to perform the downsampling.我找到了一个不需要直接访问像素数据并循环遍历它来执行下采样的解决方案。 Depending on the size of the image this can be very resource intensive, and it would be better to use the browser's internal algorithms.根据图像的大小,这可能会占用大量资源,最好使用浏览器的内部算法。

The drawImage() function is using a linear-interpolation, nearest-neighbor resampling method. drawImage()函数使用线性插值、最近邻重采样方法。 That works well when you are not resizing down more than half the original size .当您调整大小不超过原始大小的一半时,这很有效

If you loop to only resize max one half at a time, the results would be quite good, and much faster than accessing pixel data.如果循环一次最多只调整一半,结果会非常好,而且比访问像素数据快得多。

This function downsample to half at a time until reaching the desired size:此函数一次下采样一半,直到达到所需的大小:

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

Here is a reusable Angular service for high quality image / canvas resizing: https://gist.github.com/fisch0920/37bac5e741eaec60e983这是用于高质量图像/画布调整大小的可重用 Angular 服务: https : //gist.github.com/fisch0920/37bac5e741eaec60e983

The service supports lanczos convolution and step-wise downscaling.该服务支持 lanczos 卷积和逐步缩减。 The convolution approach is higher quality at the cost of being slower, whereas the step-wise downscaling approach produces reasonably antialiased results and is significantly faster.卷积方法以速度较慢为代价获得更高质量,而逐步缩小方法产生合理的抗锯齿结果并且速度明显更快。

Example usage:用法示例:

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

This is the improved Hermite resize filter that utilises 1 worker so that the window doesn't freeze.这是改进的 Hermite 调整大小过滤器,它使用 1 个工人,以便窗口不会冻结。

https://github.com/calvintwr/blitz-hermite-resize 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.
})

Maybe man you can try this, which is I always use in my project.In this way you can not only get high quality image ,but any other element on your canvas.也许你可以试试这个,这是我在我的项目中经常使用的。这样你不仅可以获得高质量的图像,还可以获得画布上的任何其他元素。

/* 
 * @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);
}

instead of .85 , if we add 1.0 .而不是.85 ,如果我们添加1.0 You will get exact answer.你会得到准确的答案。

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

You can get clear and bright image.您可以获得清晰明亮的图像。 Please check请检查

I really try to avoid running through image data, especially on larger images.我真的尽量避免遍历图像数据,尤其是在较大的图像上。 Thus I came up with a rather simple way to decently reduce image size without any restrictions or limitations using a few extra steps.因此,我想出了一种相当简单的方法来使用一些额外的步骤适当地减小图像大小而没有任何限制或限制。 This routine goes down to the lowest possible half step before the desired target size.该例程下降到所需目标大小之前可能的最低半步。 Then it scales it up to twice the target size and then half again.然后将其放大到目标大小的两倍,然后再放大一半。 Sounds funny at first, but the results are astoundingly good and go there swiftly.起初听起来很有趣,但结果非常好,而且很快就到了那里。

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

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>

DEMO : Resizing images with JS and HTML Canvas Demo fiddler.演示:使用 JS 和 HTML Canvas Demo fiddler 调整图像大小。

You may find 3 different methods to do this resize, that will help you understand how the code is working and why.您可能会找到 3 种不同的方法来调整大小,这将帮助您了解代码的工作方式以及原因。

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

Full code of both demo, and TypeScript method that you may want to use in your code, can be found in the GitHub project.可以在 GitHub 项目中找到演示的完整代码以及您可能希望在代码中使用的 TypeScript 方法。

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

This is the final code:这是最终的代码:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM