基于OpenCV和K-Means聚类实现颜色分割
颜色分割是一种图像处理技术,用于根据颜色识别图像中的不同物体或区域。通过K-means聚类算法,可以将相似颜色的气泡分组,并计算每种颜色包含的对象数量。代码使用OpenCV和scikit-learn进行图像分割和颜色聚类,流程包括读取图像、预处理、轮廓提取、计算平均颜色、聚类分组以及结果展示。最终输出展示了不同颜色气泡的分割结果。
1、前言
改写说明
该颜色聚类算法能够自动实现相似颜色的分组,无需为每组颜色设定阈值。特别适用于颜色范围广泛且阈值难以确定的情况,该算法表现出色。
更多关于聚类算法的分割,请参考:<>
在本章中,我们将基于python编程语言,通过opponent库结合k-means聚类算法实现颜色分割,并同时统计每种颜色所包含的对象数量,以供参考的测试图像如下:

大概思路如下:
1.根据轮廓查找和过滤气泡对象,
采用 K 均值算法对颜色相似的气泡进行分组(能够生成和提取颜色相似气泡的蒙版,以便后续处理)
2. 代码实验
本章使用的是OpenCV和scikit-learn库进行图像分割和颜色聚类
2.1 库函数
如下:
from matplotlib import pyplot as plt
import cv2
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
读取图像如下:

2.2 使用阈值提取掩码
第一步是从图像中提取所有的圆形小球
首先,我们对图像进行灰度化处理,这将通过cv2.cvtColor()函数实现。接着,我们使用cv2.threshold()函数将图像转换为二进制形式。在该过程中,像素值0对应背景区域,而255对应前景区域。
阈值赋值为 60,经设置后所有像素值低于 60 的像素会被置为 0,其余像素则被置为 255。
在本例中,我们采用了全局阈值方法,仅为演示目的而使用,但根据具体任务需求,建议采用大津法或自适应阈值等其他方法。参考:数字图像处理_喵星人监护人的博客-博客
因为一些圆形小球靠得较近,在二进制图像上存在轻微重叠,通过函数cv2.erode()对图像进行处理,从而达到分离目标。
腐蚀是一种形态学操作,会缩小图像中对象的尺寸。它通常用于去除图像中的微小噪声,并将彼此连接的对象分离。
腐蚀操作是灰度形态学中的一种基本运算,其核心思想是通过结构元素与图像进行特定的形态学处理,以达到图像增强、特征提取等目的。该运算通过计算结构元素与图像中每个像素的灰度值差异,从而实现对图像细节的调整。在灰度级形态学中,腐蚀操作的数学表达式为:对于图像f和结构元素B,腐蚀后的图像C可表示为C(x,y)=\min\{f(x+i,y+j) | (i,j) \in B\}。这种操作能够有效去除图像中的噪声点,并保留边缘信息,是图像处理中不可或缺的重要工具。
from matplotlib import pyplot as plt
import cv2
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
image = cv2.imread(r'test.jpg', cv2.IMREAD_UNCHANGED)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_ , mask = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY)
mask = cv2.erode(mask, np.ones((3, 3), np.uint8))
cv2.imshow('show',mask)
cv2.waitKey()
cv2.destroyAllWindows()

2.3 使用轮廓提取对象边界
cv2.findContours()是一种用于在二值图像中识别对象边界的函数。该函数通过设置标志来指定如何检索轮廓。当使用cv2.RETR_EXTERNAL标志时,仅提取图像中最外层的轮廓。该算法通过返回一个包含多个轮廓的列表来完成边界检测,其中每个轮廓代表图像中的一个独立对象边界。
关键代码如下:
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
[图像分割 - 提取图像的轮廓信息(cv2.findContours函数)-博客]( "图像分割 - 提取图像的轮廓信息(cv2.findContours函数)-博客")
2.4 过滤轮廓并提取平均颜色
为了排除由气泡引起的轮廓干扰,通过迭代生成一系列轮廓,并对生成的这些轮廓进行筛选,最终保留面积较大的轮廓。
因为图像中可能存在噪音干扰,这将有助于我们准确识别气泡的边界并去除那些微小的干扰元素,如字母或背景中的微小干扰。

