【问题标题】:Large image copy to canvas using drawImage causes large memory usage使用 drawImage 将大图像复制到画布会导致大量内存使用
【发布时间】:2016-11-22 14:48:41
【问题描述】:

我有一个 23552 像素 x 8192 像素的大图像,用于将许多 1024 像素 x 1024 像素的单个图像组合成一个图像,类似于使用精灵图像组合图标/资产的方式。

我正在使用 Javascript Image 对象加载该图像。

然后,我使用 drawImage 从大图像中复制一个 1024 x 1024 的图块,并将其复制到相同尺寸 1024 x 1024 的屏幕外画布中。

然后,我使用 drawImage 将该屏幕外画布复制到另一个相同大小的屏幕外画布上。通常,在我的实际应用程序中,第二个画布实际上是屏幕上的画布,但为了简化问题的重现,我只是将第二个画布保留在屏幕外。

从第一个画布到第二个画布的 drawImage 导致 Mac 上的 Chrome 和 Firefox 的内存使用量增加了大约 700MB。我一直无法弄清楚为什么它会这么大,但是有一个非常简单的用例会导致它。

几秒钟后的 700MB 使用量被垃圾收集,所以我不认为这是内存泄漏,只是一个巨大的内存使用量。在我的真实案例中,700MB 的使用量让一些人在他们的机器上内存不足,并导致他们得到可怕的“Aw Snap!”在 Chrome 中。

这是Plunker 上以下代码的链接,我使用的图像在Imgur 上。在 Plunker 上你可以拉起 Activity Monitor 或者任何你用来监控内存使用的东西,然后点击 Button 开始运行下面的代码,并且观察进程的内存使用增加了 > 700MB。

  function drawcanvas() {
    var oImage = new Image();
    oImage.onload = function() {
      console.log('image loaded');

      // tile size in pixels, the image loaded is 23552 x 8192, which has 23 x 8 tiles
      var tileSize = 1024;

      // create a canvas that is not on DOM
      var hiddenCanvas = document.createElement('canvas');
      hiddenCanvas.width = tileSize;
      hiddenCanvas.height = tileSize;
      var hiddenContext = hiddenCanvas.getContext('2d');

      // create another canvas that is not on DOM
      var masterCanvas = document.createElement('canvas');
      masterCanvas.width = tileSize;
      masterCanvas.height = tileSize;
      var masterContext = masterCanvas.getContext('2d'); 

      // 1) drawing one tile, 1024 x 1024, from the big image into a canvas that is 1024 x 1024
      //    this causes negligible memory increaase by itself
      hiddenContext.drawImage(oImage, 0, 0, tileSize, tileSize, 0, 0, tileSize, tileSize);

      // 2) copy the 1024 x 1024 canvas into another canvas that is 1024 x 1024
      //    this causes about 700MB of memory usage in both Chrome & Firefox on Mac
      masterContext.drawImage(hiddenContext.canvas, 0, 0, tileSize, tileSize, 0, 0, tileSize, tileSize);

      console.log('processing done');
    };
    oImage.src = "http://i.imgur.com/VcIOEJF.png";    
  }

【问题讨论】:

  • 我必须假设您只为桌面编码,因为这么大的图像会淹没移动设备。台式机通常具有带有自己的 GPU 内存的 GPU。您的图像太大,您可能正在耗尽 GPU 内存,因此 GPU 必须使用其他内存资源来完成渲染 - 因此您的内存使用量。虽然拥有一个巨大的 spritesheet 可以简化您的编码要求,但它会给您的用户设备带来负担。我建议重新编码不要使用你巨大的精灵表。
  • @markE 你是对的,这只会在桌面上。但是使用 700MB 对我来说似乎有点过分了,这是我的好奇心之一。 23552 x 8192 = 1.92 亿像素。所以要获得 700MB,它必须每个像素存储 4 个字节,我猜每个通道 rgba 是 1 个字节。但是没有意义的是为什么它会在第二次 drawImage 调用中将整个图像读入内存,而我只是在两个画布之间移动 1024 x 1024 像素?此时,原始图像的大小应该无关紧要。
  • 是的,图像存储为原始位图,因此您认为图像“成本”为 23552x8192x4 是正确的。如果 GPU 内存已用尽(并且它可能是用那么大的图像),那么为了获取您的 1kX1k 小节,必须重新读取整个 imageData 以拉出该小节。我强烈建议重新编码不要使用你巨大的精灵表。我知道一个巨大的精灵表让你的编码生活更轻松,但这对你的用户来说并不公平。 ;-)
  • 23552 像素 x 8192 像素 - 绉纱! - 这将耗尽大部分 PC 内存。我认为您需要一种不同的方法...
  • 每个 1k x 1k 瓦片代表一组不同的数据,这些数据将堆叠在一起以将其全部可视化。每一层都可以有选择地隐藏和自定义颜色,1k x 1k 是一个实用的分辨率,可以用典型的桌面分辨率显示它,因此平铺的图像可以使它只需要发生一个网络请求,但我们仍然可以单独访问每一层。恰好有 23 * 8 组数据,这些数据组成了一个很大的初始图像来获取所有数据。

