如何使图像处理逐像素快速?

3
使用2D画布上下文,我在JavaScript中实现了一个图像处理算法,具有以下特点:
- 每行可以单独处理。 - 每行从左到右进行处理。输出图像中像素的颜色计算方式如下: - 当前位置输入图像中的颜色 - 对于第一个像素之后的所有像素:前一个位置输出图像中的颜色,即向左移动一个像素。
该算法的运行时间为 O(n)。
由于画布操作速度较慢,该算法在Chrome 52中非常缓慢。
  • reading color at current location in input image: inputImageData.slice(location, location + 4)

  • (reading the output color of the previous location in the output image is quick, it’s taken from a variable)

  • writing pixel to output image, which – according to Chrome’s profiler – is the single operation that takes most of the time:

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

我研究了使用WebGL进行图像处理。然而,由于在WebGL中所有像素都是独立计算的,所以似乎不适用。该算法要求从左到右进行扫描。


1
听起来最好是完全在服务器端或通过Web Worker完成。也许你可以将这些行分开,并为每一行创建一个Web Worker实例,这样你就可以同时运行它们。 - Shilly
你试过使用 w 子数组而不是切片吗? - bjanes
1
需要查看您的算法,以确定是否存在优化的可能性。 - Blindman67
@Blindman67 算法没问题。如之前所述,它的运行时间为*O(n)*。问题出在实现上。根据Chrome的分析器,占用大部分时间的函数是outputImageContext.fillRect(x, y, 1, 1)。因此,这应该是优化的第一件事情。 - feklee
2
使用设置fillStyle + fillRect来为每个像素进行填充的话会很慢!如果你已经有了imageData,请直接修改其值,然后使用putImageData。 - Kaiido
显示剩余3条评论
2个回答

2

对于我没有理解清楚的部分我感到抱歉:您说输出应该是向左一个像素的输入颜色,但是您的“读取”算法中写的是位置+4。

但是如果我正确理解了这个程序应该做什么,似乎有一种更加优化的方法可以实现它。看起来您正在将图像向右移动一个像素。然后,最左边的空列由先前的第一列像素填充。

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


1

不清楚您想要实现什么。

根据您的描述

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

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

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

  • 第一列 = 第一列,
  • 第二列 = 第一列,
  • 第三列 = 第二列,
  • 第四列 = 第三列。

从左到右处理就是重复第一列。如果是这样,只需调用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);
<canvas width="1000" height="1000"></canvas>

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


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