Skip to content

Instantly share code, notes, and snippets.

@kitgrose
Created July 18, 2024 04:49
Show Gist options
  • Save kitgrose/fc52d624ca1e18103f8399e6e13b7cbf to your computer and use it in GitHub Desktop.
Save kitgrose/fc52d624ca1e18103f8399e6e13b7cbf to your computer and use it in GitHub Desktop.
Compute colour hot track for image as per Windows 7 taskbar highlights
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image hot track algorithm</title>
</head>
<body>
<input type="file" id="fileinput" accept="image/*" />
<script>
/*
Attempts to implement the algorithm described in an answer by Ben N on Super User Stack Exchange:
https://superuser.com/a/1039488
Doesn't seem to exactly reflect the behaviour in Windows 7, but is fairly close.
*/
document.getElementById('fileinput').addEventListener('change', fileSelectedEvt => {
const file = fileSelectedEvt.target.files[0];
if (!file.type) {
console.error('The File.type property does not appear to be supported on this browser.');
return;
}
if (!file.type.match('image.*')) {
console.error('The selected file does not appear to be an image.');
return;
}
const reader = new FileReader();
reader.addEventListener('load', fileLoadEvent => {
const image = new Image();
image.src = fileLoadEvent.target.result;
image.addEventListener('load', imageLoadEvent => {
const canvas = makeCanvasForImage(imageLoadEvent.target);
const histogram = computeHistogramForCanvasContext(canvas.getContext('2d'));
const hotTrackColour = getHotTrackColourForHistogram(histogram);
console.log("Hot track colour: ", hotTrackColour);
document.body.style.backgroundColor = `rgb(${hotTrackColour.r}, ${hotTrackColour.g}, ${hotTrackColour.b})`;
});
});
reader.readAsDataURL(file);
});
function makeCanvasForImage(img) {
if (!(img.width > 0 && img.height > 0)) {
console.error('Image dimensions could not be determined');
return;
}
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return canvas;
}
function computeHistogramForCanvasContext(ctx) {
const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height).data;
let histogram = [[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]];
// ImgData is an array of Uint8s in RGBA order
// Each pixel is 4 elements in the array
// Walk array in 4s to get each pixel
for(let i = 0; i < imgData.length; i += 4) {
const [r, g, b, a] = imgData.slice(i, i + 4);
if (a === 0) {
// Pixel is transparent.
continue;
}
const rBucket = getBucketForChannel(r);
const gBucket = getBucketForChannel(g);
const bBucket = getBucketForChannel(b);
if (rBucket == gBucket && rBucket == bBucket) {
// Pixel is too grey to count.
continue;
}
histogram[rBucket][gBucket][bBucket].push({ r: r, g: g, b: b });
}
console.log("Histogram totals by bucket:")
for (let r = 0; r < 3; r++) {
console.log(`${String(histogram[r][0][0].length).padStart(4, '0')} ${String(histogram[r][0][1].length).padStart(4, '0')} ${String(histogram[r][0][2].length).padStart(4, '0')}\n${String(histogram[r][1][0].length).padStart(4, '0')} ${String(histogram[r][1][1].length).padStart(4, '0')} ${String(histogram[r][1][2].length).padStart(4, '0')}\n${String(histogram[r][2][0].length).padStart(4, '0')} ${String(histogram[r][2][1].length).padStart(4, '0')} ${String(histogram[r][2][2].length).padStart(4, '0')}`);
}
return histogram;
}
function getBucketForChannel(channelValue) {
const maxDark = 60;
const maxMedium = 200;
if (channelValue < maxDark) {
return 2;
}
if (channelValue < maxMedium) {
return 1;
}
return 0;
}
// Histogram is a 3x3x3 array of pixel data arrays.
// Function finds bucket with most pixels, then picks the centre pixel of that bucket.
function getHotTrackColourForHistogram(histogram) {
const defaultHighlight = { r: 116, g: 184, b: 252 };
let maxBucket = null;
let maxBucketCount = 0;
for (let r = 0; r < 3; r++) {
for (let g = 0; g < 3; g++) {
for (let b = 0; b < 3; b++) {
const bucket = histogram[r][g][b];
if (bucket.length > maxBucketCount) {
maxBucket = bucket;
maxBucketCount = bucket.length;
}
}
}
}
if (!maxBucket) {
console.error('No non-grey pixels found in image. Returning default highlight.');
return defaultHighlight;
}
/*
// Alternate colour selection algorithm within the bucket.
// Finds the most represented colour in that bucket.
let colourCounts = [];
let maxCount = 0;
let maxColour = null;
for (let i = 0; i < maxBucket.length; i++) {
const pixel = maxBucket[i];
const colour = `${pixel.r},${pixel.g},${pixel.b}`;
if (!colourCounts[colour]) {
colourCounts[colour] = 0;
}
colourCounts[colour]++;
if (colourCounts[colour] > maxCount) {
maxCount = colourCounts[colour];
maxColour = pixel;
}
}
if (maxColour !== null) {
return maxColour;
}
return defaultHighlight;
*/
// Sort pixels in bucket by distance from centre of colour space.
// This tries to avoid getting colours just outside the grey range.
maxBucket.sort((a, b) => {
const aDist = Math.sqrt(Math.pow(a.r - 128, 2) + Math.pow(a.g - 128, 2) + Math.pow(a.b - 128, 2));
const bDist = Math.sqrt(Math.pow(b.r - 128, 2) + Math.pow(b.g - 128, 2) + Math.pow(b.b - 128, 2));
return aDist - bDist;
});
const centrePixelIndex = Math.floor(maxBucket.length / 2);
return maxBucket[centrePixelIndex];
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment