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.