【问题标题】:How to make image processing pixel-by-pixel fast?如何快速逐像素处理图像?
【发布时间】:2016-12-18 09:47:34
【问题描述】:

使用 2D 画布上下文,我在 JavaScript 具有以下特点:

  • 每一行都可以单独处理。

  • 每一行都是从左到右处理的。中一个像素的颜色 输出图像的计算来源:

    • 输入图像在当前位置的颜色

    • 对于第一个像素之后的所有像素:输出图片中的颜色 上一个位置,即左侧一个像素

算法的运行时间相应为:O(n)

该算法在 Chrome 52 中非常慢,因为画布操作很慢:

  • 在输入图像的当前位置读取颜色: inputImageData.slice(location, location + 4)

  • (读取输出图像中前一个位置的输出颜色为 快,它取自变量)

  • 将像素写入输出图像,根据 Chrome 的分析器,它是 花费最多时间的单个操作:

    outputImageContext.fillStyle = outputColor;
    outputImageContext.fillRect(x, y, 1, 1);
    

我研究了使用 WebGL 进行图像处理。不过好像不适用 因为使用 WebGL,所有像素都是独立计算的。算法 要求从左到右进行扫描。

【问题讨论】:

  • 听起来像是您最好在完全服务器端或通过网络工作者完成的事情。也许您可以拆分这些行并为每行创建一个 Web Worker 实例,这样您就可以同时运行它们。
  • @Shilly 我想做客户端,也想看看我能走多远。这不是一个昂贵的算法,只是当前的实现非常慢。感谢网络工作者的建议!
  • 你试过 w 子数组而不是 slice 吗?
  • 将需要查看您的算法以确定是否有优化的可能性。
  • 您敢打赌,为每个像素设置 fillstyle + fillrect 会很慢!如果你已经有了 imageData,直接修改它的值,然后使用 putImageData

标签: javascript performance image-processing canvas webgl


【解决方案1】:

不清楚你想要完成什么。

根据你的描述

每一行都是从左到右处理的。输出图像中像素的颜色计算公式为:

  • 输入图像中当前位置的颜色
  • 对于第一个像素之后的所有像素:输出图片在前一个位置的颜色,即左侧一个像素

听起来您只是要将第一列涂抹在整个图像上。

  • 第 1 列 = 第 1 列,
  • 第 2 列 = 第 1 列,
  • 第 3 列 = 第 2 列,
  • 第 4 列 = 第 3 列。

从左到右处理只是重复的第一列。如果是这样,只需调用ctx.drawImage(ctx.canvas, 0, 0, 1, ctx.canvas.height, 0, 0, ctx.canvas.width, ctx.canvas.height);,它将使用画布作为图像,取第一列并将其展开以填充整个画布。

否则我并没有真正理解你的算法。对不起

无论如何,假设您确实需要立即引用先前像素的结果,然后进行一些操作。

切片 imagedata.data 以获取像素会很慢。每个切片都是一个内存分配。如果你想要速度,最好不要为每个像素创建一个新对象。

至于写作,是的,使用fillRect 一次设置一个像素会非常慢。直接在ImageData 中设置像素,然后将 ImageData 放回画布中怎么样?

否则,请尽量减少内部循环中的工作。如果你只能计算一次,那就只计算一次。

这里有一些混合成一个圆圈的代码。这不是那么慢,虽然我认为虽然慢是主观的。

var ctx = document.querySelector("canvas").getContext("2d");
var width = ctx.canvas.width;
var height = ctx.canvas.height;

function r(min, max) {
  if (max === undefined) {
    max = min;
    min = 0;
  }
  return Math.random() * (max - min) + min;
}

function drawRandomCircle(ctx) {
  var color = "hsl(" + r(360) + ", " + r(50, 100) + "%, 50%)";
  ctx.beginPath();
  ctx.arc(r(width), r(height), r(100), 0, Math.PI * 2, false);
  ctx.fillStyle = color;
  ctx.fill();  
}

// put some image into the canvas so have something to work with
ctx.fillStyle = "white";
ctx.fillRect(0, 0, width, height);
for (var ii = 0; ii < 200; ++ii) {
  drawRandomCircle(ctx);
}

function process(time) {
  time *= 0.001;
  
  drawRandomCircle(ctx);
  
  // get a copy of the image
  var imageData = ctx.getImageData(0, 0, width, height);
  var pixels = imageData.data;
  
  var xoff = Math.sin(time) * 15 | 0;
  var yoff = Math.cos(time) * 15 | 0;

  // blur
  for (var y = 0; y < height; ++y) {
    var lineOffset = (y + yoff + height) % height * width;
    for (var x = 0; x < width; ++x) {
      var off0 = (y * width + x) * 4;
      var off1 = (lineOffset + (x + xoff + width) % width) * 4;
       
      var r0 = pixels[off0 + 0];
      var g0 = pixels[off0 + 1];
      var b0 = pixels[off0 + 2];
      
      var r1 = pixels[off1 + 0];
      var g1 = pixels[off1 + 1];
      var b1 = pixels[off1 + 2];
      
      pixels[off0 + 0] = (r0 * 9 + r1) / 10;
      pixels[off0 + 1] = (g0 * 9 + g1) / 10;
      pixels[off0 + 2] = (b0 * 9 + g1) / 10;
    }
  }
  ctx.putImageData(imageData, 0, 0);
  requestAnimationFrame(process);
}

requestAnimationFrame(process);
&lt;canvas width="1000" height="1000"&gt;&lt;/canvas&gt;

请注意,上面的代码在每一帧getImageData 都会获得一个新的画布副本。它这样做是为了可以看到刚刚绘制的新圆圈。这意味着每帧至少有一个大内存分配来获取画布中像素的新副本。如果您不需要这样做,请在初始化时制作副本并继续使用相同的数据。

【讨论】:

    【解决方案2】:

    我很抱歉不理解:你说输出应该是左边一个像素的输入颜色,但你的“读取”算法说位置 + 4。

    但如果我明白这应该做什么,似乎有一种优化的方法可以做到这一点。在我看来,您正在拍摄图像并将其向右移动一个像素。然后,最左边的空列由之前的第一列像素填充。

    使用正确的算法,您应该能够使用原始数据但复制第一列,是吗?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-17
      • 1970-01-01
      • 2023-04-04
      • 2021-02-10
      • 2013-03-11
      相关资源
      最近更新 更多