标签: javascript html canvas


【解决方案1】:

23552 by 8192 太大

就像大家说的那样。太大了。也不可能一次看到比设备的显示分辨率更多的像素。因此,不仅 RAM 使用得很好,而且使用的大部分 RAM 甚至都不能以像素的形式显示,要么在缩小时合并为单个像素,要么在放大时合并为单个像素。

总会有解决办法的

任何浏览器都可以显示任何分辨率的图片 例如365 Gigapixels,任何浏览器都可以创建任何分辨率的图片(如果你准备等待的话)

我在画布的浏览器和客户端脚本方面拥有丰富的经验,但我不是这个特定领域的专家。请参阅最后一段,了解解决问题的第一站应该是什么。我介绍的其余部分是一个概念性解决方案,并且只是众多可能解决方案中的一种。

可能的解决方案

你说你想可视化包含在 23*8 184 瓦片中的数据。

你不能像Image那样在除了高端机器之外的任何东西上存储那么多的图块。但是,您可以根据需要加载它们,一旦缓存加载将很快。

用户只能看到显示器可以显示的像素数,因此用于显示数据的图像不应超过显示分辨率。你有一个宽的图像格式(Aspect 2.875:1),所以我假设你会想要水平平移。我们可以适应宽格式,因为它不会出现问题(超过显示最大分辨率 2-3 倍的图像应该被认为太大)

由于图像的分辨率如此之高,因此可以公平地假设用户将能够放大并以一对一的分辨率查看像素。

因此,此案例将适用于全分辨率(全屏)的 1080HD 显示器。

以正确的宽高比创建屏幕外画布 (OSC) 以适合 1080 * 2.875 3105*1080(屏幕外画布的像素预算)。由于我假设所有缩放都是统一的,我们可以使用自然图像垂直分辨率 (VR= 8192) 作为计算参考,即 8192。我们将通过虚拟垂直分辨率 VVR 参考缩放。对于 VVR = 8192 的全缩放(像素一对一),对于 VVR = 8192 的一半,将是 VVR = 4096,查看图像以便使用所有显示像素(缩放以填充),这将是 VVR = 1080(显示器解决)。要查看所有图像(缩放以适应)VVR = Math.min(Display Res Hor / 23552, Display Res Vertical / 8192) * VR = ~0.0815(注意显示器的顶部和底部将为空,在此案例)

