Search code examples
javascriptimage-processingcanvasmultidimensional-arrayweb-worker

Efficiently Iterating through image in Javascript


I am doing some image processing project in JavaScript where i need to iterate through each image pixels and do some other processing to achieve my target.
I am using canvas to get an image pixels data array.
For small images, like 500x300 px in dimensions, its working fine and taking acceptable time. But for large images, like 3000x3000 px in dimensions, the iteration process is becoming a bottleneck and taking a huge time, like 10-12 seconds.

So is there any method or trick which could be used to reduce the time used in the iteration steps?

Here is what I am thinking about: I am trying to use parallel web workers(let it be 4) to iterate through equal parts of the image data: (eg. 0-[len/4], [len/4]+1-[len/2], [len/2]+1 - [len*3/4], [len*3/4]+1 - len) where lenis the size of the image data array.

I doubt this approach will be more time efficient since Javascript is single threaded.

 function rgb2grey(pix,offset){
        return (0.2989*pix[offset] + 0.5870*pix[offset+1] + 
    0.1140*pix[offset+2]);

}

function imgcompare(fileData1,fileData2,targetpix){
        var len = pix.length;
        for (var j = 0; j <len; j+=4) {
                var grey1 = rgb2grey(fileData1,j);
            var grey2 = rgb2grey(fileData2,j);
            if(grey1!=grey2){
                targetpix[j] = 255;
                targetpix[j+1] = targetpix[j+2] = 0;
            }
            else{
                targetpix[j] = fileData1[j];
                targetpix[j+1] = fileData1[j+1];
                targetpix[j+2] = fileData1[j+2];
            }       
            targetpix[j+3] = fileData1[j+3];
        }
}

Solution

  • 2D canvas API and GPU assisted image processing.

    The canvas 2D API provides a powerful set of GPU assisted composite operations. Many times they can replace slow pixel by pixel operations done via Javascript and reading pixel via getImageData.

    Many times this can make the processing a realtime solution for video or animations, and it also has the advantage that it can process tainted canvases that otherwise would be impossible using any other method.

    The OP's processing via GPU assisted compositing

    In the case the question example there is some room for optimisation by using the canvas 2D composite operations. This will utilise the GPU to do the per-pixel math for you though you will have to create two additional canvases.

    To mark pixels with red that are different between two images.

    • Create two copies
    • Get difference in pixels using comp "difference"
    • Make difference BW using comp "saturation"
    • Max the difference by rendering difference over itself using comp "lighter"
    • Invert the difference using comp difference and rendering a white rect over it
    • Multiply imageA's copy with the inverted difference using comp "multiply"
    • Invert the mask again
    • Set channels Green and Blue to zero in the difference canvas using comp "multiply".
    • Add the mask to the original masked image using comp "lighter".

    Demo

    The demo loads two images and then marks the differences between the two images in red ("#F00") using the method outlined above.

    // creates a copy of an image as a canvas
    function copyImage(image) {
        const copy = document.createElement("canvas");
        copy.width = image.width;
        copy.height = image.height;
        copy.ctx = copy.getContext("2d"); // add context to the copy for easy reference
        copy.ctx.drawImage(image, 0, 0);
        return copy;
    }
    // returns a new canvas containing the difference between imageA and imageB
    function getDifference(imageA, imageB) {
        const dif = copyImage(imageA);
        dif.ctx.globalCompositeOperation = "difference";
        dif.ctx.drawImage(imageB, 0, 0);
        return dif;
    }
    // Desaturates the image to black and white
    function makeBW(image) { // color is a valid CSS color
        image.ctx.globalCompositeOperation = "saturation";
        image.ctx.fillStyle = "#FFF";
        image.ctx.fillRect(0, 0, image.width, image.height);
        return image;
    }
    // Will set all channels to max (255) if over value 0
    function maxChannels(image) { 
        var i = 8; // 8 times as the channel values are doubled each draw Thus 1 * 2^8 to get 255
        image.ctx.globalCompositeOperation = "lighter";
        while (i--) {
            image.ctx.drawImage(image, 0, 0)
        }
        return image;
    }
    // Inverts the color channels resultRGB = 255 - imageRGB
    function invert(image) {
        image.ctx.globalCompositeOperation = "difference";
        image.ctx.fillStyle = "#FFF";
        image.ctx.fillRect(0, 0, image.width, image.height);
        return image;
    }
    // Keeps pixels that are white in mask and sets pixels to black if black in mask.
    function maskOut(image, mask) {
        image.ctx.globalCompositeOperation = "multiply";
        image.ctx.drawImage(mask, 0, 0);
        return image;
    }
    // Adds the channels from imageB to imageA. resultRGB = imageA_RGB + imageB_RGB
    function addChannels(imageA, imageB) { // adds imageB channels to imageA channels
        imageA.ctx.globalCompositeOperation = "lighter";
        imageA.ctx.drawImage(imageB, 0, 0);
        return imageA;
    }
    // zeros channels is its flag (red, green, blue) is true
    function zeroChannels(image, red, green, blue) { // set channels to zero to true
        image.ctx.fillStyle = `#${red ? "0" : "F"}${green ? "0" : "F"}${blue ? "0" : "F"}`;
        image.ctx.globalCompositeOperation = "multiply";
        image.ctx.fillRect(0, 0, image.width, image.height);
        return image;
    }
    // returns a new canvas that is a copy of imageA with pixels that are different from imageB marked in red.
    function markDifference(imageA, imageB) {
        const result = copyImage(imageA);
        const mask = invert( maxChannels( makeBW(  getDifference(imageA, imageB))));
        maskOut(result, mask);
        return addChannels(result,zeroChannels(invert(mask), false, true, true));
    }
    const images = [
        "https://i.sstatic.net/ImeHB.jpg",
        "https://i.sstatic.net/UrrnL.jpg"
      ];
      var imageCount = 0;
      function onImageLoad(){
        imageCount += 1;
        if(imageCount === 2){
          addImageToPage(markDifference(images[0],images[1]));
          addImageToPage(images[0]);
          addImageToPage(images[1]);
           
        }
      }
      function addImageToPage(image){
        image.className = "images";
        document.body.appendChild(image);
      }
      images.forEach((url, i) => {
        images[i] = new Image;
        images[i].src = url;
        images[i].onload = onImageLoad;
      });
    .images {
      width : 100%;
    }