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 len
is 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];
}
}
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.
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.
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%;
}