Created
July 18, 2024 04:49
-
-
Save kitgrose/fc52d624ca1e18103f8399e6e13b7cbf to your computer and use it in GitHub Desktop.
Compute colour hot track for image as per Windows 7 taskbar highlights
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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