HTML5 Canvasresize(缩小)图像高质量?

我使用html5 canvas元素来调整图片在我的浏览器。 事实certificate,质量是非常低的。 我发现这个: 缩放一个<canvas>时禁用插值,但它无助于提高质量。

下面是我的css和js代码以及用Photoshop调出的图像,并在canvasAPI中缩放。

在浏览器中缩放图像时,为了获得最佳质量,我需要做些什么?

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

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:

在这里输入图像描述

在canvas上调整的图像:

在这里输入图像描述

编辑:

我试图通过以下几个步骤来缩小比例:

在HTML5canvas和Html5canvas中 调整图像的大小 drawImage:如何应用抗锯齿

这是我用过的function:

 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步缩小的尺寸,结果如下:

在这里输入图像描述

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

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

编辑2013-10-04:我尝试了GameAlchemist的algorithm。 这是与Photoshop相比的结果。

PhotoShop Image:

PhotoShop图片

GameAlchemist的algorithm:

GameAlchemist算法

由于您的问题是缩小图像的大小,因此插入内容(关于创build像素)没有意义。 这里的问题是降采样。

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

由于性能方面的原因,浏览器做了一个非常简单的下采样:构build较小的图像,他们将只select一个像素在源中,并使用其值的目的地。 忘记了一些细节,增加了噪音。

但是有一个例外:由于2X图像下采样的计算非常简单(平均4个像素来制作),并且用于视网膜/ HiDPI像素,所以这种情况得到了正确处理 – 浏览器确实使用了4个像素一-。

但是…如果你多次使用2倍下采样,你将面临连续舍入误差会增加太多噪声的问题。
更糟糕的是,你不会总是通过两个幂来resize,并调整到最近的功率+最后一次resize非常嘈杂。

您所寻求的是像素完美的下采样,即:对图像进行重新采样,将所有input像素都考虑在内,而不pipe其大小。
为此,对于每个input像素,必须计算其对一个,两个或四个目标像素的贡献,这取决于input像素的缩放投影恰好在目标像素内,与X边界,Y边界或两者重叠。
(一个计划在这里会很好,但我没有。)

下面是一个范例,canvas比例与我的像素完美的比例,在1/3的比例上。

请注意,图片可能会在您的浏览器中缩放,并被SO.jpegized。
但是我们看到,尤其是在袋熊后面的草地和其右边的分支上,噪音要less得多。 毛皮上的噪音使得它更加对比,但是看起来他有白色的头发 – 像源图片。
正确的形象是不太吸引人,但明确更好。

在这里输入图像描述

以下是完成像素完美缩放的代码:

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

这是相当贪婪的内存,因为需要使用浮点缓冲来存储目标图像的中间值( – >如果我们计算结果canvas,我们使用该algorithm中源图像内存的6倍)。
这也是相当昂贵的,因为每个源像素都使用任何目的地的大小,我们必须支付的getImageData / putImageDate,也很慢。
但是在这种情况下没有办法比处理每个源值更快,并且情况并不是那么糟糕:对于我的740 * 556图像,处理需要30到40毫秒。

快速canvas重新取样质量好: http : //jsfiddle.net/9g9Nv/442/

更新:版本2.0(更快,networking工作者+可转移的对象) – 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); } 

build议1 – 扩大stream程pipe道

您可以按照您所引用的链接描述的方式使用降级,但似乎以错误的方式使用它们。

将图像缩放至1:2以上的比例(通常但不限于),不需要降级。 这是您需要做大幅度缩放的地方,您需要根据图像的内容(尤其是出现细线等高频的地方)将其分成两步(甚至更less)。

每当你下载一张图片,你就会失去细节和信息。 你不能指望得到的图像像原来一样清晰。

如果你在很多步骤中缩小了图像,那么你将会丢失大量的信息,而且结果会很差,因为你已经注意到了。

尝试一个额外的步骤,或在上面两个。

卷积

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

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

这里是添加锐化filter的代码(它是基于一个通用的卷积filter – 我把它的权重matrix锐化,以及一个混合因子来调整发音的效果):

用法:

 sharpen(context, width, height, mixFactor); 

