HTML5 Canvas调整大小(低级)图像高质量? [英] HTML5 Canvas Resize (Downscale) Image High Quality?

查看:4786
本文介绍了HTML5 Canvas调整大小(低级)图像高质量?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用html5 canvas元素在我的浏览器中调整图像大小。事实证明,质量非常低。我发现这一点:和


fiddle本身: http://jsfiddle.net/gamealchemist/r6aVp/

  //通过(float)scale缩放图像< 1 
//返回包含缩放图像的画布。
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);
}

//通过(float)scale缩放画布< 1
//返回包含缩放图像的新画布。
function downScaleCanvas(cv,scale){
if(!(scale< 1)||!(scale> 0))throw('scale must be a positive number< 1');
var sqScale = scale * scale; // square scale =目标像素的面积
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; //源x,y,源数组中的索引
var tx = 0,ty = 0,yIndex = 0,tIndex = 0; //目标数组中的目标x,y,x,y索引
var tX = 0,tY = 0; // round tx,ty
var w = 0,nw = 0,wx = 0,nwx = 0,wy = 0,nwy = 0; // weight / next weight x / y
// weight是目标中当前源点的权重。
//下一个权重是当前源点在下一个目标点内的权重。
var crossX = false; //是否缩放px跨越当前的px右边界?
var crossY = false; //是否缩放px跨越当前px底部边界?
var sBuffer = cv.getContext('2d')。
getImageData(0,0,sw,sh).data; //源缓冲区8位rgba
var tBuffer = new Float32Array(3 * tw * th); // target buffer Float32 rgb
var sR = 0,sG = 0,sB = 0; //源的当前点r,g,b
/ *未测试!
var sA = 0; // source alpha * /

for(sy = 0; sy ty = sy * scale; // y src target within target
tY = 0 | ty; // rounded:target pixel's y
yIndex = 3 * tY * tw; //目标数组中的行索引
crossY =(tY!=(0 | ty + scale));
if(crossY){// if pixel is crossing botton target pixel
wy =(tY + 1 - ty); //目标像素点的权重
nwy =(ty + scale - tY - 1); // ... in y + 1 target pixel
}
for(sx = 0; sx< sw; sx ++,sIndex + = 4){
tx = sx * scale; // x src在目标
中的位置tX = 0 | tx; // rounded:target pixel's x
tIndex = yIndex + tX * 3; // target pixel index in target array
crossX =(tX!=(0 | tx + scale));
if(crossX){//如果像素正在越过目标像素的右边
wx =(tX + 1 - tx); //目标像素点的权重
nwx =(tx + scale - tX - 1); // ... within x + 1 target pixels
}
sR = sBuffer [sIndex]; //检索r,g,b为curr src px。
sG = sBuffer [sIndex + 1];
sB = sBuffer [sIndex + 2];

/ *!未经测试:处理alpha!
sA = sBuffer [sIndex + 3];
if(!sA)continue;
if(sA!= 0xFF){
sR =(sR * sA)> 8; //或使用/ 256?
sG =(sG * sA)> 8;
sB =(sB * sA)> 8;
}
* /
if(!crossX&&!crossY){//像素不交叉
//只是添加用平方比例加权的分量。
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;
//为当前px添加加权组件
tBuffer [tIndex] + = sR * w;
tBuffer [tIndex + 1] + = sG * w;
tBuffer [tIndex + 2] + = sB * w;
//为下一个添加加权分量(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){//只在Y上交叉
w = wy * scale;
//为当前px添加加权组件
tBuffer [tIndex] + = sR * w;
tBuffer [tIndex + 1] + = sG * w;
tBuffer [tIndex + 2] + = sB * w;
//为下一个添加加权分量(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 {//交叉x和y:涉及四个目标点
//为当前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

//创建结果canvas
var resCV = document.createElement('帆布');
resCV.width = tw;
resCV.height = th;
var resCtx = resCV.getContext('2d');
var imgRes = resCtx.getImageData(0,0,tw,th);
var tByteBuffer = imgRes.data;
//将float32数组转换为UInt8Clamped数组
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;
}
//将结果写入画布。
resCtx.putImageData(imgRes,0,0);
return resCV;
}

这是相当内存贪婪,缓冲区需要存储目标映像的中间值( - >如果我们计算结果画布,我们在这个算法中使用6倍的源映像的内存)。

这也是相当昂贵的,因为每个源像素使用任何目标大小,我们必须支付getImageData / putImageDate,也很慢。

但是没有办法比处理每个源值在这种情况下,情况是不是那么糟糕:对于我的740 * 556的wombat图像,处理需要30到40毫秒。


I use html5 canvas elements to resize images im my browser. 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.

Below is my css and js code as well as the image scalled with Photoshop and scaled in the canvas 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:

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

The image resized with 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

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:

Here is the result if I use a 3 step down sizing:

Here is the result if I use a 4 step down sizing:

Here is the result if I use a 20 step down sizing:

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.

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. Here is the result compared to 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.
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 'forgots' 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-.
BUT... if you use several time a 2X downsampling, you'll face the issue that the successive rounding errors will add too much noise.
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.

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.
( 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. Notice that the picture might get scaled in your Browser, and is .jpegized by S.O..
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/
fiddle itself : 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).
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.
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.

这篇关于HTML5 Canvas调整大小(低级)图像高质量?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