K均值聚类算法在cxcoer中,因为它在ML库诞生之前就存在了.K均值尝试找到数据的自然类别.用户设置类别个数,K均值迅速地找到"好的"类别中心."好的"意味着聚类中心位于数据的自然类别中心.K均值是最常用的聚类计数之一,与高斯混合中的期望最大化算法(在ML库中实现为CvEM)很相似,也与均值漂移算法(在CV库中实现为cvMeanShift())相似.K均值是一个迭代算法,在OpenCV中采用的是Lloyd算法,也叫Voronoi迭代.算法运行如下.
1.输入数据集合和类别数K(由用户指定)
2.随机分配类别中心点的位置
3.将每个点放入离它最近的类别中心点所在的集合.
4.移动类别中心点到它所在集合的中心
5.转到第3步,直到收敛.
图13-5展示了K均值是怎么工作的.在这个例子中,只用两次迭代就达到了收敛.在现实数据中,算法经常很快的收敛,但是有的时候需要迭代的次数比较多.
K均值是一个及其高效的聚类算法,但是它也有以下3个问题.
1.它不保证能找到定位聚类中心的最佳方案,但是它能保证能收敛到某个解决方案(例如迭代不再无限继续下去).
2.K均值无法指出应该使用多少类别.在同意数据集中,例如对图13-5中的例子,选择两个类别和选择四个类别,得到的结果是不一样的,甚至是不合理的.
3.K均值假设空间的协方差矩阵不会影响结果,或者已经归一化(参考Mahalanobis距离的讨论).
这三个问题都有"解决办法".这些解决方法中最好的两种都是基于"劫色数据的方差".在K均值中,每个聚类中线拥有他的数据点,我们计算这些点的方差,最好的聚类在不引起太大的复杂度的情况下使方差达到最小.所以,列出的问题可以改进如下.
1.多进行几次K均值,每次初始的聚类中心点都不一样(很容易做到,因为OpenCV随机选择中心点),最后选择方差最小的那个结果
2.首先将类别数设为1,然后提高类别数(到达某个上限),每次聚类的时候使用前面提到的方法1.一般情况下,总方差会很快下降,直到到达一个拐点;这意味着再加一个新的聚类中心不会显著地减少总方差.在拐点处停止,保存此时的类别数.
3.将数据乘以逆协方差矩阵.例如:如果输入向量是按行排列,没行一个数据点,则通过计算新的数据向量D*,D* = D∑-1/2来归一化数据空间.
K均值函数的调用很简单:
void cvKMeans2(const CvArr* samples, int cluster_count,CvArr* labels,CvTrtmCriteria termcrit);
数组sample是一个多维的数据样本矩阵,每行一个数据样本.这里有一点点微妙,数据样本可以是浮点型CV_32FC1向量,也可以是CV_32FC2,CV_32FC3和CV_32FC(k)的多维向量.参数cluster_count指定类别数,返回向量包含每个点最后的类别索引.前面已经讨论了terncrit.
#include <QCoreApplication> #include <opencv2/highgui/highgui.hpp> #include <opencv2/core/core.hpp> int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); #define MAX_CLUSTER 5 CvScalar color_table[MAX_CLUSTER]; IplImage* img = cvCreateImage(cvSize(500,500),8,3); CvRNG rng = cvRNG(0xffffffff); color_table[0] = CV_RGB(255,0,0); color_table[1] = CV_RGB(0,255,0); color_table[2] = CV_RGB(100,100,255); color_table[3] = CV_RGB(255,0,255); color_table[4] = CV_RGB(255,255,0); cvNamedWindow("clusters",1); for(;;) { int k,cluster_count = cvRandInt(&rng)%MAX_CLUSTER +1; //类别个数 1~5 int i,sample_count = cvRandInt(&rng)%1000+1; //样本个数 1~1000 CvMat* points = cvCreateMat(sample_count,1,CV_32FC2); //创建sample_count行1列的双通道矩阵 用于存储数据样本 CvMat* clusters = cvCreateMat(sample_count,1,CV_32SC1);//创建sample_count行1列的矩阵 用来存储数据标签 //随机生成样本多元高斯分布 //样本总数为sample_count 类别总数为cluster_count //样本矩阵为points,先按类别分成cluster_count份 每一份数据的个数为sample_count/cluster_count //然后按类别随机矩阵填充样本矩阵,第一类填充矩阵的0~sample_count/cluster_count行 //第二类填充样本矩阵的sample_count/cluster_count~sample_count/cluster_count*2行,以此类推直到填充满所有矩阵,每一类的样本个数是一样的 for(k=0;k < cluster_count;k++) { CvPoint center; //为图像中的随机点 CvMat point_chunk; center.x = cvRandInt(&rng)%img->width; center.y = cvRandInt(&rng)%img->height; //返回数组在一定跨度的行 //points为输入数组,point_chunk返回数组 //开始行为k*sample_count/cluster_count //结束行为 当k== cluster-1时 为 第samplecount行 否则为(k+1)*sample_count/cluster_count cvGetRows(points,&point_chunk,k*sample_count/cluster_count, k == cluster_count -1? sample_count: (k+1)*sample_count/cluster_count); //point_chunk输出数组,CV_RAND_NORMAL分布类型为正态分布或者高斯分布 //cvScalar(center.x,center.y,0,0)随机数的平均值 // cvScalar(img->width/6,img->height/6,0,0)如果是正态分布它是随机数的标准差 cvRandArr(&rng,&point_chunk,CV_RAND_NORMAL, cvScalar(center.x,center.y,0,0), cvScalar(img->width/6,img->height/6,0,0)); } //样本重新排序 for(i=0;i<sample_count/2;i++) { //在points样本矩阵中随机取两个样本交换位置 CvPoint2D32f* pt1=(CvPoint2D32f*)points->data.fl+cvRandInt(&rng)%sample_count; CvPoint2D32f* pt2=(CvPoint2D32f*)points->data.fl+cvRandInt(&rng)%sample_count; CvPoint2D32f temp; CV_SWAP(*pt1,*pt2,temp); } //points样本矩阵,cliuster_count分类数,输出向量clusters,最后一个参数指定精度 cvKMeans2(points,cluster_count,clusters,cvTermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER,10,1.0)); cvZero(img); for(i=0;i<sample_count;i++) { CvPoint2D32f pt = ((CvPoint2D32f*)points->data.fl)[i]; int cluster_idx = clusters->data.i[i]; cvCircle(img,cvPointFrom32f(pt),2, color_table[cluster_idx],CV_FILLED); } cvReleaseMat(&points); cvReleaseMat(&clusters); cvShowImage("clusters",img); int key = cvWaitKey(0); if(key==27) break; } return a.exec(); }
然后我们进入数据生成for{}循环,这个循环可以用于其他算法中使用.我们给每个类别填写sample_count/cluster_count个数据样本,这些2维数据样本服从正态分布,正态分布的中心是随机选择的.
下一个for{}循环仅仅打乱了数据样本的顺序.然后我们使用cvKMean2(),直到聚类中心的最大移动小于1.
最后的for{}循环话出结果,在图中显示.然后释放数组,等待用户的输入进入下一次计算或者Esc键退出.