如何快速逐像素地进行图像处理?

时间:2016-08-11 15:04:37

标签: javascript performance image-processing canvas webgl

使用2D画布上下文,我实现了一个图像处理算法 具有以下特征的JavaScript:

  • 每行都可以单独处理。

  • 每条线从左到右处理。中的一个像素的颜色 输出图像来自:

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

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

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

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

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

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

  • 将像素写入输出图像,根据Chrome的分析器,它是 大部分时间的单一操作:

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

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

2 个答案:

答案 0 :(得分:2)

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

但是,如果我理解这应该是做什么的,那么似乎有一种非常优化的方式。在我看来,你喜欢拍摄一张图像并向右移动一个像素。然后,最左边的空列由前一个第一列像素填充。

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

答案 1 :(得分:1)

目前尚不清楚你想要完成什么。

根据您的说明

  

每条线从左到右处理。输出图像中像素的颜色来自:

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

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

  • 第1栏=第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);
<canvas width="1000" height="1000"></canvas>

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