OpenCV K-Means 聚类完全指南:从算法原理到 cv.kmeans() 实战与颜色量化
2026/9/7 3:58:00 网站建设 项目流程

OpenCV K-Means 聚类完全指南:从算法原理到 cv.kmeans() 实战与颜色量化

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

本文以 OpenCV 官方 K-Means 聚类教程(py_kmeans_index.markdown)为主体,完整讲解 K-Means 聚类的算法思想、cv.kmeans()函数的全部输入输出参数,以及一维数据聚类、多维数据聚类和图像颜色量化三个可运行的实战示例,并结合 modules/core/src/kmeans.cpp 的源码实现剖析终止条件、k-means++ 初始化、空簇处理等底层细节,帮助你在掌握原理的同时正确使用 OpenCV 的聚类接口。

1. 为什么需要 K-Means:T 恤尺码问题

K-Means 聚类要解决的核心问题是:在没有标签的数据中,把样本自动划分为 K 个组,使得组内样本尽量紧凑。

OpenCV 教程用一个经典例子说明(见 py_kmeans_understanding.markdown):一家公司要推出新款 T 恤,它收集了所有人的身高和体重数据并绘制成散点图:

公司不可能为每个人定制一种尺码,而是把人群分成 S、M、L 三档,只生产这三种尺码以覆盖所有人。这种"把人群划分为三组"的分组操作正是 K-Means 聚类能完成的:算法给出最优的 3 个中心点(即最佳尺码规格)。如果三档不够用,可以把 K 增大到 5 甚至更多。

2. K-Means 算法的迭代流程

K-Means 是一个迭代过程,可以分解为以下三个步骤循环执行:

  • Step 1(随机选中心):算法随机选取 K 个质心 C1、C2……(有时也可以直接取任意 K 个数据点作为初始质心)。
  • Step 2(按距离打标签):计算每个数据点到各质心的距离。若某数据点离 C1 更近则标记为 '0',离 C2 更近则标记为 '1'(K 个质心时依次标记 '2'、'3'……)。教程示例中以红色表示 '0'、蓝色表示 '1'。
  • Step 3(更新质心):分别计算所有红色点和蓝色点的平均值,作为新的质心,即 C1、C2 移动到新的位置。

之后重复 Step 2 和 Step 3,直到质心收敛到固定位置(或满足我们给定的停止条件,如最大迭代次数、达到指定精度)。收敛得到的质心满足一个优化目标:所有数据点与其对应质心的距离之和最小,即同时最小化 C1 与红点集合、C2 与蓝点集合的距离总和:

minimize J = Σ(distance(C1, Red_Point) over all Red Points) + Σ(distance(C2, Blue_Point) over all Blue Points)

需要说明:教程中的示意图仅为演示,并非真实数值或真实比例。以上是 K-Means 的直观理解;其变体研究(如初始质心如何选取、如何加速迭代)属于更深入的专题。

3. cv.kmeans() 接口:参数详解

OpenCV 将 K-Means 实现为cv.kmeans()函数。注意它在 C++ 层面属于core 模块(声明见 core.hpp),而非 ml 模块;Python 侧则通过cv2.kmeans()调用。

C++ 原型为:

double kmeans( InputArray data, int K, InputOutputArray bestLabels, TermCriteria criteria, int attempts, int flags, OutputArray centers = noArray() );

官方头文件对函数的描述是:"The function kmeans implements a k-means algorithm that finds the centers of cluster_count clusters and groups the input samples around the clusters",且bestLabels[i]存储第 i 个样本所属的 0 起始簇索引;返回值是每次尝试后计算的紧致度(compactness)最小值。

3.1 输入参数

参数说明
samples参与聚类的数据。必须是np.float32类型,每个特征放在单独一列(即列 = 特征,行 = 样本)。
nclusters(K)要求的最终聚类数量。
criteria迭代终止条件,满足后算法停止。是一个三元组(type, max_iter, epsilon)
·cv.TERM_CRITERIA_EPS—— 当指定精度 epsilon 达到时停止;
·cv.TERM_CRITERIA_MAX_ITER—— 运行指定迭代次数 max_iter 后停止;
·cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER—— 任一条件满足即停止。
· max_iter:最大迭代次数(整数);epsilon:要求的精度。
attempts指定算法用不同初始标记重复执行的次数。算法返回紧致度(compactness)最优的那组标签,紧致度作为输出返回。
flags指定初始质心的选取方式。常用两个标志:
·cv.KMEANS_PP_CENTERS—— k-means++ 初始化(Arthur & Vassilvitskii, 2007);
·cv.KMEANS_RANDOM_CENTERS—— 随机初始化。

