HTML5 Canvas 缩放(降低分辨率)图像高质量?
HTML5 Canvas 缩放(降低分辨率)图像高质量?
我在浏览器中使用html5画布元素来调整图像大小。结果发现图像质量非常低。我找到了这个链接:Disable Interpolation when Scaling a 但它并不能提高图像质量。
以下是我的CSS和JS代码,以及使用Photoshop缩放和使用canvas 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; }
JS:
var $img = $(''); var $originalCanvas = $('
使用Photoshop缩放的图像:
使用canvas缩放的图像:
编辑:
我尝试按照以下链接中建议的方法进行多步缩小操作:
Resizing an image in an HTML5 canvas和
Html5 canvas drawImage: how to apply antialiasing
我使用了以下函数:
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; // 使用最适合将图像放入maxWidth x maxHeight框的最小比例。 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); // 初始化 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); // 临时 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); // 复制回来 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 // 复制回画布 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的算法:
HTML5 Canvas 图片缩放(降低分辨率)的高质量问题是什么原因以及解决方法?
问题的原因是为了缩小图像,而不是进行插值(创建像素)的讨论。这里的问题是降采样。为了对图像进行降采样,我们需要将原始图像中的每个 p * p 像素的正方形转换为目标图像中的单个像素。
出于性能原因,浏览器进行了非常简单的降采样:为了构建较小的图像,它们只会在源中选择一个像素,并将其值用于目标,这会“遗忘”一些细节并添加噪音。
然而,有一个例外:由于2倍图像降采样非常简单(平均4个像素得到一个像素)并且用于视网膜/高DPI像素,因此这种情况得到了适当处理-浏览器确实使用了4个像素来生成一个像素。
但是...如果您多次使用2倍降采样,您将面临连续的舍入误差会增加太多噪音的问题。
更糟糕的是,您不总是按2的幂进行调整大小,并且调整为最近的幂加上最后一个调整会非常嘈杂。
您想要的是像素完美的降采样,即:图像的重新采样将考虑到所有输入像素-无论比例如何。
为了做到这一点,我们必须计算出每个输入像素对于一个、两个或四个目标像素的贡献,具体取决于缩放的输入像素的投影是否在目标像素的内部,是否重叠X边界,Y边界或两者都重叠。
这里是一个画布缩放和我的像素完美缩放的示例,图像是一只袋鼠的1/3缩放。
请注意,图片可能在您的浏览器中缩放,并且被操作系统.jpeg化。
然而,我们可以看到,在袋鼠后面的草地和右边的树枝上,噪音要少得多。毛发上的噪音使其更加对比鲜明,但看起来像是有白色的毛发-与原始图片不同。
右侧的图像没有那么吸引人,但绝对更好。
以下是执行像素完美缩小的代码:
// 将图像按(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; // 源像素在目标像素内的面积 var sw = cv.width; // 源图像宽度 var sh = cv.height; // 源图像高度 var tw = Math.floor(sw * scale); // 目标图像宽度 var th = Math.floor(sh * scale); // 目标图像高度 var sx = 0, sy = 0, sIndex = 0; // 源 x, y, 索引 var tx = 0, ty = 0, yIndex = 0, tIndex = 0; // 目标 x, y, 索引 var tX = 0, tY = 0; // 四舍五入的目标 x, y var w = 0, nw = 0, wx = 0, nwx = 0, wy = 0, nwy = 0; // 权重 / 下一个权重 x / y // 权重是当前源点在目标中的权重。 // 下一个权重是当前源点在下一个目标点中的权重。 var crossX = false; // 缩放像素是否跨越当前像素的右边界? var crossY = false; // 缩放像素是否跨越当前像素的底边界? var sBuffer = cv.getContext('2d').getImageData(0, 0, sw, sh).data; // 源缓冲区8位RGBA var tBuffer = new Float32Array(3 * tw * th); // 目标缓冲区Float32 RGB var sR = 0, sG = 0, sB = 0; // 源的当前点 r,g,b /* 未经测试! var sA = 0; // 源 alpha */ for (sy = 0; sy < sh; sy++) { ty = sy * scale; // y 在目标中的位置 tY = 0 | ty; // 四舍五入的:目标像素的 y yIndex = 3 * tY * tw; // 在目标数组中的行索引 crossY = (tY != (0 | ty + scale)); if (crossY) { // 如果像素越过目标像素的底部 wy = (tY + 1 - ty); // 在目标像素中的权重 nwy = (ty + scale - tY - 1); // 在下一个y+1目标像素中的权重 } for (sx = 0; sx < sw; sx++, sIndex += 4) { tx = sx * scale; // x 在目标中的位置 tX = 0 | tx; // 四舍五入的:目标像素的 x tIndex = yIndex + tX * 3; // 目标像素在目标数组中的索引 crossX = (tX != (0 | tx + scale)); if (crossX) { // 如果像素越过目标像素的右边界 wx = (tX + 1 - tx); // 在目标像素中的权重 nwx = (tx + scale - tX - 1); // 在下一个x+1目标像素中的权重 } sR = sBuffer[sIndex ]; // 获取当前源像素的 r,g,b 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) { // 仅越过X轴 w = wx * scale; // 添加当前像素的加权组件 tBuffer[tIndex ] += sR * w; tBuffer[tIndex + 1] += sG * w; tBuffer[tIndex + 2] += sB * w; // 添加下一个(tX+1)像素的加权组件 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; // 添加当前像素的加权组件 tBuffer[tIndex ] += sR * w; tBuffer[tIndex + 1] += sG * w; tBuffer[tIndex + 2] += sB * w; // 添加下一个(tY+1)像素的加权组件 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轴:涉及四个目标点 // 添加当前像素的加权组件 w = wx * wy; tBuffer[tIndex ] += sR * w; tBuffer[tIndex + 1] += sG * w; tBuffer[tIndex + 2] += sB * w; // 对于tX + 1; tY 像素 nw = nwx * wy; tBuffer[tIndex + 3] += sR * nw; tBuffer[tIndex + 4] += sG * nw; tBuffer[tIndex + 5] += sB * nw; // 对于tX ; tY + 1 像素 nw = wx * nwy; tBuffer[tIndex + 3 * tw ] += sR * nw; tBuffer[tIndex + 3 * tw + 1] += sG * nw; tBuffer[tIndex + 3 * tw + 2] += sB * nw; // 对于tX + 1 ; tY +1 像素 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 // 创建结果画布 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; // 将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倍)。这也非常昂贵,因为无论目标大小如何,每个源像素都会被使用,并且我们必须付出很多代价来获取和写入图像数据,这也是非常慢的。
但是,在这种情况下,没有比处理每个源值更快的方法,而且情况并不是那么糟糕:对于我一只740x556的袋鼠的图像,处理时间在30到40毫秒之间。
如果在放入画布之前缩放图像,速度会更快吗?
我不明白...似乎这就是我所做的。缓冲区以及我创建的画布(resCV)的大小与缩放图像的大小相同。我认为更快的唯一方法是使用类似于breshensam的整数计算。但是,对于视频游戏来说,40毫秒只是慢了一点(25帧每秒),对于绘图应用程序来说并不慢。
您是否认为在保持质量的同时加快算法的速度有机会吗?
我尝试通过使用0 | 来舍入缓冲区(算法的最后部分)而不是使用Math.ceil来使其更快。速度确实有所提高。但是无论如何,使用get/putImageData存在相当多的开销,并且我们无法避免处理每个像素。
在使用context.ImageSmoothingEnabled或不使用时是否会有区别?
不会,因为此设置是针对drawImage的。在这里,我只使用了get/putImageData,它们与“原始”图像处理有关。
这会生成高质量的缩小图像,但是对于具有不透明度/透明度的GIF/PNG存在问题。整个透明区域会变成黑色。有什么想法为什么会这样?
嗯,我根本没有处理不透明度,所以透明==黑色。我编辑了代码以处理alpha,但我没有时间测试它,所以我暂时将其注释掉。如果您进行了测试,请向我更新。
我创建了一个jsFiddle,其中取消了注释的新脚本的alpha部分。看起来不起作用,因为完全透明的部分仍然变成纯黑色。如果我对alpha使用256而不是8,我会在曲线上获得透明度,所以这似乎是一个更好的解决方案。
好的,感谢您提醒。如果您查看downScaleCanvas的代码末尾,每个像素都会获得255的alpha值,因此所有内容都是不透明的。我必须仔细考虑这个问题。快速修复方法:将#000视为透明,并在这种情况下将alpha设置为0。或者让tbuffer为rgb'c',其中c是非透明像素的计数(您可以猜到结尾)。真正的解决方案似乎是像r,g,b一样插值a,但我不清楚,例如,四分之一的alpha黄色如何与实心绿色混合:结果是固体还是不透明?我不认为我会很快看到这个问题的答案,所以我暂时在这里回复。
我尝试过这个,但是无法成功弄清楚。我能够使jsfiddle.net/kpQyE/2工作,但是只有在比例为0.50时才有效(其他比例会导致图像不好)。我对算法实际上是如何工作的没有很清楚的理解,所以我怀疑我自己能否单独弄清楚。如果您有额外的时间来考虑这个问题,请随时告诉我。谢谢!
好的,所以我看了一下代码:你离解决方案很近了。两个错误:tX+1的索引比(+3,+4,+5,+6)多了一个(+4,+5,+6,+7),并且在rgba中更改行是乘以4,而不是3。我只测试了4个随机值以进行检查(0.1, 0.15, 0.33, 0.8),看起来没问题。您更新的fiddle在这里:jsfiddle.net/gamealchemist/kpQyE/3
您可以更新已打印的解决方案吗?
好的!实际上,不同的fiddles不仅是改进,而且还是不同版本(是否处理透明度,是否期望目标大小或比例等...),所以我对此应该做些什么感到困惑...
有没有好的书籍或资源可以了解图像缩放和操作的算法?
我从非常多不同的来源进行了研究,所以很难说。我可以推荐《图形宝石》这本我非常喜欢的书...不知道,抱歉:-/
您是否看到了最新的回答?Enric说在某些情况下应该使用floor。这是真的吗?如果是,请更正您的答案。
我没有看到这个帖子,不,谢谢告诉我。我更新了帖子和fiddle:jsfiddle.net/gamealchemist/kpQyE/14
你在打印的解答里有一个错误,Enric说你应该在某些情况下使用floor。如果是这样,请更正你的回答。
我没有看到这个帖子,不,感谢告诉我。我更新了帖子和fiddle:jsfiddle.net/gamealchemist/kpQyE/14
它非常优秀,速度非常快,结果与Photoshop的结果非常接近。为了更容易使用,您可以使用一个自调用函数来包装它,并将downScaleCanvas和downScaleImage函数暴露给window对象。这也可以防止某人执行window.log2 = null;并破坏代码。
这会在某个数学计算过程中产生以下错误:SecurityError: The operation is insecure.
HTML5 Canvas Resize (Downscale) Image High Quality 的问题是为了实现高质量的图像缩小。解决方法是使用Hermite滤波器进行图像重采样。
问题的原因是在进行图像缩小时,常规的缩放算法会导致图像失真和质量下降。为了解决这个问题,可以使用Hermite滤波器来实现高质量的图像缩小。Hermite滤波器是一种基于插值的算法,可以在图像缩放时保持细节和色彩的准确性。
下面是一个使用Hermite滤波器进行图像重采样的示例代码:
function resample_single(canvas, width, height, resize_canvas) { // 省略代码... } // 使用示例 var canvas = document.getElementById("myCanvas"); var width = 500; var height = 300; resample_single(canvas, width, height, true);
通过调用`resample_single`函数,可以将指定的canvas元素按照指定的宽度和高度进行高质量的缩小。如果`resize_canvas`参数为`true`,则会同时调整canvas元素的大小。
此外,在评论中还提到了一些关于透明图像和旋转canvas的问题,以及一些相关的讨论和解决方法。
总之,通过使用Hermite滤波器进行图像重采样,可以实现高质量的图像缩小。这对于需要在HTML5 Canvas中处理图像的开发者来说是一个有用的工具。