如何通过WebSockets高效处理大量的HTML5画布像素数据

9

可能是重复问题:
通过websocket接收图像

使用

imageData = context.getImageData(0, 0, width, height);
JSON.stringify(imageData.data);

我获取像素数据,将其转换为字符串,然后通过websockets发送。但是,这个字符串可能相当大,取决于画布对象的大小。我尝试使用在此处找到的压缩技术:JavaScript implementation of Gzip,但是socket.io会抛出错误Websocket message contains invalid character(s). 有没有一种有效的方法来压缩这些数据,以便可以通过websockets发送?

这个怎么样?https://dev59.com/bGw15IYBdhLWcg3wCnQn。实际上,我只是在谷歌上搜索 :D - Esailija
@Esailija 是的,但这些都不是关于最佳压缩字符串的方法...只是关于如何发送图像或二进制数据的。 - user730569
@Esailija,将字符串转换为原始二进制的方法是什么? - user730569
不要使用base64字符串或utf-8字符串,因为1)浏览器支持将画布提取为压缩的JPEG或PNG图像以原始二进制数据格式(blob)的形式,2)WebSockets支持发送二进制blob。只有在处理遗留(不)兼容性时才需要处理base64。 - Mikko Ohtamaa
@Mikko,实际上,目前浏览器对于canvas to Blob的本地支持有些有限。另一方面,几乎每个支持canvas的浏览器都支持toDataURL方法。此外,基于base64的编码/解码在CPU方面相当高效。 - kanaka
显示剩余4条评论
4个回答

7

根据你想要提高的效率轴(带宽 vs CPU 效率),我会推荐几种方法。

选项1: 可以使用 canvas toDataURL 方法。这将返回一个 canvas 图像数据的 base64 编码图像。它将使用指定的图像格式进行压缩(或使用默认的 PNG 格式),并且会被预编码为 base64 以便通过 WebSocket 发送。

canvas = document.getElementById("mycanvas");
b64png = canvas.toDataURL();

ws.send(b64png);

选项2:如果您可以接受有损压缩,则可以使用toDataURL方法请求图像的base64编码JPEG格式:

canvas = document.getElementById("mycanvas");
b64jpeg = canvas.toDataURL("image/jpeg");

ws.send(b64jpeg);

选项 3:如果您正在使用支持二进制 WebSocket 数据的浏览器(Chrome、Firefox、IE 10),那么您可以直接通过 WebSocket 发送画布数组缓冲区。

canvas = document.getElementById("mycanvas");
ctx = canvas.getContext('2d');
imgdata = ctx.getImageData(0,0, width, height).data; // This is a Uint8ClampedArray
ws.send(imgdata.buffer); // Send the ArrayBuffer from the Uint8ClampedArray

选项3在带宽方面可能不太高效,但在客户端和服务器端的处理能力方面最高效,因为图像数据是原始的,几乎不需要预处理或后处理。
最节省带宽的选项可能是#2,但在将图像数据转换为JPEG格式时会损失一些图像质量。甚至可以进一步将数据解码为数组缓冲区或 blob,并通过二进制 WebSocket 发送,以避免33%的 base64 带宽开销,但这会增加更多的 CPU 开销。
如果您想要高效的带宽而不失去任何图像质量,则选项#2 是您最好的选择。
一些注意事项/警告:
toDataURL 将 base64 数据前缀命名为类似于此的内容:
"data:image/png;base64,iVBORw0KGgoAAAA..."

数据URL格式的一个好处是你可以将整个URL复制到浏览器地址栏中,浏览器就会渲染出图片。

查看MDN Canvas页面以获取更多关于toDataURL的信息。


最好发送二进制而不是Base64编码吗? - Mikko Ohtamaa
@MikkoOhtamaa,我认为我在答案中解决了这个问题。从带宽效率来看,它更好,但我不知道任何可以给你一个未经base64编码的压缩图像的API。这意味着你必须在发送之前手动将其解码为二进制数据类型,这会增加客户端的CPU开销。 - kanaka
@dforce,没错,旧规范要求使用ArrayBuffers,但现在已经允许使用完整的类型化数组。我对发送缓冲区进行了更改,这样就可以与支持通过WebSockets传输二进制数据的所有浏览器版本兼容。 - kanaka
@dforce:你不应该手动复制画布数据。如果你这样做了,那就说明浏览器有bug。请向存在此问题的浏览器供应商报告bug,然后在此处回复以供参考。 - kanaka
显示剩余7条评论