从源码结构看,core.hpp 中的KmeansFlags枚举实际还包含第三个标志KMEANS_USE_INITIAL_LABELS = 1:首次(可能也是唯一一次)尝试时,不使用初始质心计算,而直接使用用户提供的标签作为起点,后续尝试再改用随机中心。官方头文件特别提到这一技巧——把 attempts 设为 1、用自定义算法初始化标签并传入该标志,可以只把kmeans当作迭代核心,再在外层自行挑选最紧凑的聚类结果。

3.2 输出参数

输出说明
compactness每个点到其对应中心的距离平方和(紧致度,值越小越好)。
labels标签数组(即教程前文中提到的 'code'),每个元素取 '0'、'1'……,表示样本所属的簇。Python 中形状为 (N, 1) 的 int32 数组。
centers各簇中心的数组,每行一个质心。

官方示例 samples/python/snippets/kmeans.py 展示了典型调用方式:_ret, labels, _centers = cv.kmeans(points, cluster_n, None, term_crit, 10, 0),其中term_crit = (cv.TERM_CRITERIA_EPS, 30, 0.1),第三个参数传None表示让 OpenCV 自行分配 labels 数组。

4. 实战示例一:单特征(一维)数据聚类

以只用身高一个特征来决定 T 恤尺码为例。先生成一维数据并用 Matplotlib 画直方图:

import numpy as np import cv2 as cv from matplotlib import pyplot as plt x = np.random.randint(25,100,25) y = np.random.randint(175,255,25) z = np.hstack((x,y)) z = z.reshape((50,1)) z = np.float32(z) plt.hist(z,256,[0,256]),plt.show()

这里得到 50 个 0~255 范围内的值。z被 reshape 成列向量(50x1)——当特征不止一个时这种"每特征一列"的格式更通用,随后转换为np.float32类型(这是cv.kmeans()的硬性要求)。

接下来应用 KMeans。教程采用的终止条件是:运行 10 次迭代,或达到 epsilon = 1.0 的精度,就停止并返回结果:

# Define criteria = ( type, max_iter = 10 , epsilon = 1.0 ) criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10, 1.0) # Set flags (Just to avoid line break in the code) flags = cv.KMEANS_RANDOM_CENTERS # Apply KMeans compactness,labels,centers = cv.kmeans(z,2,None,criteria,10,flags)

得到 compactness、labels 和 centers 后,教程示例中运行得到 centers 约为 60 和 207;labels 与测试数据同尺寸,每个样本按其最近的质心被标记为 '0'、'1'、'2' 等。按标签拆分数据:

A = z[labels==0] B = z[labels==1]

再分别用红色画 A、蓝色画 B、黄色画质心:

# Now plot 'A' in red, 'B' in blue, 'centers' in yellow plt.hist(A,256,[0,256],color = 'r') plt.hist(B,256,[0,256],color = 'b') plt.hist(centers,32,[0,256],color = 'y') plt.show()

5. 实战示例二:多特征(二维)数据聚类

上一例只用了身高,这里同时使用身高和体重两个特征。

注意数据矩阵的约定:每个特征占一列,每一行是一个输入样本。此例构造 50x2 的测试数据(50 个人的身高和体重):第一列是 50 个人的身高,第二列是体重;第一行的两个元素分别是第 1 个人的身高和体重,其余行类推。

import numpy as np import cv2 as cv from matplotlib import pyplot as plt X = np.random.randint(25,50,(25,2)) Y = np.random.randint(60,85,(25,2)) Z = np.vstack((X,Y)) # convert to np.float32 Z = np.float32(Z) # define criteria and apply kmeans() criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10, 1.0) ret,label,center=cv.kmeans(Z,2,None,criteria,10,cv.KMEANS_RANDOM_CENTERS) # Now separate the data, Note the flatten() A = Z[label.ravel()==0] B = Z[label.ravel()==1] # Plot the data plt.scatter(A[:,0],A[:,1]) plt.scatter(B[:,0],B[:,1],c = 'r') plt.scatter(center[:,0],center[:,1],s = 80,c = 'y', marker = 's') plt.xlabel('Height'),plt.ylabel('Weight') plt.show()

6. 实战示例三:颜色量化

颜色量化(Color Quantization)是减少图像中颜色数量的过程。动机通常是压缩内存,或者某些设备只能显示有限数量的颜色。用 K-Means 做颜色量化的思路并不复杂:

  1. 图像有 R、G、B 三个特征,把图像 reshape 成 Mx3 数组(M 为像素总数);
  2. 对每个像素执行聚类;
  3. 聚类后用质心值(同样是 R,G,B)替换所有像素,得到只有指定颜色数的结果图像;
  4. 再把结果 reshape 回原图形状。

完整代码(教程使用home.jpg,可替换为任意彩色图像):

