0

I want to loop over about 50 images in HTML, extract the src from each, check if the dominant color of the image background is white, then based on the results add some css styling (e.g padding).

So far I have this code however for some reason it's not working. The code works separately but when placed in a for loop it doesn't work. Usually, this loop either doesn't work at all or works only till some point then just feeds out a default result of "#FFFFFF" because the canvas itself is filled white.

I'm not sure why it's not working. I've been trying to fix it but to no avail.

DEMO HERE (please look through) : https://jsbin.com/tucegomemi/1/edit?html,js,console,output

JAVASCRIPT HERE :

var i;
var GlobalVariable;
for (i=0; i < document.querySelectorAll('img').length ; i++) {
  
let canvas = document.getElementById("canvas"),
              canvasWidth = canvas.width,
        canvasHeight = canvas.height,
        c = canvas.getContext("2d"),
        img = new Image();
        img.crossOrigin="anonymous";
img.src = document.querySelectorAll('img')[i].src


      // Prepare the canvas
      var ptrn = c.createPattern(img, 'repeat'); 
      c.fillStyle = "white";
      c.fillRect(0,0,canvasWidth,canvasHeight);
      c.fillStyle = ptrn;
      c.fillRect(0,0,canvasWidth,canvasHeight);
      
      // Get img data
      var imgData = c.getImageData(0, 0, canvasWidth, canvasHeight),
          data = imgData.data,
          colours = {};

      
      // Build an object with colour data.
      for (var y = 0; y < canvasHeight; ++y) {
        for (var x = 0; x < canvasWidth; ++x) {
          var index = (y * canvasWidth + x) * 4,
              r = data[index],
              g = data[++index],
              b = data[++index],
              rgb = rgbToHex(r,g,b);
          
          if(colours[rgb]){
            colours[rgb]++;
          }else{
            colours[rgb] = 1;
          }
        }
      }
      
      // Determine what colour occurs most.
      var most = {
        colour:'',
        amount:0
      };
      for(var colour in colours){
        if(colours[colour] > most.amount){
          most.amount = colours[colour];
          most.colour = colour;
        }
      }

     GlobalVariable =  most.colour; 
     console.log(i);  
     console.log(GlobalVariable);  

  if (GlobalVariable !== "#ffffff") {document.querySelectorAll('img')[i].style.padding = "50px" ;}  

}
    
    function rgbToHex(r, g, b) {
      return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
    }





Filburt
  • 17,626
  • 12
  • 64
  • 115
Ani
  • 328
  • 1
  • 4
  • 13

2 Answers2

3

Wait for images to load

As the images are on the page you can wait for the page load event to fire. This will fire only when all the images have loaded (or fail to load). Do read the link as there are some caveats to using the load event.

Also as the images are on the page there is no need to create a copy of the image using new Image You can use the image directly from the page.

Also I assume that all images will load. If image do not load there will be problems

Looking at your code it is horrifically inefficient, thus the example is a complete rewrite with an attempt to run faster and chew less power.

Note: that the example uses a temp canvas that is in memory only. It does not need a canvas on the page. Note: that it stop counting if a pixel has a count greater than half the number of pixels in the image.

addEventListener("load",() => {         // wait for page (and images to load) 
    const toHex = val => (val & 0xFF).toString(16).padStart(2,"0");  // mask the to hex and pad with 0 if needed
    const pixel2CSScolor = px => `#${toHex(px >> 16)}${toHex(px >> 8)}${toHex(px)}`;
    const images = document.querySelectorAll('img');
    const canvas = document.createElement("canvas"); // Only need one canvas
    const ctx = canvas.getContext("2d");             // and only one context
    for (const image of images) {                    // do each image in turn
        const w = canvas.width = image.naturalWidth; // size to fit image
        const h = canvas.height = image.naturalHeight;
        ctx.fillStyle = "#FFF";
        ctx.fillRect(0, 0, w, h);
        ctx.drawImage(image, 0, 0);
        const imgData = ctx.getImageData(0, 0, w, h);
        const pixels = new Uint32Array(imgData.data.buffer); // get a pixel view of data (RGBA as one number)
        const counts = {};                                   // a map of color counts
        var idx = pixels.length, maxPx, maxCount = 0;        // track the most frequent pixel count and type
        while (idx-- > 0) {
            const pixel = pixels[idx];  // get pixel
            const count = counts[pixel] = counts[pixel] ? counts[pixel] + 1 : 1;
            if (count > maxCount) {
                maxCount = count;
                maxPx = pixel;
                if (count > pixels.length / 2) { break }
            }
        }
        image._FOUND_DOMINATE_COLOR = pixel2CSScolor(maxPx);
    }
}); 

