當前位置: 首頁>>技術教程>>正文


使用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/zh-tw/article/4416.html,未經允許,請勿轉載。