banner
Vinking

Vinking

你写下的每一个BUG 都是人类反抗被人工智能统治的一颗子弹

Let's add different theme colors to the website.

Recently, I wanted to create a feature to obtain the colors of the blog post header image, and I remembered a long time ago I wrote an article on how to extract image colors using the Median Cutting Algorithm.

Median Cutting Algorithm and K-Means Clustering Algorithm#

The median cutting algorithm is a method based on color histograms that recursively divides the color space into small cubes, with the color value of each small cube represented by the median color within that cube. This algorithm can produce significant color bias when the image color distribution is uneven, leading to inaccurate extracted colors. For example, if an image has a lot of red and a small amount of green, the extracted color will lean towards red. Additionally, when using the median cutting algorithm, the colors extracted from an image are always the same.

On the other hand, the K-Means clustering algorithm is better at handling cases where the image color distribution is uneven, and by using randomly initialized cluster centers, the extracted colors can vary each time.

Of course, due to the slower convergence speed of the K-Means clustering algorithm, a large image may take a longer time to process. However, for the size of blog images, this can be negligible. Like the median cutting algorithm, the K-Means clustering algorithm also draws the image on a canvas, which can lead to cross-origin issues that cause the canvas to be tainted.

Effect#

This is the effect after applying it to the website. If your browser supports displaying the theme color of the webpage, you will see the status bar color change to a random color from the header image.

Status Bar Color

Below is the real-time rendering of five colors extracted from the image using the K-Means clustering algorithm:

K-Means Clustering Algorithm JavaScript Code#

The main JavaScript code is as follows:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
fetch('{image address}')
    .then(response => response.blob())
    .then(blob => createImageBitmap(blob))
    .then(imageBitmap => {
        canvas.width = imageBitmap.width;
        canvas.height = imageBitmap.height;
        ctx.drawImage(imageBitmap, 0, 0);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;
        const pixels = Array.from({ length: data.length / 4 }, (_, i) => [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]]);
        const kMeans = new KMeans();
        const clusters = kMeans.cluster(pixels, 5);
        const colors = clusters.centroids;
        const colorsDiv = document.getElementById('colors');
        colors.forEach((color, items) => {
            const div = document.createElement('div');
            console.log(`Color ${items + 1}: rgb(${color[0]}, ${color[1]}, ${color[2]})`);
            div.style.backgroundColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
            colorsDiv.appendChild(div);
        });
    });

class KMeans {
    cluster(data, k) {
        const centroids = this._initCentroids(data, k);
        let oldClusters = [];
        let newClusters = [];
        const distancesMap = new Map();
        while (!this._clustersEqual(oldClusters, newClusters)) {
            oldClusters = newClusters;
            newClusters = Array.from({ length: k }, () => []);
            data.forEach(point => {
                let distances = distancesMap.get(point);
                if (!distances) {
                    distances = centroids.map(centroid => this._euclideanDistance(point, centroid));
                    distancesMap.set(point, distances);
                }
                const nearestCentroidIndex = distances.indexOf(Math.min(...distances));
                newClusters[nearestCentroidIndex].push(point);
            });
            centroids.forEach((centroid, i) => {
                const cluster = newClusters[i];
                if (cluster.length > 0) {
                    const [sumR, sumG, sumB] = cluster.reduce(([accR, accG, accB], [r, g, b]) => [accR + r, accG + g, accB + b], [0, 0, 0]);
                    centroids[i] = [sumR / cluster.length, sumG / cluster.length, sumB / cluster.length];
                }
            });
        }
        return { centroids, clusters: newClusters };
    }

    _initCentroids(data, k) {
        const shuffledData = this._shuffle(data);
        return shuffledData.slice(0, k);
    }

    _euclideanDistance(p1, p2) {
        const dR = p1[0] - p2[0];
        const dG = p1[1] - p2[1];
        const dB = p1[2] - p2[2];
        return Math.sqrt(dR * dR + dG * dG + dB * dB);
    }

    _shuffle(array) {
        const shuffledArray = [...array];
        for (let i = shuffledArray.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
        }
        return shuffledArray;
    }

    _clustersEqual(oldClusters, newClusters) {
        if (oldClusters.length !== newClusters.length) {
            return false;
        }
        for (let i = 0; i < oldClusters.length; i++) {
            if (oldClusters[i].length !== newClusters[i].length) {
                return false;
            }
            for (let j = 0; j < oldClusters[i].length; j++) {
                if (oldClusters[i][j] !== newClusters[i][j]) {
                    return false;
                }
            }
        }
        return true;
    }
}

Mini-batch K-Means Algorithm#