Each image has a new property attached called _FOUND_DOMINATE_COLOR which hold a string with the colour as a CSS hex color

An even better way

As I am unsure of the image format and the content of the image the above example is the cover all solution.

If the images have large areas of similar color, or the image has a lot of noise you can use the GPU rendering to do most of the counting for you. This is done by drawing the image at progressively smaller scales. The drawImage function will average the pixel values as it does.

This means that when your code looks at the pixel data the is a lot less, Half the image size and there is 4 times less memory and CPU load, quarter the size and 16 times less work.

The next example reduces the image to 1/4 its natural size and then uses the averaged pixel values to find the color. Note that for the best results the images should be at least larger than 16 pixels in width and height

addEventListener("load",() => {        
    const toHex = val => (val & 0xFF).toString(16).padStart(2,"0");  
    const pixel2CSScolor = px => `#${toHex(px >> 16)}${toHex(px >> 8)}${toHex(px)}`;
    const reduceImage = image => {
        const w = canvas.width = image.naturalWidth;         
        const h = canvas.height = image.naturalHeight;
        ctx.globalCompositeOperation = "source-over";
        ctx.fillStyle = "#FFF";
        ctx.fillRect(0, 0, w, h);
        ctx.drawImage(image, 0, 0);
        ctx.globalCompositeOperation = "copy";
        ctx.drawImage(canvas, 0, 0, w / 2, h / 2);
        ctx.drawImage(canvas, 0, 0, w / 2, h / 2,  0, 0, w / 4, h / 4);
        return new Uint32Array(ctx.getImageData(0, 0, w / 4 | 0, h / 4 | 0).data.buffer);
    }
    const images = document.querySelectorAll('img');
    const canvas = document.createElement("canvas"); 
    const ctx = canvas.getContext("2d");             
    for (const image of images) {                    
        const pixels = reduceImage(image), counts = {};                                   
        var idx = pixels.length, maxPx, maxCount = 0;       
        while (idx-- > 0) {
            const pixel = pixels[idx];  // get pixel
            const count = counts[pixel] = counts[pixel] ? counts[pixel] + 1 : 1;
            if (count > maxCount) {
                maxCount = count;
                maxPx = pixel;
                if (count > pixels.length / 2) { break }
            }
        }
        image._FOUND_DOMINATE_COLOR = pixel2CSScolor(maxPx);
    }
}); 

Update

As there were some questions in the comments the next snippet is a check to make sure all is working.

I could not find any problems with the code apart from the correction I detailed in the comments.

I did change some names and increased the image reduction steps a lot more for reasons outlined under the next heading

Color frequency does not equal dominate color

The example below shows two images, when loaded the padding is set to the color found. You will note that the image on the right does not seem to get the color right.

This is because there are many browns yet no one brown is the most frequent.

In the my answer Finding dominant hue. I addressed the problem and found a solution that is more in tune with human perception.

Working example

. Warning for low end devices. one of the images is ~9Mpx .

