原文:https://blog.csdn.net/google19890102/article/details/52911835
一、理論準備
1.1、圖像分割
圖像分割是圖像處理中的一種方法,圖像分割是指將一幅圖像分解成若干互不相交區域的集合,其實質可以看成是一種像素的聚類過程。通常使用到的圖像分割的方法可以分爲:
- 基於邊緣的技術
- 基於區域的技術
基於聚類算法的圖像分割屬於基於區域的技術。
1.2、K-Means算法
K-Means算法是基於距離相似性的聚類算法,通過比較樣本之間的相似性,將形式的樣本劃分到同一個類別中,K-Means算法的基本過程爲:
- 初始化常數 ,隨機初始化k個聚類中心
- 重複計算以下過程,直到聚類中心不再改變
- 計算每個樣本與每個聚類中心之間的相似度,將樣本劃分到最相似的類別中
- 計算劃分到每個類別中的所有樣本特徵的均值,並將該均值作爲每個類新的聚類中心
- 輸出最終的聚類中心以及每個樣本所屬的類別
經典K-means算法:
經典K-means算法 |
---|
Step1: 從數據集中隨機選取k個樣本作爲初始聚類中心`$ |
Step2: 針對數據集中每個樣本$x_i$ ,計算它到k個聚類中心的距離並將其分到距離最小的聚類中心所對應的類中 |
Step3: 針對每個類別$c_i$ ,重新計算它的聚類中心$c_i=\frac{1}{\vert c_i \vert}\sum_{x\in c_i} x$ (即屬於該類的所有樣本的質心) |
Step4: 重複第2步和第3步直到聚類中心的位置不再變化 |
1.3、K-Means++算法
在K-Means算法中,需要隨機初始化k個聚類中心,而K-Means算法對初始聚類中心的選取較爲敏感,若選擇的聚類中心不好,則得到的聚類結果會非常差,因此,對K-Means算法提出了很多的改進的方法,如K-Means++算法,在K-Means++算法中,希望初始化的k個聚類中心之間的距離儘可能的大,其具體過程爲:
- 步驟一:在數據集中隨機選擇一個樣本點作爲第一個初始化的聚類中心
- 步驟二:選擇出其餘的聚類中心:
- 計算樣本中的每一個樣本點與已經初始化的聚類中心之間的距離,並選擇其中最短的距離(即與最近一個聚類中心的距離),用D(x)表示;
- 這個值越大,表示被選取作爲聚類中心的概率較大;
- 最後,用輪盤法選出下一個聚類中心;
- 步驟三:重複步驟二,知道選出 k 個聚類中心。
K-means++算法 |
---|
Step1: 從數據集中隨機選取一個樣本作爲初始聚類中心$c_1$ |
Step2: 首先計算每個樣本與當前已有聚類中心之間的最短距離(即與最近的一個聚類中心的距離),用D(x)表示;接着計算每個樣本被選爲下一個聚類中心的概率`$. 最後,按照輪盤法選擇出下一個聚類中心 |
Step3: 重複第2步直到選擇出共k個聚類中心 |
下面結合一個簡單的例子說明K-means++是如何選取初始聚類中心的。
數據集中共有8個樣本,分佈以及對應序號如下圖所示:
假設經過圖2的步驟一後6號點被選擇爲第一個初始聚類中心,
那在進行步驟二時每個樣本的D(x)和被選擇爲第二個聚類中心的概率如下表所示:
序號 | ① | ② | ③ | ④ | ⑤ | ⑥ | ⑦ | ⑧ |
---|---|---|---|---|---|---|---|---|
$D(x)$$| $ |
1 | 0 | 1 | |||||
$D(x)^2$ |
8 | 13 | 5 | 10 | 1 | 0 | 2 | 1 |
`$ | 0.2 | 0.325 | 0.125 | 0.25 | 0.025 | 0 | 0.05 | 0.025 |
Sum | 0.2 | 0.525 | 0.65 | 0.9 | 0.925 | 0.925 | 0.975 | 1 |
其中的P(x)就是每個樣本被選爲下一個聚類中心的概率。
最後一行的Sum是概率P(x)的累加和,用於輪盤法選擇出第二個聚類中心。
方法是隨機產生出一個0~1之間的隨機數,判斷它屬於哪個區間,那麼該區間對應的序號就是被選擇出來的第二個聚類中心了。
例如1號點的區間爲[0,0.2),2號點的區間爲[0.2, 0.525)。
從上表可以直觀的看到第二個初始聚類中心是1號,2號,3號,4號中的一個的概率爲0.9。而這4個點正好是離第一個初始聚類中心6號點較遠的四個點。
這也驗證了K-means的改進思想:即離當前已有聚類中心較遠的點有更大的概率被選爲下一個聚類中心。
可以看到,該例的K值取2是比較合適的。當K值大於2時,每個樣本會有多個距離,需要取最小的那個距離作爲D(x)。
python實現
# coding: utf-8
import math
import random
from sklearn import datasets
def euler_distance(point1: list, point2: list) -> float:
"""
計算兩點之間的歐拉距離,支持多維
"""
distance = 0.0
for a, b in zip(point1, point2):
distance += math.pow(a - b, 2)
return math.sqrt(distance)
def get_closest_dist(point, centroids):
min_dist = math.inf # 初始設爲無窮大
for i, centroid in enumerate(centroids):
dist = euler_distance(centroid, point)
if dist < min_dist:
min_dist = dist
return min_dist
def kpp_centers(data_set: list, k: int) -> list:
"""
從數據集中返回 k 個對象可作爲質心
"""
cluster_centers = []
cluster_centers.append(random.choice(data_set))
d = [0 for _ in range(len(data_set))]
for _ in range(1, k):
total = 0.0
for i, point in enumerate(data_set):
d[i] = get_closest_dist(point, cluster_centers) # 與最近一個聚類中心的距離
total += d[i]
total *= random.random()
for i, di in enumerate(d): # 輪盤法選出下一個聚類中心;
total -= di
if total > 0:
continue
cluster_centers.append(data_set[i])
break
return cluster_centers
if __name__ == "__main__":
iris = datasets.load_iris()
print(kpp_centers(iris.data, 4))
二、實踐準備
- 使用到的模塊Numpy,PIL(python image library)
import PIL.Image as image
- 打開圖片
首先是以二進制文件的形式打開文件,再利用Image模塊的open方法導入圖片.
fp=open(file_path,'rb')
im=image.open(fp)
- 常用操作
im.format,im.size,im.mode
結果爲:
JPEG,(1600,1067),RGB
r,g,b=im.split() #通道分離
im.getpixel((4,4)) #取得像素點的值
im.putpixel(x,y,color) #改變單個像素點的值
im=im.convert("L") #圖像類型轉換
image.new(mode,size,color) #生成新的圖像
im.save('save.gif','GIF')
圖片如下:
三、利用K-Means++算法進行圖像分割
3.1、利用K-Means聚類
在利用K-Means++算法進行圖像分割時,將圖像中的每一個像素點作爲一個樣本,對RGB圖像來說,每個樣本包括三維:(151, 169, 205),通過歸一化,將每個通道的值壓縮到[0,1]區間上。數據的導入和處理如下面程序所示:
import numpy as np
import PIL.Image as image
def load_data(file_path):
'''導入數據
input: file_path(string):文件的存儲位置
output: data(mat):數據
'''
f = open(file_path, "rb") # 以二進制的方式打開圖像文件
data = []
im = image.open(f) # 導入圖片
m, n = im.size # 得到圖片的大小
print(m, n)
for i in range(m):
for j in range(n):
tmp = []
x, y, z = im.getpixel((i, j))
tmp.append(x / 256.0)
tmp.append(y / 256.0)
tmp.append(z / 256.0)
data.append(tmp)
f.close()
return np.mat(data)
data = load_data("c:/users/administrator/desktop/1/qc.jpg")
data.shape
#結果爲:(370400, 3)
最終保存成矩陣的形式,矩陣的行爲樣本的個數,列爲每一個通道的數值(RGB)。
K-Means++程序的實現如下面程序所示:
def distance(vecA, vecB):
'''計算vecA與vecB之間的歐式距離的平方
input: vecA(mat)A點座標
vecB(mat)B點座標
output: dist[0, 0](float)A點與B點距離的平方
'''
dist = (vecA - vecB) * (vecA - vecB).T
return dist[0, 0]
def randCent(data, k):
'''隨機初始化聚類中心
input: data(mat):訓練數據
k(int):類別個數
output: centroids(mat):聚類中心
'''
n = np.shape(data)[1] # 屬性的個數
centroids = np.mat(np.zeros((k, n))) # 初始化k個聚類中心
for j in range(n): # 初始化聚類中心每一維的座標
minJ = np.min(data[:, j])
rangeJ = np.max(data[:, j]) - minJ
# 在最大值和最小值之間隨機初始化
centroids[:, j] = minJ * np.mat(np.ones((k , 1))) + np.random.rand(k, 1) * rangeJ
return centroids
centroids=randCent(data,4)
centroids
結果爲:
matrix([[0.60464688, 0.91807467, 0.46543835],
[0.05775657, 0.92319872, 0.49939161],
[0.20882795, 0.66650272, 0.83838715],
[0.81367366, 0.13199625, 0.51818457]])
kmeans函數代碼如下:
def kmeans(data, k, centroids):
'''根據KMeans算法求解聚類中心
input: data(mat):訓練數據
k(int):類別個數
centroids(mat):隨機初始化的聚類中心
output: centroids(mat):訓練完成的聚類中心
subCenter(mat):每一個樣本所屬的類別
'''
m, n = np.shape(data) # m:樣本的個數,n:特徵的維度
subCenter = np.mat(np.zeros((m, 2))) # 初始化每一個樣本所屬的類別
change = True # 判斷是否需要重新計算聚類中心
while change == True:
change = False # 重置
for i in range(m):
minDist = np.inf # 設置樣本與聚類中心之間的最小的距離,初始值爲爭取窮
minIndex = 0 # 所屬的類別
for j in range(k):
# 計算i和每個聚類中心之間的距離
dist = distance(data[i, ], centroids[j, ])
if dist < minDist:
minDist = dist
minIndex = j
# 判斷是否需要改變
if subCenter[i, 0] != minIndex: # 需要改變
change = True
subCenter[i, ] = np.mat([minIndex, minDist])
# 重新計算聚類中心
for j in range(k):
sum_all = np.mat(np.zeros((1, n)))
r = 0 # 每個類別中的樣本的個數
for i in range(m):
if subCenter[i, 0] == j: # 計算第j個類別
sum_all += data[i, ]
r += 1
for z in range(n):
try:
centroids[j, z] = sum_all[0, z] / r
print(r)
except:
print(" r is zero")
return subCenter
subCenter=kmeans(data, 4, centroids)
3.2、利用聚類結果保存和生成新的圖片
首先,保存圖片如下:
def save_result(file_name, source):
'''保存source中的結果到file_name文件中
input: file_name(string):文件名
source(mat):需要保存的數據
output:
'''
m, n = np.shape(source)
f = open(file_name, "w")
for i in range(m):
tmp = []
for j in range(n):
tmp.append(str(source[i, j]))
f.write("\t".join(tmp) + "\n")
f.close()
save_result("sub_pp", subCenter)
上述的過程中,對每一個像素點進行了聚類,最終利用聚類中心點的RGB值替換原圖中每一個像素點的值,便得到了最終的分割後的圖片,代碼如下所示:
import PIL.Image as image
f_center = open("d:/0402/center_pp_fj1_7")
center = []
for line in f_center.readlines():
lines = line.strip().split("\t")
tmp = []
for x in lines:
tmp.append(int(float(x) * 256))
center.append(tuple(tmp))
print(center)
f_center.close()
fp = open("d:/0402/fj1.jpg", "rb")
im = image.open(fp)
# 新建一個圖片
m, n = im.size
pic_new = image.new("RGB", (m, n))
f_sub = open("d:/0402/sub_pp_fj1_7")
i = 0
for line in f_sub.readlines():
index = float((line.strip().split("\t"))[0])
index_n = int(index)
pic_new.putpixel((int(i/n),(i % n)),center[index_n])
i = i + 1
f_sub.close()
pic_new.save("d:/0402/fj1_7.jpg", "JPEG")
對於上述的聖托裏尼的圖片,取不同的k值,得到如下的一些結果:
-
原圖
-
k=4
-
k=7
-
k=10
再來看一個例子。 -
原圖
-
k=3
-
k=5
-
k=10