基于K-Means聚类算法的主颜色提取
点击上方“小白学视觉 ”,选择加"星标 "或“置顶 ”
重磅干货,第一时间送达

01.简介
本期我们将一起以K-Means聚类算法为基础进行主色调提取。在深入研究代码之前,请让我们先了解一些关于该算法的基本知识。
02.K均值类聚算法
K-means algorithm is a widely used and simple unsupervised learning method. For all data points distributed in the n-dimensional space, it clusters data points with similar characteristics into several clusters. After randomly initializing k cluster centroids, the algorithm iteratively performs two steps: first, it calculates the distance between each sample and each centroid; second, it assigns each sample to the cluster of the nearest centroid.
1. 聚类分配:根据每个数据点距聚类质心的距离,为其分配一个聚类。
2. 移动质心:计算聚类所有点的平均值,并将聚类质心重定位到平均位置。
根据新的质心位置,将数据点重新分配给群集。

K-Means算法的迭代步骤
经过若干次迭代后观察得知,在质心不会再发生进一步移动或移位至任何新的位置时,并没有数据点发生变动的情况出现。此时认为算法已收敛完成
我们将整个程序分为多个功能,首先导入该程序所需的模块
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.colors as color
import numpy as np
from collections import Counter
import pandas as pd
import math
from sklearn.cluster import KMeans
from PIL import Image
import webcolors
import json
import argparse
在程序执行流程中, 我们将初始化一个ArgumentParser对象来接收command-line parameters, 并设置相应的变量来存储这些参数的值. 同时, 两个可选的command-line arguments, clusters和imagepath, were maintained.
parser = argparse.ArgumentParser()
parser.add_argument("--clusters", help="No. of clusters")
parser.add_argument("--imagepath", help="Path to input image")
args = parser.parse_args()
IMG_PATH = args.imagepath if args.imagepath else "images/poster.jpg"
CLUSTERS = args.clusters if args.clusters else 5
WIDTH = 128
HEIGHT = 128
在 clusters 选项卡中,当指定包含图像文件名的路径时,请确定要从每个图像中提取的颜色数量。程序默认会提取 5 种典型颜色,并从该文件夹中选择名为 posters 的图片。建议团队成员根据具体需求来设定默认值。此外,在进行颜色提取之前,请确保将图片尺寸设置为 128x128 像素以提高准确性。
为了有效地处理十六进制代码及其相应的颜色名称,在我的项目中我采用了JSON格式的数据存储方式。这些颜色名称与其对应的十六进制代码信息已经构建成了一个完整的词典集合,并且这些内容都源自下面提供的JavaScript文件:
http : //chir.ag/projects/ntc/ntc.js(JavaScript文件)
http:// chir.ag/projects/ntc/(链接到创建者的网站)
我们已经在名为color_dict的变量中解析了JSON文件。目前而言,在此字典变量下可以直接访问JSON文件中的键值对部分。
with open('colors.json') as clr:
color_dict = json.load(clr)
现在让我们开始将图像作为输入并将其传递给K-Means算法。
def TrainKMeans(img):
new_width, new_height = calculate_new_size(img)
image = img.resize((new_width, new_height), Image.ANTIALIAS)
img_array = np.array(image)
img_vector = img_array.reshape((img_array.shape[0] * img_array.shape[1], 3))
'''
----------
Training K-Means Clustering Algorithm
----------
'''
kmeans = KMeans(n_clusters = CLUSTERS, random_state=0)
labels = kmeans.fit_predict(img_vector)
hex_colors = [rgb_to_hex(center) for center in kmeans.cluster_centers_]
color_name = {}
for c in kmeans.cluster_centers_:
h, name = findColorName(c)
color_name[h] = name
img_cor = [[*x] for x in img_vector]
'''
img_cor is a nested list of all the coordinates (pixel -- RGB value) present in the
image
'''
cluster_map = pd.DataFrame()
cluster_map['position'] = img_cor
cluster_map['cluster'] = kmeans.labels_
cluster_map['x'] = [x[0] for x in cluster_map['position']]
cluster_map['y'] = [x[1] for x in cluster_map['position']]
cluster_map['z'] = [x[2] for x in cluster_map['position']]
cluster_map['color'] = [hex_colors[x] for x in cluster_map['cluster']]
cluster_map['color_name'] = [color_name[x] for x in cluster_map['color']]
print(cluster_map)
return cluster_map, kmeans
众所周知,在第一步中,该函数以图像文件作为参数被调用,并将图像按照我们之前在程序中定义的尺寸进行调整,并采用自定义函数来优化其尺寸。
def calculate_new_size(image):
'''
We are resizing the image (one of the dimensions) to 128 px and then, scaling the
other dimension with same height by width ratio.
'''
if image.width >= image.height:
wperc = (WIDTH / float(image.width))
hsize = int((float(image.height) * float(wperc)))
new_width, new_height = WIDTH, hsize
else:
hperc = (HEIGHT / float(image.height))
wsize = int((float(image.width) * float(hperc)))
new_width, new_height = wsize, HEIGHT
return new_width, new_height
在自定义尺寸调节功能模块里,在调用该模块时会执行以下操作:首先将图片的长边设定为预设高度HEIGHT或宽度WIDTH,并相应地调节剩余维度;同时确保输出后的高宽比例始终保持一致。随后执行完TrainKMeans函数后返回;此时系统会对图片进行尺寸优化,并将其转化为numpy格式的数据对象;最后对该数据对象进行重塑处理生成三维向量序列用于后续RGB分量分析。
为了在图像中创建颜色簇。通过调用KMeans函数来创建群集,在程序运行初期我们从标准输入获取命令行参数以设定n_clusters参数设为clusters值,并将random_state被设定为零作为固定随机种子以确保结果一致性。随后我们将输入图像文件拟合模型用于聚类分析,并根据这些结果进行预测分类工作。接着利用聚类中心(以RGB值表示)找出各聚类代表的颜色特征,并通过自定义函数rgb_to_hex完成了这一转换步骤
def rgb_to_hex(rgb):
'''
Converting our rgb value to hex code.
'''
hex = color.to_hex([int(rgb[0])/255, int(rgb[1])/255, int(rgb[2])/255])
print(hex)
return hex
这个函数具有极高的简单性,并采用matplotlib.colors库中的to_hex方法实现颜色转换功能。经过归一化处理后将RGB值标准化至0至1范围内的数值,并随后将其转换为其对应的十六进制表示直至获得所有颜色簇对应的十六进制表示
在下一步中,我们将使用findColorName()函数查找每种颜色的名称。
def findColorName(rgb):
'''
Finding color name :: returning hex code and nearest/actual color name
'''
aname, cname = get_colour_name((int(rgb[0]), int(rgb[1]), int(rgb[2])))
hex = color.to_hex([int(rgb[0])/255, int(rgb[1])/255, int(rgb[2])/255])
if aname is None:
name = cname
else:
name = aname
return hex, name
def closest_colour(requested_colour):
'''
We are basically calculating euclidean distance between our set of RGB values
with all the RGB values that are present in our JSON. After that, we are looking
at the combination RGB (from JSON) that is at least distance from input
RGB values, hence finding the closest color name.
'''
min_colors = {}
for key, name in color_dict['color_names'].items():
r_c, g_c, b_c = webcolors.hex_to_rgb("#"+key)
rd = (r_c - requested_colour[0]) *
gd = (g_c - requested_colour[1]) *
bd = (b_c - requested_colour[2]) *
min_colors[math.sqrt(rd + gd + bd)] = name
#print(min(min_colours.keys()))
return min_colors[min(min_colors.keys())]
def get_colour_name(requested_colour):
'''
In this function, we are converting our RGB set to color name using a third
party module "webcolors".
RGB set -> Hex Code -> Color Name
By default, it looks in CSS3 colors list (which is the best). If it cannot find
hex code in CSS3 colors list, it raises a ValueError which we are handling
using our own function in which we are finding the closest color to the input
RGB set.
'''
try:
closest_name = actual_name = webcolors.rgb_to_name(requested_colour)
except ValueError:
closest_name = closest_colour(requested_colour)
actual_name = None
return actual_name, closest_name
在名为findColorName的函数中调用了一个名为get_color_name的自定义函数。该函数返回两项结果:实际名称和最近的颜色名称。
在本功能模块中调用第三方库webcolors来进行RGB值转色。通常情况下,默认调用该函数时会首先检查CSS3标准色数据库中的对应信息。如果数据库中不存在匹配的颜色,则会导致ValueError异常。此时则采用名为closest_colour()的自定义处理函数来替代标准操作流程。在其中,系统会对输入的RGB值逐一计算其与数据库中所有记录项之间的欧氏距离,并最终选取与输入数据最接近的那个条目作为结果返回。
该函数生成的十六进制编码字典及其对应的名称被首先创建完成。接着利用img_vector构建了图像中所有RGB像素点的列表。随后初始化了一个空的数据框cluster_map,并在其增加一个名为position的列。该列用于记录图像中各数据点(像素)及其所属簇中心的颜色信息。同时记录了每个像素所属的具体簇编号。最后,在color和color_name两列中为图像中的每个像素存储了其对应的十六进制编码以及颜色名称。最后返回了包含聚类结果的数据框以及kmeans对象。
def plotColorClusters(img):
cluster_map, kmeans = TrainKMeans(img)
fig = plt.figure()
ax = Axes3D(fig)
# grouping the data by color hex code and color name to find the total count of
# pixels (data points) in a particular cluster
mydf = cluster_map.groupby(['color', 'color_name']).agg({'position':'count'}).reset_index().rename(columns={"position":"count"})
mydf['Percentage'] = round((mydf['count']/mydf['count'].sum())*100, 1)
print(mydf)
# Plotting a scatter plot for all the clusters and their respective colors
ax.scatter(cluster_map['x'], cluster_map['y'], cluster_map['z'], color = cluster_map['color'])
plt.show()
'''
Subplots with image and a pie chart representing the share of each color identified
in the entire photograph/image.
'''
plt.figure(figsize=(14, 8))
plt.subplot(221)
plt.imshow(img)
plt.axis('off')
plt.subplot(222)
plt.pie(mydf['count'], labels=mydf['color_name'], colors=mydf['color'], autopct='%1.1f%%', startangle=90)
plt.axis('equal')
plt.show()
def main():
img = Image.open(IMG_PATH)
plotColorClusters(img)
最后通过散点图绘制了3D空间中图像的每个数据点(像素),并在图像中标注颜色,并被饼图展示了图像的颜色分布。
