简体   繁体   English

JavaScript逐像素画布操作

[英]JavaScript pixel by pixel canvas manipulation

I'm working on a simple web app which simplifies the colours of an uploaded image to a colour palette selected by the user. 我正在使用一个简单的Web应用程序,该应用程序将上传的图像的颜色简化为用户选择的调色板。 The script works, but it takes a really long time to loop through the whole image (for large images it's over a few minutes), changing the pixels. 该脚本可以工作,但是要花很长时间才能遍历整个图像(对于大图像来说,这需要几分钟的时间),因此需要更改像素。

Initially, I was writing to the canvas itself, but I changed the code so that changes are made to an ImageData object and the canvas is only updated at the end of the script. 最初,我是在写画布本身,但是我更改了代码,以便对ImageData对象进行更改,并且仅在脚本末尾更新画布。 However, this didn't really make much difference. 但是,这并没有太大的区别。

// 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);

During the entire process, the website is completely frozen, so I can't even display a progress bar. 在整个过程中,网站被完全冻结,因此我什至无法显示进度栏。 Is there any way to optimise this so that it takes less time? 有什么方法可以对此进行优化,从而减少时间吗?

One problem or option for improvement is clearly your slice function, which will create a new array every time it is called, you do not need this. 改进的一个问题或选择显然是您的slice函数,该函数将在每次调用它时创建一个新数组,而您不需要这样做。 I would change the for loop like so: 我会像这样更改for循环:

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

There is no need to slice the array each iteration. 无需在每次迭代时对数组进行切片。 (As niklas has already stated). (如niklas所述)。

I would loop over the data array instead of looping over the canvas dimensions and directly edit the array. 我将遍历数据数组,而不是遍历画布尺寸并直接编辑数组。

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]];
}

Also, console.log can bring a browser to it's knees if you've got the console open. 另外,如果您打开控制台,console.log可以使浏览器崩溃。 If your image is 1920 x 1080 then you will be logging to the console 2,073,600 times. 如果您的图像是1920 x 1080,那么您将登录到控制台2,073,600次。

You can also pass all of the processing off to a Web Worker for ultimate threaded performance. 您还可以将所有处理传递给Web Worker,以获得最终的线程性能。 Eg. 例如。 https://jsfiddle.net/pnmz75xa/ https://jsfiddle.net/pnmz75xa/

Finding difference in color 发现颜色差异

I am adding an answer because you have use a very poor color match algorithm. 我添加一个答案,因为您使用了非常差的色彩匹配算法。

Finding how closely a color matches another is best done if you imagine each unique possible colour as a point in 3D space. 如果您将每种可能的颜色想象为3D空间中的一个点,那么最好找到一种颜色与另一种颜色的匹配程度最佳。 The red, green, and blue values represent the x,y,z coordinate. 红色,绿色和蓝色值代表x,y,z坐标。

You can then use some basic geometry to locate the distance from one colour to the another. 然后,您可以使用一些基本几何图形来定位一种颜色到另一种颜色的距离。

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

It is also important to note that the channel value 0-255 is a compressed value, the actual intensity is close to that value squared ( channelValue ** 2.2 ). 同样重要的是要注意,通道值0-255是压缩值,实际强度接近于该值的平方( channelValue ** 2.2 )。 That means that red = 255 is 65025 times more intense than red = 1 这意味着红色= 255的强度是红色= 1的65025倍

The following function is a close approximation of the colour difference between two colors. 以下函数是两种颜色之间色差的近似值。 Avoiding the Math.hypot function as it is very slow. 避免使用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;
  }

As for performance, your best bet is either via a web worker, or use webGL to do the conversion in realtime. 至于性能,最好的选择是通过网络工作者,或使用webGL实时进行转换。

If you want to keep it simple to prevent the code from blocking the page cut the job into smaller slices using a timer to allow the page breathing room. 如果要简化操作,以防止代码阻塞页面,请使用计时器为页面腾出空间,将作业切成较小的切片。

The example uses setTimeout and performance.now() to do 10ms slices letting other page events and rendering to do there thing. 该示例使用setTimeoutperformance.now()进行10ms的切片,让其他页面事件和渲染执行该操作。 It returns a promise that resolves when all pixels are processed 它返回一个承诺,该承诺将在处理所有像素时解决

  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));

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM