JavaScript逐像素画布操作

时间:2019-08-04 11:00:22

标签: javascript jquery canvas html5-canvas

我正在开发一个简单的Web应用程序,该应用程序可以将上传的图像的颜色简化为用户选择的调色板。该脚本可以工作,但是要花很长时间才能遍历整个图像(对于大图像来说,这需要几分钟的时间),因此需要更改像素。

最初,我是在写画布本身,但是我更改了代码,以便对ImageData对象进行更改,并且仅在脚本末尾更新了画布。但是,这并没有太大的不同。

// User selects colours:
colours = [[255,45,0], [37,36,32], [110,110,105], [18,96,4]];

function colourDiff(colour1, colour2) {
    difference = 0
    difference += Math.abs(colour1[0] - colour2[0]);
    difference += Math.abs(colour1[1] - colour2[1]);
    difference += Math.abs(colour1[2] - colour2[2]);
    return(difference);
}

function getPixel(imgData, index) {
    return(imgData.data.slice(index*4, index*4+4));
}

function setPixel(imgData, index, pixelData) {
    imgData.data.set(pixelData, index*4);
}

data = ctx.getImageData(0,0,canvas.width,canvas.height);
for(i=0; i<(canvas.width*canvas.height); i++) {
    pixel = getPixel(data, i);
    lowestDiff = 1024;
    lowestColour = [0,0,0];
    for(colour in colours) {
        colour = colours[colour];
        difference = colourDiff(colour, pixel);
        if(lowestDiff < difference) {
            continue;
        }
        lowestDiff = difference;
        lowestColour = colour;
    }
    console.log(i);
    setPixel(data, i, lowestColour);
}
ctx.putImageData(data, 0, 0);

在整个过程中,网站被完全冻结,因此我什至无法显示进度栏。有什么方法可以优化它,从而减少时间吗?

3 个答案:

答案 0 :(得分:0)

一个明显的问题或改进选择显然是您的slice函数,该函数每次被调用时都会创建一个新数组,您不需要这样做。我会像这样更改for循环:

for y in canvas.height {
  for x in canvas.width {
    //directly alter the canvas' pixels
  }
}

答案 1 :(得分:0)

无需在每次迭代时对数组进行切片。 (就像尼古拉斯已经说过的那样。)

我将遍历数据数组,而不是遍历画布尺寸并直接编辑数组。

for(let i = 0; i < data.length; i+=4) { // i+=4 to step over each r,g,b,a pixel
  let pixel = getPixel(data, i);
  ...
  setPixel(data, i, lowestColour);
}

function setPixel(data, i, colour) {
    data[i] = colour[0];
    data[i+1] = colour[1];
    data[i+2] = colour[2];
}

function getPixel(data, i) {
    return [data[i], data[i+1], data[i+2]];
}

此外,如果您打开控制台,console.log可以使浏览器崩溃。如果您的图像是1920 x 1080,那么您将登录控制台2,073,600次。

您还可以将所有处理传递给Web Worker,以获得最终的线程性能。例如。 https://jsfiddle.net/pnmz75xa/

答案 2 :(得分:0)

发现颜色差异

我添加一个答案是因为您使用了非常差的色彩匹配算法。

如果您将每种可能的颜色想象成3D空间中的一个点,那么最好找到一种颜色与另一种颜色的接近程度。红色,绿色和蓝色代表x,y,z坐标。

然后,您可以使用一些基本的几何图形来定位从一种颜色到另一种颜色的距离。

// the two colours as bytes 0-255
const colorDist = (r1, g1, b1, r2, g2, b2) => Math.hypot(r1 - r2, g1 - g2, b1 - b2);

还必须注意,通道值0-255是压缩值,实际强度接近该值的平方(channelValue ** 2.2)。这意味着红色= 255的强度是红色= 1的65025倍

以下功能是两种颜色之间色差的近似值。避免使用Math.hypot函数,因为它非常慢。

 const pallet = [[1,2,3],[2,10,30]]; // Array of arrays representing rgb byte 
                                     // of the colors you are matching
 function findClosest(r,g,b) {  
     var closest;
     var dist = Infinity;
     r *= r;
     g *= g;
     b *= b;
     for (const col of pallet) {
        const d = ((r - col[0] * col[0]) + (g - col[1] * col[1]) + (b - col[2] * col[2])) ** 0.5;
        if (d < dist) {
            if (d === 0) { // if same then return result
                return col;
            }
            closest = col;
            dist = d;
        }
    }
    return closest;
  }

关于性能,最好的选择是通过网络工作者或使用webGL实时进行转换。

如果要保持简单性以防止代码阻塞页面,请使用计时器为页面腾出空间,将作业切成较小的切片。

该示例使用setTimeoutperformance.now()进行10ms的切片,让其他页面事件和渲染完成该操作。它返回一个承诺,该承诺将在处理完所有像素后解决

  function convertBitmap(canvas, maxTime) { // maxTime in ms (1/1000 second)
     return new Promise(allDone => {
         const ctx = canvas.getContext("2d");
         const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
         const data = pixels.data;
         var idx = data.length / 4;
         processPixels(); // start processing
         function processPixels() {
             const time = performance.now();
             while (idx-- > 0) {
                 if (idx % 1024) { // check time every 1024 pixels
                      if (performance.now() - time > maxTime) {
                          setTimeout(processPixels, 0);
                          idx++;
                          return;
                      }
                 }
                 let i = idx * 4;
                 const col = findClosest(data[i], data[i + 1], data[i + 2]);
                 data[i++] = col[0];
                 data[i++] = col[1];
                 data[i] = col[2];
             }
             ctx.putImageData(pixels, 0, 0);
             allDone("Pixels processed");
         }
      });
  }

  // process pixels in 10ms slices. 
  convertBitmap(myCanvas, 10).then(mess => console.log(mess));