我有一个<canvas>
,我每100毫秒更新一次来自HTTP请求的位图图像数据:
var ctx = canvas.getContext("2d");
setInterval(() => {
fetch('/get_image_data').then(r => r.arrayBuffer()).then(arr => {
var byteArray = new Uint8ClampedArray(arr);
var imgData = new ImageData(byteArray, 500, 500);
ctx.putImageData(imgData, 0, 0);
});
}, 100);
当/get_image_data
提供RGBA数据时,此操作有效。在我的例子中,因为alpha总是100%,所以我不通过网络发送A通道。问题:
- 当请求传递RGB二进制数据时,如何有效地执行此操作?
- 并且当请求递送灰度二进制数据时也是如此?
(Can我们避免了一个for
循环,这在Javascript中对于兆字节的数据可能会慢到每秒10次?)
灰度=> RGBA情况下的示例:每个输入值..., a, ...
应该在输出数组中被..., a, a, a, 255, ...
替换。
Here is a pure JS solution:对于1000 x1000 px灰度=> RGBA阵列转换,约为10 ms。
Here is an attempt的WASM解决方案。
4条答案
按热度按时间vhmi4jdf1#
将
ArrayBuffer
从RGB转换为RGBA在概念上很简单:只需在每个RGB三元组后拼接不透明Alpha通道字节(255
)。(灰度到RGBA也很简单:对于每个灰色字节:复制3次,然后插入255
。)这个问题(稍微)更有挑战性的部分是将工作卸载到另一个带有wasm或worker的线程。
由于您表示熟悉JavaScript,因此我将提供一个示例,说明如何使用几个实用程序模块在worker中完成它,并且我将展示的代码将使用TypeScript语法。
在示例中使用的类型上:它们非常弱(大量的
any
)-它们的出现只是为了提供关于示例中涉及的数据结构的结构清晰性。在强类型的worker应用程序代码中,需要根据每个环境(worker和host)中应用程序的具体情况重新编写类型,因为消息传递中涉及的所有类型都是约定的。面向任务的worker代码
您的问题中的问题是面向任务的(对于每个特定的二进制RGB数据序列,您需要其RGBA对应项)。在这种情况下,不方便的是,Worker API是面向消息的,而不是面向任务的-这意味着我们只提供了一个接口来监听和响应 * 每一条消息 *,而不管它的原因或上下文-没有内置的方法来关联一对特定的消息到和从一个工人。因此,第一步是在该API之上创建一个面向任务的抽象:
task-worker.ts
:我不想过多解释这段代码:它主要是在对象之间选择和移动属性,这样就可以避免应用程序代码中的所有样板文件。值得注意的是:它还抽象了为每个任务示例创建唯一ID的必要性。我将谈谈三个出口:
TaskWorker
:在主机中使用-它是示例化worker模块的抽象,并在其worker
属性上公开worker。它还有一个process
方法,该方法接受任务信息作为对象参数,并返回处理任务的结果的承诺。任务对象参数有三个属性:type
:要执行的任务类型(下面将详细介绍)。这仅仅是指向工作者中的任务处理功能的键。value
:将由关联任务函数操作的有效负载值transfer
:transferable objects的可选数组(稍后我将再次提出)registerTask
:在工作器中使用-将任务函数设置为其在字典中的关联类型名称,以便工作器可以在接收到该类型的任务时使用该函数来处理有效负载。handleTaskMessage
:用于工人-这很简单,但很重要:必须在worker模块脚本中将其分配给self.onmessage
。高效转换RGB(或灰度)到RGBA
第二个实用程序模块具有将alpha字节拼接到RGB数据中的逻辑,并且还有一个从灰度到RGBA的转换函数:
rgba-conversion.ts
:我认为这里的迭代数学代码是不言自明的(但是-如果这里或答案的其他部分使用的任何API不熟悉-MDN有解释性文档)。我认为值得注意的是,输入和输出值(
ArrayBuffer
)都是transferable objects,这意味着它们本质上可以在主机和工作上下文之间 * 移动 * 而不是 * 复制 *,以提高内存和速度效率。此外,感谢@Kaiido提供的信息,这些信息用于提高这种方法的效率,而不是在这个答案的早期版本中使用的技术。
创建worker
由于上面的抽象,实际的worker代码非常少:
worker.ts
:每个任务函数所需要的只是将缓冲区结果移动到返回对象中的
value
属性,并发出其底层内存可以转移到主机上下文的信号。示例应用代码
我不认为这里会有什么让你惊讶的:唯一的样板是mocking
fetch
以返回一个示例RGB缓冲区,因为您的问题中引用的服务器对以下代码不可用:main.ts
:这些TypeScript模块只需要编译,
main
脚本作为HTML中的模块脚本运行。如果不访问您的服务器数据,我无法做出性能声明,所以我将把它留给您。如果有什么我在解释中忽略的(或者任何仍然不清楚的),请随时在评论中提问。
b4wnujal2#
类型化数组视图。
可以使用类型化数组创建像素数据的视图。
例如,您有一个字节数组
const foo = new Uint8Array(size)
,您可以使用const foo32 = new Uint32Array(foo.buffer)
将视图创建为32位字数组foo32
是相同的数据,但JS将其视为32位字而不是字节,创建它是零复制操作,几乎没有开销。因此,您可以在一次操作中移动4个字节。
不幸的是,您仍然需要索引和格式化来自其中一个数组的字节数据(如灰度或RGB)。
但是,使用类型化数组视图仍然可以获得有价值的性能提升
移动灰度像素
移动灰度字节示例
要比
其中src和dest是指向源gray字节的
Uint8Array
和目标RGBA字节。移动RGB像素
要将RGB移动到RGBA,可以使用
这比如下移动字节快30%左右
hk8txs483#
关于您的主要问题:
For
循环...?“*我建议你尝试使用GPU处理你的像素在这个任务。
您可以从CPU
canvas.getContext("2d")
...使用canvas.getContext("webgl")
转换为GPU将
<canvas>
设置为WebGL(GPU)模式意味着它现在可以接受更多格式的像素数据,包括RGB甚至LUMINANCE格式的值(其中单个灰度输入值自动写入GPU画布的R-G-B通道)。您可以在此处阅读更多信息:WebGL introduction to "Data Textures"
WebGL安装起来并不有趣…这是一个很长的代码,但值得为“几乎在光”的速度,它给了回来。
下面是一个从my other answer修改而来的示例代码(它本身是从这个JSfiddle修改而来的,这是我在GPU初学者时学到的)。
示例代码:创建一个1000 x1000的纹理,以“N”FPS的速率用RGB/灰色重新填充。
变量:
pix_FPS
:设置FPS速率(将作为1000/FPS使用)。pix_Mode
:设置输入像素类型为“grey”或“rgb”pix_FPS
:设置FPS速率(将作为1000/FPS使用)。测试一下。。
ktca8awb4#
为了完整起见,这里是一个纯JS版本。
1000 x 1000 px灰度阵列→ RGBA阵列
~ 9或10毫秒在我的机器上。
我们能用WASM或其他技术做得更好吗?