addEventListener("load",() => { geMostFrequentColor() },{once: true});
const downScaleSteps = 4;
function geMostFrequentColor() {
    const toHex = val => (val & 0xFF).toString(16).padStart(2,"0");  
    const pixel2CSScolor = px => `#${toHex(px >> 16)}${toHex(px >> 8)}${toHex(px)}`;
    const reduceImage = image => {
        var w = canvas.width = image.naturalWidth, h = canvas.height = image.naturalHeight, step = 0;
        ctx.globalCompositeOperation = "source-over";
        ctx.fillStyle = "#FFF";
        ctx.fillRect(0, 0, w, h);
        ctx.drawImage(image, 0, 0);
        ctx.globalCompositeOperation = "copy";
        while (step++ < downScaleSteps) {
            ctx.drawImage(canvas, 0, 0, w, h, 0, 0, w /= 2, h /= 2);
        }
        return new Uint32Array(ctx.getImageData(0, 0, w | 0, h | 0).data.buffer);
    }
    const images = document.querySelectorAll('img');
    const canvas = document.createElement("canvas"); 
    const ctx = canvas.getContext("2d");  
    var imgCount = 0;           
    for (const image of images) {           
        info.textContent = "Processing image: " + imgCount++;
        const pixels = reduceImage(image), counts = {};    
        let idx = pixels.length, maxPx, maxCount = 0;      
        while (idx-- > 0) {
            const pixel = pixels[idx];  // get pixel
            const count = counts[pixel] = counts[pixel] ? counts[pixel] + 1 : 1;
            if (count > maxCount) {
                maxCount = count;
                maxPx = pixel;
                if (count > pixels.length / 2) { break }
            }
        }
        image._MOST_FREQUENT_COLOR = pixel2CSScolor(maxPx);
        image.style.background = image._MOST_FREQUENT_COLOR;
    }
    info.textContent = "All Done!";
}
img {
   height: 160px;
   padding: 20px;
}
<div id="info">Loading...</div>
<img src="https://upload.wikimedia.org/wikipedia/commons/d/dd/Olympus-BX61-fluorescence_microscope.jpg"  crossorigin="anonymous">
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a5/Compound_Microscope_(cropped).JPG" alt="Compound Microscope (cropped).JPG"  crossorigin="anonymous"><br>
 Images from wiki no attribution required.
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Awesome! Thank you soo much. You did great! This will be really helpful for e-commerce stores figuring out if their product images background. The only issue I'm facing is that I am getting the results as this string '#{toHex(px >> 16)}{toHex(px >> 8)}{toHex(px)}' or 'Failed to execute 'drawImage' error. (DEMO-> https://jsbin.com/xipapuledu/1/edit?html,js,console,output) I'm not sure why.. Is there a quick fix for this? : ) – Ani Oct 09 '20 at 22:47
  • @Ani Sorry my bad. should be `const toHex = val => (val & 0xFF).toString(16).padStart(2,"0"); const pixel2CSScolor = px => `#${toHex(px >> 16)}${toHex(px >> 8)}${toHex(px)}`; Forgot the `$` in the template string and the `0x` for the hex value in the first line. Will fix answer now. ` – Blindman67 Oct 09 '20 at 22:50
  • It's listing as HEX code now!! Great! here -> https://jsbin.com/zapodavowo/1/edit?html,js,console,output ^_^ .. It's just that now all the values are #FFFFFF which shouldn't be the case. Sorry for the bother. There's not a lot of stack flow answers for this problem and if another small business owner comes across this, I would want it to help them also : ) Thanks! – Ani Oct 09 '20 at 23:06
  • @Ani OK I did not test the code so as soon as I get a chance I will get it working 100%. – Blindman67 Oct 09 '20 at 23:10
  • Bless!! That would be amazing! Thank you! whenever you get the chance ^_^ – Ani Oct 09 '20 at 23:11
  • @Ani I could not find any other problems. However the color that is the most frequent is not always the dominate color. Adding a solid white to the image will almost always return white. 24bit color has ~16million different possible values thus you can have a 4K image with only 2 white pixels being the most frequent.; To make sure code works and to illustrate color frequency problem I have updated the answer with an example . I have also added a link related to finding dominate color. – Blindman67 Oct 10 '20 at 00:43
1

You're currently drawing an empty image. The image needs some time to load so you'll have to wait for that to happen.

Use the onload callback to draw the image to the canvas as soon as it has finished loading. Every other process should continue after this event.

img = new Image();
img.crossOrigin = "anonymous";
img.src = document.querySelectorAll('img')[i].src;
img.onload = function() {
  c.clearRect(0, 0, canvasWidth, canvasHeight);
  c.drawImage(img, 0, 0, canvasWidth, canvasHeight); 
  // Continue here
}
Emiel Zuurbier
  • 19,095
  • 3
  • 17
  • 32