import numpy as np import cv2 as cv img = cv.imread('home.jpg') Z = img.reshape((-1,3)) # convert to np.float32 Z = np.float32(Z) # define criteria, number of clusters(K) and apply kmeans() criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 10, 1.0) K = 8 ret,label,center=cv.kmeans(Z,K,None,criteria,10,cv.KMEANS_RANDOM_CENTERS) # Now convert back into uint8, and make original image center = np.uint8(center) res = center[label.flatten()] res2 = res.reshape((img.shape)) cv.imshow('res2',res2) cv.waitKey(0) cv.destroyAllWindows()

上图为 K=8 时的量化效果:原图被压缩为仅 8 种颜色,整体色调保留但细节明显简化。

7. 源码级实现剖析:cv::kmeans 内部如何工作

以上例行为什么有效?结合 kmeans.cpp 的实现可以确认几个关键事实:

(1)输入约束与教程要求一一对应。实现入口(L236-L251)断言data0.dims <= 2 && type == CV_32F && K > 0,并检查样本数不少于簇数("There can't be more clusters than elements")。这正是教程强调"必须np.float32""每特征一列"的原因——传错类型会直接报错。

(2)终止条件的实际处理方式。源码中 epsilon 会被平方后再使用(L289-L293:criteria.epsilon *= criteria.epsilon),因为收敛判断比较的是质心位移的平方和;若 criteria 含 COUNT(MAX_ITER)标志,maxCount会被钳制在 [2, 100] 区间(L295-L298)。收敛判定在 L438:当某次迭代中所有质心位移的平方和最大值不超过 epsilon²,或迭代次数达到max(criteria.maxCount, 2)时停止。

(3)k-means++ 初始化(KMEANS_PP_CENTERS)。当 flags 含KMEANS_PP_CENTERS时,走 generateCentersPP:先随机取第一个中心,之后按"距已有中心越远、被选中的概率越大"的概率分布采样候选中心,每次做SPP_TRIALS = 3次试验并选取使总距离和最小的候选——这就是 Arthur & Vassilvitskii (2007) 的 k-means++ 算法。而默认的KMEANS_RANDOM_CENTERS(flags=0)则先在 L306-L324 统计每个特征维度的 [min, max] 边界盒,再由generateRandomCenter在边界盒内随机生成中心。

(4)空簇处理。若某次迭代后出现空簇,实现(L364-L413)会找到最大簇中离其质心最远的那个点,把它从最大簇中剔除并单独成簇,保证 K 个簇始终非空。

(5)attempts 与紧致度。外层按 attempts 次数重复整个流程(L327),每次尝试结束计算紧致度 = 各样本到其簇中心的距离平方和,最终返回所有尝试中最小紧致度对应的 labels、centers 和 compactness(L440-L468)。因此"增大 attempts"确实能提升结果的稳定性。

(6)并行化。距离计算与标签分配都通过parallel_for_分块并行,并行粒度由环境变量OPENCV_KMEANS_PARALLEL_GRANULARITY(默认 1000,见 L53)控制。此外,modules/flann/include/opencv2/flann/kmeans_index.h 中还包含 FLANN 基于 k-means 树结构的最近邻索引,属于另一类用途,可作延伸阅读。

8. 测试与参考示例

  • 官方 Python 测试 modules/python/test/test_kmeans.py:用make_gaussians生成 5 个高斯簇的 float32 数据,以term_crit = (cv.TERM_CRITERIA_EPS, 30, 0.1)、attempts=10 调用cv.kmeans,断言每个真实高斯簇中占多数的标签置信度大于 0.9——这验证了接口在真实分布数据上的正确行为,也示范了 epsilon 取 0.1 这类更精细精度的写法。
  • 交互示例 samples/python/snippets/kmeans.py:循环生成 512x512 画布上的 5 个高斯簇点集,每次按 labels 给点着不同颜色绘制;按 space 重新采样、ESC 退出,适合直观感受聚类效果。

9. 小结

  • K-Means 以"随机初始质心 → 最近质心打标 → 重算质心"的迭代逼近距离平方和最小的分组,收敛目标可写为各点到对应质心距离之和的极小化。
  • 使用cv.kmeans()时的三个硬性要点:数据必须np.float32且每特征一列;K ≤ 样本数;用(type, max_iter, epsilon)元组控制终止。
  • 三个由浅入深的场景覆盖了典型用法:一维直方图分档、二维散点聚类、以及把 Mx3 像素矩阵量化为 K 种颜色的图像颜色量化。
  • 追求更稳定的初始质心可选cv.KMEANS_PP_CENTERS(内部即 k-means++),追求重复实验可比对KMEANS_RANDOM_CENTERS与不同 attempts 的结果。

【免费下载链接】opencvOpen Source Computer Vision Library项目地址: https://gitcode.com/GitHub_Trending/opencv31/opencv

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询