mixFactor是一个介于[ mixFactor ]之间的值,可以让您淡化锐化效果 – 经验法则:尺寸越小,效果越less。

function (基于此片段 ):

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

使用这种组合的结果将是:

在线演示此处

结果降采样和锐化卷积

根据您要添加到混合的多less锐化,您可以从默认的“模糊”到非常锐利的结果:

锐化的变化

build议2 – 低层algorithm实现

如果你想要得到最好的结果质量明智的,你需要去低层次,并考虑实施例如这个全新的algorithm来做到这一点。

参见IEEE的插值相关图像下采样 (2011)。
这里是全文链接(PDF) 。

目前在JavaScript AFAIK中没有这个algorithm的实现,所以如果你想把自己完成这个任务的话,那么你需要手动完成。

其实质是(摘自论文):

抽象

针对低比特率图像编码,提出了一种面向插值的自适应下采样algorithm。 给定一个图像,该algorithm能够获得一个低分辨率的图像,从中可以内插与input图像具有相同分辨率的高质量图像。 与传统的下采样algorithm不同,它与插值过程无关,所提出的下采样algorithm将下采样转化为插值过程。 因此,所提出的下采样algorithm能够最大限度地保持input图像的原始信息。 下采样的图像然后被馈送到JPEG。 然后将基于总变差(TV)的后处理应用于解压缩的低分辨率图像。 最终,处理的图像被内插以保持input图像的原始分辨率。 实验结果validation了利用所提出algorithm的下采样图像,可以获得更高质量的插值图像。 此外,所提出的algorithm在低比特率图像编码方面能够实现比JPEG更好的性能。

从纸张快照

(请参阅提供的链接的所有细节,公式等)

为什么使用canvas来调整图像大小? 现代浏览器都使用双三次插值 – 与Photoshop使用相同的过程(如果你正确的话) – 它们比canvas过程更快。 只需指定您想要的图像大小(只使用一个维度,高度或宽度,按比例resize)。

这是由大多数浏览器,包括更高版本的IE支持。 早期版本可能需要特定于浏览器的CSS 。

一个简单的函数(使用jQuery)来调整图像的大小是这样的:

 function resizeImage(img, percentage) { var coeff = percentage/100, width = $(img).width(), height = $(img).height(); return {"width": width*coeff, "height": height*coeff} } 

编辑更改image img以匹配函数参数。 ^)^

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

显然你可以做出不同的改进,但是这样做可以完成工作。

附录

将以下代码粘贴到此页面的控制台中,并观察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"; }); 

如果您只想使用canvas,最好的结果将是多个下降步骤。 但是这还不够好。 为了更好的质量,你需要纯粹的js实现。 我们刚刚发布了asynchronous – 高速下变换器,具有不同的质量/速度。 简而言之,它resize为1280 * 1024px〜0.1s,1x5000x3000px,最高质量(lanczosfilter,带3个波瓣)。 Pica有演示 ,在这里你可以玩你的图像,质量水平,甚至在移动设备上试用。

非洲裔美国人还没有不太清晰的面具,但很快就会join。 比实现resize的高速卷积滤波器容易得多。

这是一个可重用的Angular服务,用于高质量的图像/canvas大小调整: 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 }) }) 

我find了一个解决scheme,不需要直接访问像素数据,并通过循环来执行下采样。 根据图像的大小,这可能是非常耗费资源的,最好使用浏览器的内部algorithm。

drawImage()函数使用线性内插最近邻居重采样方法。 如果您不调整原来尺寸的一半以上,那么效果会很好

如果您只循环调整最大值的一半,结果将会非常好,并且比访问像素数据要快得多。

此function一次下调一半,直到达到所需的大小:

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

积分到这个职位

这是改进的Hermite调整filter,利用1名工人,使窗户不冻结。

https://github.com/calvintwr/Hermite-resize

对于真正需要调整图像本身大小的人来说,这不是正确的答案, 而只是为了缩小文件大小

我有一个“直接从相机”图片的问题,我的客户经常上传“无压缩”的JPEG。

不太知名的是,canvas支持(在大多数浏览器2017)来改变JPEG的质量

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

有了这个技巧,我可以减less4k x 3k图片> 10Mb到1或2Mb,这取决于您的需求。

看这里

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>