Why is putImageData so slow?
我正在使用相对较大的Canvas,其中包含各种(复杂的)东西。然后,我想保存"画布"的状态,以便稍后可以将其快速重置为现在的状态。我为此使用getImageData并将数据存储在变量中。然后,我在画布上绘制更多内容,稍后将使用putImageData将Canvas重置为保存状态时的状态。
但是,事实证明,putImageData非常慢。实际上,它比简单地从头重新绘制整个Canvas慢,后者涉及多个drawImage覆盖大部分表面,并且超过40.000 lineTo操作,然后进行笔触和填充。
从头开始重新绘制大约2000 x 5000像素的画布大约需要170毫秒,使用putImageData可能需要240毫秒。与重新绘制画布相比,为什么putImageData这么慢,尽管重新绘制画布包括用drawImage填充几乎整个画布,然后使用lineTo,stroke和fill再次用多边形填充大约50%的画布。因此,基本上,每个重画像素ist在重绘时都至少触摸一次。
因为drawImage似乎比putImageData快得多(毕竟,重新绘制画布的drawImage部分不到30毫秒)。我决定尝试不使用getImageData而是使用canvas.toDataURL保存画布的状态,然后从数据URL创建一个图像,将其粘贴到drawImage中以将其绘制到画布上。事实证明,整个过程要快得多,大约只需35毫秒即可完成。
那么为什么putImageData比其他方法(使用getDataURL或只是重绘)要慢得多?我如何才能进一步加快速度?是否存在,如果可以,通常什么是存储画布状态的最佳方法?
(所有数字均使用Firefox内部的Firebug进行测量)
- 如果您可以在某个地方在线发布问题演示,那将很有趣。在noVNC(github.com/kanaka/noVNC)中,我将putImageData用于许多中小型图像数据数组,而putImageData则看不到性能问题。也许您遇到了一个特殊的性能问题,应该对此进行调试。
-
您可以在此处查看danielbaulig.de/A3O如果已关闭Firebug控制台,它将无法100%运行,因此请确保将其打开。签出的版本是使用putImageData的版本。您可以通过单击任何"平铺"来触发它。它将使用putImageData刷新缓冲画布,然后"突出显示"选择的图块。在a3o_oo.js中,注释掉了几行,可用于在使用putImageData(当前),使用getDataURL(提及this.boardBuffer的两行)和缓冲画布的普通重绘(drawBoard行)之间进行切换。
-
伟大的问题和伟大的解决方案。但是,您是否找到了putImageData与drawImage相比如此慢的真正原因?
-
@cherouvim不,不是真的。我的假设是,主要原因是ImageData结构不再在硬件加速的图形结构中进行管理,因此调用getImageData / putImageData必须与这些对象进行转换,这很慢,涉及复制大量数据,实例化对象,等等,而使用drawImage只是将一个已经存在的纹理/绘图上下文绘制到屏幕上-使用现代硬件,这是非常快的。
仅此做一个最好的方法的小更新。我实际上写了关于高性能ECMAScript和HTML5 Canvas的学士论文(pdf,德语),因此,到目前为止,我已经在该主题上积累了一些专业知识。显然最好的解决方案是使用多个画布元素。从一个画布上绘制到另一块画布上的速度与在画布上绘制任意图像一样快。因此,"存储"画布的状态与稍后使用两个画布元素再次恢复它的速度一样快。
这个jsPerf测试用例非常清楚地展示了各种方法以及它们的优缺点。
为了完整起见,这是您真正应该如何做:
1 2 3 4 5 6 7 8 9 10 11
| // setup
var buffer = document.createElement('canvas');
buffer.width = canvas.width;
buffer.height = canvas.height;
// save
buffer.getContext('2d').drawImage(canvas, 0, 0);
// restore
canvas.getContext('2d').drawImage(buffer, 0, 0); |
根据浏览器的不同,此解决方案的速度比获得支持的解决方案快5000倍。
-
如果您需要在数组中存储很多状态怎么办?一个人应该创建一堆画布吗?例如var numBuffers = 20; var tmpCan = document.createElement('canvas'); var buffers = [tmpCan]; for (var i = 1, len = numBuffers, i < numBuffers; i++) { buffers.push(tmpCan.cloneNode()); }或类似的东西?还是有更好的解决方案?
在Firefox 3.6.8中,我能够通过改用toDataUrl / drawImage来解决putImageData的慢度问题。对我来说,它的运行速度足够快,我可以在处理mousemove事件时调用它:
要保存:
1 2
| savedImage = new Image()
savedImage.src = canvas.toDataURL("image/png") |
要还原的:
1 2
| ctx = canvas.getContext('2d')
ctx.drawImage(savedImage,0,0) |
-
刚才已确认您的回应。实际上,我目前以完全相同的方式进行操作:)另外,我目前正在尝试使用其他隐藏画布作为缓冲区。这样可以提高创建缓冲区的性能,使用toDataURL的速度相当慢,并且绘制速度应保持大致相同(因为drawImage也可以将canvas元素用作图像)。
-
刚刚意识到,这个答案一直在不断增加。我很欣赏这个答案,但是说明的解决方案实际上是糟糕的性能。请自己参考接受的答案。
-
我正在尝试从存储在Uint8Clamped数组中的像素数据绘制画布,其性能优于putImageData。这似乎是实现该目标的一种有前途的方法。我应该能够将Uint8Clamped数组转换为DataURL,使用该DataURL填充Image对象,然后将该图像绘制到画布上。我还没有测试过,但是我希望它会比putImageData更快。
首先,您说您正在使用Firebug进行测量。实际上,我发现Firebug大大降低了JS的执行速度,因此您可能无法获得良好的性能指标。
对于putImageData,我怀疑是因为函数采用了包含许多Number对象的大型JS数组,所有这些对象都必须检查范围(0..255)并复制到本机画布缓冲区中。
也许一旦WebGL ByteArray类型可用,这种事情就可以变得更快。
base64解码和解压缩数据(使用PNG数据URL)更快似乎很奇怪,但是只用一个JS字符串调用一个JS函数,因此它主要使用本机代码和类型。
- 由于我的数字大部分来自本机代码执行,因此我怀疑Firebug是否会对它们产生重大影响。尽管如此,我们不是在谈论毫秒的分数,而是实际上但实际上是对于基本上单个函数调用(putImageData)的四分之一秒。由于JS数组,性能可能很差。我将通过测试JS在putImageData之外可以处理(复制,操作等)数组的速度来检查这一点。
-
继续:在恢复画布状态时,不会发生除灭,解压缩等操作,而是在保存时进行。因此,这种情况仅发生一次,而我实际上并没有对其进行衡量,因为保存状态所花费的时间并不在乎。关键部分是恢复画布状态。到那时,我已经创建了Image对象。因此,如果Image对象将其数据包含在本机缓冲区中,则确实可能是问题的原因(或者更好的是drawImage方法缺少它)。
-
我知道putImageData的性能还可以(在480x320缓冲区下为80-100fps)-但是您要处理的是非常大的图像!
-
啊,1代表提到WebGL ByteArray。我一直在寻找有关Javascript二进制数组的信息,您的评论帮助我找到了它。这是当前的讨论:listware.net/201009/w3c-public-webapps/…。这是建议的标准:cvs.khronos.org/svn/repos/registry/trunk/public/webgl/doc/sp??ec / ...
大概是因为现代浏览器对<canvas>元素使用硬件加速,并且getImageData() / putImageData()要求将图像数据从图形卡传输到主机,反之亦然。众所周知,这太慢了。
使用两个画布会更快,因为所有数据都保留在图形卡上。