The mini-batch K-Means algorithm is an optimized version of the K-Means algorithm. Since the mini-batch K-Means algorithm only randomly selects a portion of the data for computation, it can reduce computational costs and process large datasets more quickly. However, because it only randomly selects a portion of the data, the results of the mini-batch K-Means algorithm may not be as accurate as those of the K-Means algorithm.

Below is the result optimized using the mini-batch K-Means algorithm:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
fetch('{image address}')
    .then(response => response.blob())
    .then(blob => createImageBitmap(blob))
    .then(imageBitmap => {
        canvas.width = imageBitmap.width;
        canvas.height = imageBitmap.height;
        canvas.classList.remove("animate-pulse");
        ctx.drawImage(imageBitmap, 0, 0);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;
        const pixels = Array.from({
            length: data.length / 4
        }, (_, i) => [data[i * 4], data[i * 4 + 1], data[i * 4 + 2]]);
        const kMeans = new KMeans();
        const clusters = kMeans.cluster(pixels, 5);
        const colors = clusters.centroids;
        const colorsDiv = document.getElementById('colors');
        colors.forEach((color, items) => {
            const div = document.createElement('div');
            console.log(`Color ${items + 1}: rgb(${color[0]}, ${color[1]}, ${color[2]})`);
            div.style.backgroundColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
            colorsDiv.appendChild(div);
        });
    });
class KMeans {
    cluster(data, k, batchSize = 100) {
        const centroids = this._initCentroids(data, k);
        let oldClusters = [];
        let newClusters = [];
        const distancesMap = new Map();
        while (!this._clustersEqual(oldClusters, newClusters)) {
            oldClusters = newClusters;
            newClusters = Array.from({
                length: k
            }, () => []);
            for (let i = 0; i < data.length; i += batchSize) {
                const batch = data.slice(i, i + batchSize);
                const batchDistances = new Map();
                batch.forEach(point => {
                    let distances = distancesMap.get(point);
                    if (!distances) {
                        distances = centroids.map(centroid => this._euclideanDistance(point, centroid));
                        distancesMap.set(point, distances);
                    }
                    batchDistances.set(point, distances);
                });
                batch.forEach(point => {
                    const distances = batchDistances.get(point);
                    const nearestCentroidIndex = distances.indexOf(Math.min(...distances));
                    newClusters[nearestCentroidIndex].push(point);
                });
            }
            centroids.forEach((centroid, i) => {
                const cluster = newClusters[i];
                if (cluster.length > 0) {
                    const [sumR, sumG, sumB] = cluster.reduce(([accR, accG, accB], [r, g, b]) => [accR + r, accG + g, accB + b], [0, 0, 0]);
                    centroids[i] = [sumR / cluster.length, sumG / cluster.length, sumB / cluster.length];
                }
            });
        }
        return {
            centroids,
            clusters: newClusters
        };
    }
    _initCentroids(data, k) {
        const shuffledData = this._shuffle(data);
        return shuffledData.slice(0, k);
    }
    _euclideanDistance(p1, p2) {
        const dR = p1[0] - p2[0];
        const dG = p1[1] - p2[1];
        const dB = p1[2] - p2[2];
        return Math.sqrt(dR * dR + dG * dG + dB * dB);
    }
    _rgbToHex(color) {
        let values = color.replace(/rgba?\(/, '').replace(/\)/, '').replace(/[\s+]/g, '').split(',');
        let a = parseFloat(values[3] || 1),
            r = Math.floor(a * parseInt(values[0]) + (1 - a) * 255),
            g = Math.floor(a * parseInt(values[1]) + (1 - a) * 255),
            b = Math.floor(a * parseInt(values[2]) + (1 - a) * 255);
        return "#" + ("0" + r.toString(16)).slice(-2) + ("0" + g.toString(16)).slice(-2) + ("0" + b.toString(16)).slice(-2);
    }
    _shuffle(array) {
        const shuffledArray = [...array];
        for (let i = shuffledArray.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
        }
        return shuffledArray;
    }
    _clustersEqual(oldClusters, newClusters) {
        if (oldClusters.length !== newClusters.length) {
            return false;
        }
        for (let i = 0; i < oldClusters.length; i++) {
            const oldCentroid = oldClusters[i].centroid;
            const newCentroid = newClusters[i].centroid;
            const dist = distance(oldCentroid, newCentroid);
            if (dist > threshold) {
                return false;
            }
        }
        return true;
    }
}

This article is updated synchronously to xLog by Mix Space. The original link is https://www.vinking.top/posts/codes/median-cut-k-means-algorithms-javascript-implementation

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.