您使用 VR 来计算图块比例 TS = VVR/VR。然后,您将每个图块加载为图像,并使用按比例 TS 将该图像渲染到 OSC。在 (VVR

您可以购买另一个屏幕外画布来保存当前 OSC 的可见部分,以阻止在呈现给自身时可能发生的一些反馈伪影,但您应该从图块的经验法则可用 RAM 中减去该画布的 RAM(有关 RAM 预算的详细信息,请参阅此文档的下方)。

Notes 1, 2

这就是如何呈现大格式图像数据集的基础知识。

改进此解决方案的一些方法

加载和刷新屏幕外画布。

在 OSC 与完整图像具有相同分辨率的完美世界中,缩放将是平滑且实时的。不幸的是,所提出的解决方案有一些不需要的伪影,当放大显示时可能会变得模糊,当平移和缩小边缘的像素时会丢失并且需要等待图块填充。这是不可避免的。

预测和预加载图块

您可以通过预测用户将要执行的操作来缩短刷新 OSC 的时间,并在该操作确实发生时预加载图块。预加载图块的数量将取决于可用的 RAM。由于您不知道有多少可用(浏览器不提供该信息),您可以使用经验法则计算来获取数字。

Thumb 的 RAM 预算规则

计算图块的 RAM 预算。 RAM 以设备显示分辨率 * 9 存储图像。因此,如果屏幕分辨率为 1920 * 1080,则显示 RAM 为 1920 * 1080 * 4 = 8Mb,然后乘以 9 (3*3) 得到 72Mb。然后除以平铺 RAM 1024 * 1024 = 1MB 约 72 个平铺,您可以合理地确保该设备将具有可用的(请注意,具有低分辨率的设备可能是精打细算的设备和显示比例可用 RAM 的大小会更小。我将乘数从 9 减少到 4 (2*2)

多分辨率图块

您可以通过创建许多不同的图块集来提高图块加载性能。基本图块集为 1024 * 1024 像素(全分辨率),但当缩小时,大部分像素都会丢失。与其加载 4 个图块以将它们全部绘制成一半大小,不如预先渲染一个以该缩放比例设置的新图块,以便一个 1024 x 1024 的图块包含 4 个原始图块,因此您只需要加载 1/4 的图块。多少您创建的瓦片集将由许多因素决定,将每个集的分辨率加倍是最简单的,但如果用户停止在与瓦片集不匹配的缩放时,您需要以更高的分辨率加载瓦片。在最大缩小时,平铺集可能与该缩放不匹配。由于它相当容易上手并且服务器/缓存存储丰富,因此可以通过实验(和用户案例研究,类似于文档底部的注释 2)找到最佳结果

编码或解码图像

存储在缓存/存储设备中的图像是图像的编码表示。编码通常包括压缩(对于 jpg/webp 是有损的,对于 png/gif 是无损的。)当您创建和加载图像时,像素会被解码以创建位图。解码图像比编码图像大得多,将 184 个图块存储为解码图像会突破 RAM 限制,但您可以将编码图像作为 BASE64 存储在 8 位类型数组中。您还可以在 BASE64 上进行简单快速的 BASE64 到二进制流打包(因为 BASE64 不使用每 3 个字节编码的 6 位),以便类型化数组大小与映像磁盘大小匹配。您可以通过 AJAX 或 fileloader 加载编码的图像

如果您不需要图块的 Alpha 通道,请使用 JPEG。 “高质量”或质量设置为 50 的 JPEG 的平均压缩率约为 15:1(对于 RGB)和 20:1(对于 RAM rgba(Loaded Image()))。在这种质量压缩伪影刚刚开始对不经意的观察者变得可见。在这种压缩下,完整的瓦片集将需要约 40Mb 来将所有瓦片存储为编码位压缩的 JPEG,并在 1920×1080 案例解决方案机器资源的 RAM 预算内

警告与将编码图像存储在 RAM 中有关。当涉及到每秒指令时,Javascript 在浏览器之间甚至在浏览器版本之间并不一致。由于这种方法使用相同的少量指令数百万次,即使是最次要的性能因素也会对整个加载和呈现 BASE64url 到 Image.src 以进行解码产生重大影响。您应该彻底测试在您期望使用的尽可能多的浏览器、浏览器版本、设备和设备 CPU 类型上实现此方法的任何尝试。应该针对从缓存/本地磁盘加载编码图像进行测试。仅在有明显好处的情况下使用此方法。由于执行此操作的代码并不多,因此您可以选择性地将此方法仅用于能够提高性能的平台。

所以要从你应该开始的地方结束...... :)

从哪里开始

现成的解决方案

所有这些工作量很大,对于这种类型的图像显示,有企业和开源解决方案值得研究,并且应该是实施您自己的解决方案之前的第一站。我只介绍了一些在避免过度使用 RAM(和浏览器崩溃)方面改进应用程序的方法。其他人构建和优化此类应用程序涉及更多内容和花费大量时间,我绝对不会意味着该特定领域的专家,即使只是给您一些提示,四处购物也会有所帮助。


注1

放大时,如果要查看的数据可能会导致大量垂直平移,则可以更改屏幕外画布分辨率,使其更高,如果平移预期是任何方向,则使其呈方形,如果平移主要是水平保持它宽。

注2

如果您的应用是基于服务器的,则将所有用户的所有平移记录为两个数字。水平 (HP) 和垂直 (VP) 平移的像素数。在会话结束时将统计信息发送到服务器,然后在服务器上划分 HP/VP 以获得所有用户的平移方向比率。如果这个比率接近 1,你知道在缩放时创建一个方形的屏幕画布,如果远大于 1,你知道用户更有可能水平平移,所以保持宽纵横比,小于 1 使它更高(不要超出像素预算))

【讨论】:

  • 感谢您提供非常详细的回答。我认为我们要做的是混合服务器/客户端解决方案。目前,大部分负担都在客户端,它比服务器有更多的内存限制。所以我认为我们可能不得不将更多的东西推到服务器上,并让它帮助预先组合/堆叠一些数据,以减少我们最初开始客户端部分的图像/数据的大小的处理。
猜你喜欢
  • 1970-01-01
  • 2013-07-19
  • 1970-01-01
  • 2013-11-01
  • 2012-05-01
  • 1970-01-01
  • 2015-06-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多