con*_*ile 146 javascript css html5 canvas html5-canvas
我使用html5 canvas元素在浏览器中调整图像大小.事实证明,质量非常低.我发现:在缩放<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;
}
Run Code Online (Sandbox Code Playgroud)
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);
});
Run Code Online (Sandbox Code Playgroud)
使用photoshop调整图像大小:

图像在画布上调整大小:

编辑:
我尝试按照以下方案中提出的多个步骤进行缩减:
调整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);
}
Run Code Online (Sandbox Code Playgroud)
如果我使用两步缩小尺寸,结果如下:

如果我使用3步减小尺寸,结果如下:

如果我使用4步减小尺寸,结果如下:

如果我使用20步减小尺寸,结果如下:

注意:事实证明,从1步到2步,图像质量有了很大的提高,但是你添加到这个过程的步骤越多,图像就越模糊.
有没有办法解决图像越模糊的问题,你添加的步骤越多?
编辑2013-10-04:我尝试了GameAlchemist的算法.这是与Photoshop相比的结果.
PhotoShop图片:
GameAlchemist的算法:

Gam*_*ist 168
由于你的问题是缩小你的图像,所以谈论插值是没有意义的 - 这是关于创建像素.这里的问题是下采样.
要对图像进行下采样,我们需要将原始图像中每个p*p像素的平方转换为目标图像中的单个像素.
出于性能原因,浏览器进行非常简单的下采样:要构建较小的图像,它们只需在源中选择一个像素并将其值用于目标."忘记"一些细节并增加噪音.
然而有一个例外:由于2X图像下采样计算非常简单(平均4个像素制作一个)并用于视网膜/ HiDPI像素,这种情况得到妥善处理 - 浏览器确实利用了4个像素来制作一-.
但是......如果您使用多次2X下采样,您将面临连续舍入误差会增加太多噪音的问题.
更糟糕的是,你不会总是以2的幂来调整大小,并且调整到最接近的功率+最后一次调整大小非常嘈杂.
您所寻求的是像素完美的下采样,即:对图像进行重新采样,将所有输入像素考虑在内 - 无论尺度如何.
为此,我们必须为每个输入像素计算其对一个,两个或四个目标像素的贡献,这取决于输入像素的缩放投影是在目标像素内部,与X边界,Y边界或两者重叠.
(这里的计划很不错,但我没有.)
这是一个画布比例的示例与我在zombat的1/3比例上的像素完美比例.
请注意,图片可能会在您的浏览器中缩放,并且由.jpegized.
然而,我们看到噪音要小得多,特别是在袋熊身后的草地和右边的树枝上.毛皮中的噪音使它更加鲜明对比,但看起来他有白发 - 不像源图片.
正确的形象不那么吸引人,但确实更好.

这是完成像素完美缩减的代码:
小提琴结果: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;
}
Run Code Online (Sandbox Code Playgroud)
它非常内存贪婪,因为需要一个浮点缓冲区来存储目标图像的中间值( - >如果我们计算结果画布,我们在此算法中使用6倍的源图像内存).
它也非常昂贵,因为无论目标大小如何都使用每个源像素,我们必须为getImageData/putImageDate付费,也很慢.
但是在这种情况下,没有办法比处理每个源值更快,情况也不是那么糟糕:对于我的740*556袋熊图像,处理时间在30到40毫秒之间.
Vil*_*usL 47
快速帆布重新取样,质量好:http://jsfiddle.net/9g9Nv/442/
更新: 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);
}
Run Code Online (Sandbox Code Playgroud)
小智 28
您可以按照我在您引用的链接中描述的步骤使用降压,但您似乎以错误的方式使用它们.
不需要降低步骤以将图像缩放到高于1:2的比率(通常但不限于).您需要根据图像的内容(特别是在出现细线等高频的情况下)将其分成两个(并且很少,更多)步骤,从而需要进行大幅缩小.
每次下载图像时,都会丢失细节和信息.您不能指望生成的图像与原始图像一样清晰.
如果您在多个步骤中缩小图像,则总会丢失大量信息,结果会很差,因为您已经注意到了.
尝试一个额外的步骤,或在顶部两个.
如果Photoshop注意到它在重新采样图像后应用卷积,例如锐化.这不仅仅是双三次插值,因此为了完全模拟Photoshop,我们还需要添加Photoshop正在执行的步骤(使用默认设置).
对于这个例子,我将使用你在帖子中提到的原始答案,但我已经添加了一个锐化的卷积来提高它作为后期处理的质量(见底部的演示).
这里是添加锐化滤镜的代码(它基于通用的卷积滤镜 - 我将重量矩阵用于锐化内部以及调整效果发音的混合因子):
用法:
sharpen(context, width, height, mixFactor);
Run Code Online (Sandbox Code Playgroud)
这mixFactor是[0.0,1.0]之间的值,允许您淡化锐化效果 - 经验法则:尺寸越小,需要的效果越少.
功能(基于此片段):
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);
}
Run Code Online (Sandbox Code Playgroud)
使用此组合的结果将是:

根据您要添加到混合的锐化程度,您可以获得从默认"模糊"到非常清晰的结果:

如果你想在质量方面获得最好的结果,你需要进行低级别的考虑,并考虑实施这种全新的算法来实现这一目标.
参见IEEE的Interpolation-Dependent Image Downsampling(2011).
以下是该论文的完整链接(PDF).
目前在JavaScript AFAIK中没有这种算法的实现,所以如果你想要完成这项任务,那么你就可以获得一个完整的算法.
本质是(摘自论文):
抽象
针对低比特率图像编码,提出了一种面向插值的自适应下采样算法.给定图像,所提出的算法能够获得低分辨率图像,从该图像可以内插具有与输入图像相同分辨率的高质量图像.与传统的下采样算法不同,传统的下采样算法独立于插值过程,所提出的下采样算法将下采样与插值过程联系起来.因此,所提出的下采样算法能够最大程度地保持输入图像的原始信息.然后将下采样的图像馈送到JPEG.然后将基于总变差(TV)的后处理应用于解压缩的低分辨率图像.最终,实验结果验证了通过所提出的算法利用下采样图像,可以实现具有更高质量的内插图像.此外,所提出的算法能够在低比特率图像编码中实现优于JPEG的性能.

(有关所有细节,公式等,请参阅提供的链接)
Rob*_*sto 17
为什么要使用画布来调整图像大小?现代浏览器都使用双三次插值 - 与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}
}
Run Code Online (Sandbox Code Playgroud)
然后,只需使用返回的值在一个或两个维度中调整图像大小.
显然,您可以进行不同的改进,但这可以完成工作.
将以下代码粘贴到此页面的控制台中,并观察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";
});
Run Code Online (Sandbox Code Playgroud)
对于真正需要调整图像大小的人来说,这不是正确的答案,而只是缩小文件大小.
我的"直接来自相机"图片有问题,我的客户经常以"未压缩"JPEG格式上传.
不太知名的是,画布支持(在大多数浏览器2017中)改变JPEG的质量
data=canvas.toDataURL('image/jpeg', .85) # [1..0] default 0.92
Run Code Online (Sandbox Code Playgroud)
通过这个技巧,我可以减少4k x 3k图片,大于10Mb到1或2Mb,确定它取决于您的需求.
这是改进的 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.
})
Run Code Online (Sandbox Code Playgroud)
我找到了一个不需要直接访问像素数据并循环遍历它来执行下采样的解决方案。根据图像的大小,这可能会占用大量资源,最好使用浏览器的内部算法。
所述的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] );
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
137164 次 |
| 最近记录: |