当前位置: 首页>>技术教程>>正文


使用kmeans聚类从图像中提取颜色

 

问题

想编写一个程序,使我可以从图像中提取一组颜色,希望考虑到人类的感知并以一种看起来自然的方式进行操作。配色方案通常可以刻画出整个图像的“氛围”,因此我认为能够从图像提取色彩有趣而且有用。

所以……我花了一些时间思考可以做些什么。我设计了一些相当简单的算法,例如,将图像有规则地切成块,然后输出每个部分的平均颜色。也许还可以添加额外的图层,在这些图层中将块进行比较并组合成组,或者将每种颜色与另一种颜色递归组合,直到达到所需的颜色数量为止。这些基于规则的做法都不太理想,不过我很快发现,这个问题已经有通用的解决方案了,而且通用方案是相当有效的。

那么从图像提取颜色的通用方法是什么呢?请看下文详解。

解决方案

K-means聚类就是一种解决此类问题的不错方法,通过该方法可以将一组数据点划分为几个不相交的子集,其中每个子集中的点被视为彼此“接近”(根据某种度量标准)。当处理几何空间中点时,标准欧几里得距离函数是一个常用度量。事实证明,这种方法正是我们需要的从图像提取一组颜色的方法。

具体案例

在我们的案例中,“数据点”是颜色,而距离函数则是两种颜色“有多么不同”的度量。我们的任务是将这些颜色分组为给定数量的集合,然后计算每个集合的平均颜色。在每个集合中使用均值似乎是一个相当明智的选择。当然,我们也可以改用任何其他统计量度(众数,中位数或其他任何形式!),也可能得到不错的结果。

以下程序使用JavaScript!代码编写


数据点

每个数据点都是一种颜色,可以表示为RGB色彩空间

在JavaScript中,单个数据点可能看起来像这样:

// An array if we want to be general
let colour = [100,168,92];// Or an object if we want to be more explicit
let colour = {red: 100, green: 168, blue: 92};

距离函数

由于我们希望能够计算两种颜色的相似程度,因此我们需要一个函数。一个简单的选择就是使用每种颜色的成分值来计算欧式距离。

我们的距离函数如下所示:

// Distance function
function euclideanDistance(a, b) {
    let sum = 0;
    for (let i = 0; i < a.length; i++) {
        sum += Math.pow(b[i] — a[i], 2);
    }
    return Math.sqrt(sum);
}

由于我们尚未在函数中指定固定数量的组件,因此该组件可用于n-dimensional数据点(具有n个分量)。如果以后我们要用其他方式表示颜色,这很有用。

算法

用于k-means聚类的最常见算法称为劳埃德(Lloyd)算法(尽管通常简称为k-means算法)。我们将在这里使用该算法。

我们将创建一组称为“质心”的对象,每个对象都定义一个唯一的簇。

一个质心与之相关的两件事:

  • 数据集范围内的一个点(质心的位置)
  • 数据集中的一组数据点(质心簇中的点)

该算法包括三个主要步骤:

1。初始化。选择质心的初始值。在这种情况下,我们只为每个簇选择一个随机点作为质心。

2。分配。将每个数据点分配给离簇均值(质心)距离最小的聚类。

3。更新。将每个质心设置为与其相关联的所有数据点的均值(即质心的所在的簇中点的均值)。

该算法将执行初始化一次,然后执行分配更新并依次重复,直到算法收敛为止。

当分配质心不再有任何变化时,该算法“收敛”。

辅助函数

让我们定义一些辅助函数:用于计算给定数据集范围的函数,另一个用于在给定范围内生成随机整数的函数。 n-dimensional数据集的“范围”——即每个维度对应一个范围。

// Calculate range of a one-dimensional data set
function rangeOf(data) {
    return {min: Math.min(data), max: Math.max(data)};
}

// Calculate range of an n-dimensional data set
function rangesOf(data) {
    let ranges = [];    for (let i = 0; i < data[0].length; i++) {
        ranges.push(rangeOf(data.map(x => x[i])));
    }    return ranges;
}

// Generate random integer in a given closed interval
function randomIntBetween(a, b) {
    return Math.floor(Math.random() * (b - a + 1)) + a;
}

有了上述准备之后,可以给算法的三个步骤编写代码了。


第一步-初始化

对于所需的每个质心(假设质心数为k),我们在提供的数据集范围内生成一个随机的整数值点,并将其附加到数组中。数组中的每个点代表一个质​​心的位置。当然,质心与数据的维数相同。

function initialiseCentroidsRandomly(data, k) {
    let ranges = rangesOf(data);
    let centroids = [];
    for (let i = 0; i < k; i++) {
        let centroid = [];
        for (let r in ranges) {
            centroid.push(
            randomIntBetween(ranges[r].min, ranges[r].max));
        }        
        centroids.push(centroid);
    }   
    return centroids;
}

第二步-分配

这是我们将数据点分配给簇的阶段。对于每个点,我们选择距离最短的质心,然后将点附加到对应的簇。

注:这里我们使用了数组的Map功能以及箭头功能如果您不太确定发生了什么,请点击链接快速浏览一下。

function clusterDataPoints(data, centroids) {
    let clusters = [];
    centroids.forEach(function() {
        clusters.push([]);
    });    
    data.forEach(function(point) {    
        let nearestCentroid = Math.min(
        centroids.map(x => euclideanDistance(point, x)));    
        clusters[centroids.indexOf(nearestCentroid)].push(point);
    });    
    return clusters;
}

第三步-更新

对于每个簇,我们计算其内部数据点的平均值,并将其设置为簇的质心位置。然后,我们返回新的质心集。

实际上,这里使用了另一个函数均值(这里没有给出)。它返回一个点,其成分是每个传递的点中相应成分值的均值。

function getNewCentroids(clusters) {
    let centroids = [];    
    clusters.forEach(function(cluster) {
        centroids.push(meanPoint(cluster));
    });
    return centroids;
}

注意点!

这种算法有一个问题,就是有时候簇会变空。对于发生这种情况时该怎么做尚无共识,但是一些可能的方法是:

  • 删除簇(有点傻)
  • 为簇分配一个随机数据点
  • 将最接近的数据点分配给簇
  • 重新启动算法,希望它不再发生

尽管最后一个选项似乎有些困难,但是该算法(带有随机初始化)是不确定的,因此只需重启就可以很好地工作。

汇总

现在,我们已经定义了基本功能,剩下的就是编写一些汇总代码,以根据算法定义在需要时调用每个函数。

我将把它留给您作为练习,参考这里。 🌈

上图说明:从图片提取6种的颜色配色的示例。

参考资料

本文由《纯净天空》出品。文章地址: https://vimsky.com/article/4416.html,未经允许,请勿转载。