为了获取每个气泡的平均色调,我们首先通过在黑色背景图像上绘制气泡轮廓来生成每个气泡的遮罩。随后,我们通过在黑色背景图像上绘制气泡轮廓来生成每个气泡的遮罩。接着,我们利用cv2.mean()函数,结合原始图像和气泡的遮罩,计算出气泡的平均蓝色、绿色和红色 (BGR) 通道值。每个气泡的平均BGR值则被存储在pandas DataFrame中。
2.5 使用 K-means 算法对相似颜色进行聚类
最后,应用 K 均值聚类算法将颜色相似的气泡分组在一起
该系统采用轮廓平均颜色值作为KMeans算法的输入数据源,其中n_clusters超参数决定了聚类数量。
在本例中,由于气泡有 6 种颜色,我们将该值设置为 6 。
K 均值算法是一种广泛使用的聚类技术,能够将相似的数据点归类为同一群组。该算法的基本原理是将输入数据集划分为预设数量的群组,每个群组由一个质心点代表。质心的初始位置通常随机确定,算法通过迭代过程将每个数据点分配到最近的质心所对应的群组中。在所有数据点完成分配后,质心会更新为各群组内数据点的平均位置,这一过程持续进行直至质心位置稳定,数据点不再发生重新分配。
该算法通过迭代优化来实现质心位置的调整,最终达到数据点与质心之间距离最小化的目标。在实现过程中,质心的更新是基于当前群组内数据点的平均值计算得出,确保算法的收敛性。通过不断迭代,算法能够逐步优化群组划分,最终形成稳定的聚类结果。
该算法能够基于每个气泡的平均 BGR 值的输入,实现将具有相似颜色的气泡分组在一起。
当KMeans类初始化完成后,fit_predict方法将执行聚类操作。该fit_predict函数生成每个数据对象的聚类标签,并将其赋予数据集中的新'标签'列。这从而使得我们能够识别出哪些数据点属于哪个特定的聚类。
km = KMeans( n_clusters=6)
df_mean_color['label'] = km.fit_predict(df_mean_color)
首先提取分段对象,然后并构建一个函数,用于生成一个新的混合图像蒙版,其中包含相同颜色的气泡区域。
首先,创建一个二进制蒙版:在黑色图像上,用白色勾勒出所有具有相同标签的气泡的轮廓。随后,通过调用cv2.bitwise_and函数,对原始图像和蒙版进行按位与操作,得到一个仅显示相同标签气泡的图像。为清晰起见,使用cv2.putText函数在图像上标注每种气泡的数量信息。
def draw_segmented_objects(image, contours, label_cnt_idx, bubbles_count):
mask = np.zeros_like(image[:, :, 0])
cv2.drawContours(mask, [contours[i] for i in label_cnt_idx], -1, (255), -1)
masked_image = cv2.bitwise_and(image, image, mask=mask)
masked_image = cv2.putText(masked_image, f'{bubbles_count} bubbles', (200, 200), cv2.FONT_HERSHEY_SIMPLEX,
fontScale = 1, color = (255, 255, 255), thickness = 3, lineType = cv2.LINE_AA)
return masked_image
对每组具有相同标签的气泡,调用该draw_segmented_objects函数,以生成每种颜色的蒙版图像。可以通过统计按颜色分组后的 DataFrame 中的行数,来确定每种颜色的珠子数量。
img = image.copy()
for label, df_grouped in df_mean_color.groupby('label'):
bubbles_amount = len(df_grouped)
masked_image = draw_segmented_objects(image, contours, df_grouped.index, bubbles_amount)
img = cv2.hconcat([img, masked_image])
3. 完整代码
如下:
from matplotlib import pyplot as plt
import cv2
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
def draw_segmented_objects(image, contours, label_cnt_idx, bubbles_count):
mask = np.zeros_like(image[:, :, 0])
cv2.drawContours(mask, [contours[i] for i in label_cnt_idx], -1, (255), -1)
masked_image = cv2.bitwise_and(image, image, mask=mask)
masked_image = cv2.putText(masked_image, f'{bubbles_count} bubbles', (200, 200), cv2.FONT_HERSHEY_SIMPLEX,
fontScale = 1, color = (255, 255, 255), thickness = 3, lineType = cv2.LINE_AA)
return masked_image
image = cv2.imread(r'test.jpg', cv2.IMREAD_UNCHANGED)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
_ , mask = cv2.threshold(gray, 60, 255, cv2.THRESH_BINARY)
mask = cv2.erode(mask, np.ones((5, 5), np.uint8))
# 使用轮廓提取边界
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
filtered_contours = []
masked = np.zeros_like(image[:, :, 0]) # This mask is used to get the mean color of the specific bead (contour), for kmeans
df_mean_color = pd.DataFrame()
for idx, contour in enumerate(contours):
area = int(cv2.contourArea(contour))
# if area is higher than 100:
if area > 100:
filtered_contours.append(contour)
# get mean color of contour:
masked = np.zeros_like(image[:, :, 0]) # This mask is used to get the mean color of the specific bead (contour), for kmeans
masked = cv2.drawContours(masked, [contour], 0, [255,0,255], -1)
B_mean, G_mean, R_mean, _ = cv2.mean(image, mask=masked)
df = pd.DataFrame({'B_mean': B_mean, 'G_mean': G_mean, 'R_mean': R_mean}, index=[idx])
df_mean_color = pd.concat([df_mean_color, df])
km = KMeans( n_clusters=6)
df_mean_color['label'] = km.fit_predict(df_mean_color)
img = image.copy()
for label, df_grouped in df_mean_color.groupby('label'):
bubbles_amount = len(df_grouped)
masked_image = draw_segmented_objects(image, contours, df_grouped.index, bubbles_amount)
img = cv2.hconcat([img, masked_image])
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB) )
plt.show()
效果:

本文可被视为一种强大的图像处理工具,可基于颜色识别和量化图像中的对象。通过结合K-means算法、OpenCV和scikit-learn,我们实现了对图像进行颜色分割,并统计每种颜色对象的数量。该方法在多个依赖颜色分析和分类图像对象的领域具有广泛的应用潜力。