4
最节省带宽的方法是以JPEG编码二进制形式发送类似照片的数据作为blob。您可以将<canvas>数据转换为二进制JPEG blob:

https://github.com/miohtama/Krusovice/blob/master/src/tools/resizer.js#L51

对于非照片类内容,您也可以获取PNG二进制大对象。

二进制大对象始终是原始二进制,不涉及UTF-8或base64编码。

WebSocket.send()支持以二进制大对象作为输入:

https://developer.mozilla.org/en/WebSockets/WebSockets_reference/WebSocket

HTTP Blob发送:

https://developer.mozilla.org/en/DOM/XMLHttpRequest/Sending_and_Receiving_Binary_Data

您在不同浏览器中的里程可能会有所不同。


除非您正在运行支持canvas.mozGetAsFile的Firefox(这很可能会消失),否则这将是最不CPU高效的方法之一,因为您必须将toDataURL的结果解码为ArrayBuffer并构造一个Blob。此外,在这种情况下,Blob转换只是开销,ArrayBuffer可以直接通过WebSocket通道发送。 - kanaka
2
-1 有几个原因:JPEG并不总是编码图像数据最有效的方式(对于类似剪贴画的图像,PNG通常更有效),而且在WebSockets上发送ArrayBuffer与Blob的带宽效率相同(在大多数浏览器下,Blob的CPU效率更低,因为你在这种情况下进行了不必要的ArrayBuffer到Blob转换)。如果修正了明显错误的开头陈述,我会取消投票反对。 - kanaka
这真的取决于很多变量。对于较小的图像,这似乎是正确的。对于较大的图像,我的临时测试表明,一个base64字符串在被gzip压缩后可能会比其等效的JPEG文件占用更少的字节。但当然,这取决于服务器是否接受gzip压缩的http请求。 - nikc.org
对于类似照片的图像数据,base64字符串不可能比JPEG更小。JPEG已经在内部执行了它自己的熵编码。如果需要图像数据的无损编码,只需使用PNG,这可能适用于类似图形的内容,如答案中所述。 - Mikko Ohtamaa
Blob比发送base64字符串更节省带宽。此外,正如答案中提到的那样,PNG对于像图像数据这样的图形具有更好的压缩效果。 - Mikko Ohtamaa
浏览器中还提供了用于提取<canvas>数据的新API,但效果因人而异。 - Mikko Ohtamaa

3

既然你要求更高效的方法,我不同意关闭。我们能做的最少是帮你想出更高效的方法。

这取决于你在做什么。客户端能否多做些工作?

你真的需要发送全部画布像素数据吗?能否只发送已更改的像素?(或者几乎所有像素都更改了吗?)

来回只发送更改内容将使问题转化为计算问题,而非大量数据在网络上传输的问题。


根据你的应用程序,你能否跟踪已更改的区域?如果画布上有2-3个小矩形发生了变化,那么发送的内容应该比整个画布要小得多。


与任何效率问题一样,值得问一下自己是否首先在做正确的事情。你真的需要通过网络传输大量像素数据吗?通常,使用画布,通过发送更改视图的命令来重新创建场景相对于发送位图本身要容易得多。WebSockets非常适合此类功能。这可能是许多绘图应用程序和游戏的良好解决方案,但这确实取决于你要完成什么任务。


在“使用websocket以最节省带宽的方式将完整画布图像发送到服务器”的限制条件下,我已经写下了明确的答案。 - Mikko Ohtamaa

0

我发现了一种显著减少通过网络发送的数据量的方法。

getImageData 方法返回一个对象,其中 data 属性本身就是一个对象,其键为像素的索引,值为单个红色、绿色、蓝色或 alpha 值。由于键使对象变得非常大,特别是因为 300x300 画布对象将具有 300x300x4 = 360,000 个对象键/值对。

因此,通过仅提取颜色的值并将它们推入数组中:

function extractValues(obj){
  var array = [];
  for (var key in obj){
    array.push(obj[key]);
  }
  return array;
}

我成功地将数据减少了50%以上,这导致了显著的性能提升。


客户端编码为PNG或JPEG可以将数据传输减少数个数量级。 - Mikko Ohtamaa
@MikkoOhtamaa 当然可以,但是如果不做我在这个答案中提到的事情,那就太愚蠢了,除了压缩还要做其他的。 - user730569

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接