媒介
前段时间在 Halo 的 应用市场 中碰到希望主题和插件的封面图配景色为封面图主色的问题,于是乎需要根据封面图提取主色就想到使用 K-Means 算法来提取。
在图像处置处罚中,图像是由像素点构成的,每个像素点都有一个颜色值,颜色值通常由 RGB 三个分量组成。因此,我们可以将图像看作是一个由颜色值构成的点云,每个点代表一个像素点。
为了更好地理解,我们可以将图像的颜色值可视化为一个 Scatter 3D 图。在 Scatter 3D 图中,每个点的坐标由 RGB 三个分量组成,点的颜色与其坐标对应的颜色值雷同。
图像的颜色值量化
以下面的图片为例
它的色值分布为如下的图像
从上述 RGB 3D Scatter Plot 图假如将相似的颜色值归为一类可以看出图像大概有三种主色调蓝色、绿色和粉色:
假如我们从三簇中各选一个中心,如以 A、B、C三点表现 A(50, 150, 200)、B(240, 150, 200)、C(50, 100, 50) 并将每个数据点分配到最近的中心地点的簇中这个过程称之为聚类而这个中心称之为聚类中心,这样就可以得到 K 个以聚类中心为坐标的主色值。而 K-Means 算法是一种常用的聚类算法,它的根本思想就是将数据集分成 K 个簇,每个簇的中心点称为聚类中心,将每个数据点分配到最近的聚类中心地点的簇中。
K-Means 算法的实现过程如下:
- 初始化聚类中心:随机选择 K 个点作为聚类中心。
- 分配数据点到最近的聚类中心地点的簇中:对于每个数据点,盘算它与每个聚类中心的间隔,将它分配到间隔最近的聚类中心地点的簇中。
- 更新聚类中心:对于每个簇,盘算它的所有数据点的均匀值,将这个均匀值作为新的聚类中心。
- 重复步骤 2 和步骤 3,直到聚类中心不再改变或达到最大迭代次数。
在图像处置处罚中,我们可以将每个像素点的颜色值看作是一个三维向量,使用欧几里得间隔盘算两个颜色值之间的间隔。对于每个像素点,我们将它分配到间隔最近的聚类中心地点的簇中,然后将它的颜色值更换为地点簇的聚类中心的颜色值,如 A1(10, 140, 170) 以间隔它最近的间隔中心 A 的坐标表现即 A1 = A(50, 150, 200)。这样,我们就可以将图像中的颜色值进行量化,将相似的颜色值归为一类。
最后,我们可以根据聚类中心的颜色值,盘算每个颜色值在图像中出现的次数,并按出现次数从大到小排序,取前几个颜色作为主要颜色。
- <script>
- const img = new Image();
- img.src = "https://guqing-blog.oss-cn-hangzhou.aliyuncs.com/image.jpg";
- img.setAttribute("crossOrigin", "");
- img.onload = function () {
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- canvas.width = img.width;
- canvas.height = img.height;
- ctx.drawImage(img, 0, 0);
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const data = imageData.data;
- const k = 3; // 聚类数
- const centers = quantize(data, k);
- console.log(centers)
- for (const color of centers) {
- const div = document.createElement("div");
- div.style.width = "50px";
- div.style.height = "50px";
- div.style.backgroundColor = color;
- document.body.appendChild(div);
- }
- };
- function quantize(data, k) {
- // 将颜色值转换为三维向量
- const vectors = [];
- for (let i = 0; i < data.length; i += 4) {
- vectors.push([data[i], data[i + 1], data[i + 2]]);
- }
- // 随机选择 K 个聚类中心
- const centers = [];
- for (let i = 0; i < k; i++) {
- centers.push(vectors[Math.floor(Math.random() * vectors.length)]);
- }
- // 迭代更新聚类中心
- let iterations = 0;
- while (iterations < 100) {
- // 分配数据点到最近的聚类中心所在的簇中
- const clusters = new Array(k).fill().map(() => []);
- for (let i = 0; i < vectors.length; i++) {
- let minDist = Infinity;
- let minIndex = 0;
- for (let j = 0; j < centers.length; j++) {
- const dist = distance(vectors[i], centers[j]);
- if (dist < minDist) {
- minDist = dist;
- minIndex = j;
- }
- }
- clusters[minIndex].push(vectors[i]);
- }
- // 更新聚类中心
- let converged = true;
- for (let i = 0; i < centers.length; i++) {
- const cluster = clusters[i];
- if (cluster.length > 0) {
- const newCenter = cluster
- .reduce((acc, cur) => [
- acc[0] + cur[0],
- acc[1] + cur[1],
- acc[2] + cur[2],
- ])
- .map((val) => val / cluster.length);
- if (!equal(centers[i], newCenter)) {
- centers[i] = newCenter;
- converged = false;
- }
- }
- }
- if (converged) {
- break;
- }
- iterations++;
- }
- // 将每个像素点的颜色值替换为所在簇的聚类中心的颜色值
- for (let i = 0; i < data.length; i += 4) {
- const vector = [data[i], data[i + 1], data[i + 2]];
- let minDist = Infinity;
- let minIndex = 0;
- for (let j = 0; j < centers.length; j++) {
- const dist = distance(vector, centers[j]);
- if (dist < minDist) {
- minDist = dist;
- minIndex = j;
- }
- }
- const center = centers[minIndex];
- data[i] = center[0];
- data[i + 1] = center[1];
- data[i + 2] = center[2];
- }
- // 计算每个颜色值在图像中出现的次数,并按出现次数从大到小排序
- const counts = {};
- for (let i = 0; i < data.length; i += 4) {
- const color = `rgb(${data[i]}, ${data[i + 1]}, ${data[i + 2]})`;
- counts[color] = counts[color] ? counts[color] + 1 : 1;
- }
- const sortedColors = Object.keys(counts).sort(
- (a, b) => counts[b] - counts[a]
- );
- // 取前 k 个颜色作为主要颜色
- return sortedColors.slice(0, k);
- }
- function distance(a, b) {
- return Math.sqrt(
- (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2
- );
- }
- function equal(a, b) {
- return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
- }
- </script>
复制代码 主动选取 K 值
在现实应用中,我们大概不知道应该选择多少个聚类中心,即 K 值。一种常用的方法是使用 Gap 统计量法,它的根本思想是比力聚类效果与随机数据集的聚类效果之间的差异,选择使差异最大的 K 值。
Gap 统计量法的实现过程如下:
- 对原始数据集进行 K-Means 聚类,得到聚类效果。
- 生成 B 个随机数据集,对每个随机数据集进行 K-Means 聚类,得到聚类效果。
- 盘算聚类效果与随机数据集聚类效果之间的差异,使用 Gap 统计量表现。
- 选择使 Gap 统计量最大的 K 值。
下面是使用 JavaScript 实现 Gap 统计量法的示例代码:
- function gap(data, maxK) {
- const gaps = [];
- for (let k = 1; k <= maxK; k++) {
- const quantized = quantize(data, k);
- const gap = logWk(quantized) - logWk(randomData(data.length));
- gaps.push(gap);
- }
- const maxGap = Math.max(...gaps);
- return gaps.findIndex((gap) => gap === maxGap) + 1;
- }
- function logWk(quantized) {
- const counts = {};
- for (let i = 0; i < quantized.length; i++) {
- counts[quantized[i]] = counts[quantized[i]] ? counts[quantized[i]] + 1 : 1;
- }
- const n = quantized.length;
- const k = Object.keys(counts).length;
- const wk = Object.values(counts).reduce((acc, cur) => acc + cur * Math.log(cur / n), 0);
- return Math.log(n) + wk / n;
- }
- function randomData(n) {
- const data = new Uint8ClampedArray(n * 4);
- for (let i = 0; i < data.length; i++) {
- data[i] = Math.floor(Math.random() * 256);
- }
- return data;
- }
复制代码 使用:
- const k = gap(data, 10)
- // const k = 3; // 聚类数
- const centers = quantize(data, k);
复制代码 好吧,挺贫困的,最终直接将封面图再作为配景图添加 backdrop-filter 来实现了 |