最近想把博客的前端翻新一下,换一套渲染方案,其中有一需求就是把博客文章头图的颜色进行分析,提取出图像的 主色 用来做当前文章的 theme , 举个例子:
:root{
--siroi-primary-color: #ffee6f;
--siroi-bg-color: #495057;
--siroi-color-6: var(--siroi-primary-color);
--siroi-color-1: color-mix(in srgb, var(--siroi-color-6) 0%, white);
--siroi-color-2: color-mix(in srgb, var(--siroi-color-6) 25%, white);
--siroi-color-3: color-mix(in srgb, var(--siroi-color-6) 50%, white);
--siroi-color-4: color-mix(in srgb, var(--siroi-color-6) 70%, white);
--siroi-color-5: color-mix(in srgb, var(--siroi-color-6) 100%, white);
--siroi-font-color: rgb(from var(--siroi-bg-color) calc(255 - r) calc(255 - g) calc(255 - b));
}
我有如上默认的主题变量,当我的文章有头图的时候,我可以把 primary-color 和 bg-color 根据图片的色彩动态的去做调整。
在图形图像领域,有很多算法去解 颜色提取 的问题,常见的有 K-means , 八叉树 等,本文先使用前者进行提取,八叉树后续进行对比。
什么是 K-means
K-means 是一种无监督的机器学习算法,通过样本间的相似性对数据进行归类,使类内差距最小化,类间差距最大化,将数据分成若干组。
实现 K-means
以下的实现方案均基于 typescript ,参考了 python 和 c++ 以及感谢 豆包 的帮助 。
随机初值的确定
初值的选择会最终影响算法的结果,在 K-means++ 算法中调整了初值的选择从而加速收敛, 本文采用简单的随机初值,即随机打散数据取前 K 个值。随机算法采用 Fisher-Yates,感谢 @zeroToHero 的实现:
/**
* @param array
* @see https://rzx007.github.io/nav/html-dom/ts-shuffle
* @returns
*/
static shuffle<T>(array: T[]): T[] {
// 创建一个数组的副本,避免修改原数组
const shuffledArray = [...array];
let currentIndex = shuffledArray.length;
// 当还有未处理的元素时
while (currentIndex !== 0) {
// 随机选择一个未处理的元素
const randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// 交换当前元素和随机选择的元素
[shuffledArray[currentIndex], shuffledArray[randomIndex]] = [
shuffledArray[randomIndex],
shuffledArray[currentIndex],
];
}
return shuffledArray;
}
颜色归类
距离函数的实现 是我任务在 K-means 比较重要的一步了,向量距离的度量 决定了最终结果。在色彩领域,色差 一直都是一个很有意思的课题,业界知名的 ant design 就采用 CIE1976 色彩空间,用 欧几里得距离 来计算该空间不同颜色的差异。
真正讨论色差问题也有很多论文可以借鉴,其他算法本文不在论述,本文为了简单就用 rgb 色彩并直接对齐加权计算:
具体实现如下:豆包改的位运算😋
private colorDistance(c1: Color, c2: Color) {
const dr = c1.r - c2.r;
const dg = c1.g - c2.g;
const db = c1.b - c2.b;
const wr = c1.r + c2.r;
const wrShift = wr >> 8;
const term1 = ((2 << 8) + wrShift) * dr * dr;
const term2 = (4 << 8) * dg * dg;
const term3 = ((2 << 8) + ((255 - wr) >> 8)) * db * db;
const wd = (term1 + term2 + term3) >> 8;
return Math.sqrt(wd);
}
更新聚类中心
这步也简单实现,即把所有颜色的 r, g, b 依次相加求平均
private updateCenters(clusters: Cluster[]): Color[] {
return clusters.map((cluster) => {
if (cluster.pixels.length === 0) return cluster.center;
const sum = cluster.pixels.reduce(
(acc, pixel) => ({
r: acc.r + pixel.r,
g: acc.g + pixel.g,
b: acc.b + pixel.b,
}),
{ r: 0, g: 0, b: 0 }
);
return {
r: Math.round(sum.r / cluster.pixels.length),
g: Math.round(sum.g / cluster.pixels.length),
b: Math.round(sum.b / cluster.pixels.length),
};
});
}
在小样本数据下,最大迭代 100 次,如果提取收敛则中止,否则就用最后一次迭代结果。
最终效果如下:
下一步便是如何选择提取出的颜色和应用了,我们下篇见。
参考链接