Opencv 关于Kmeans算法

KMeans算法是数据聚类的重要算法之一。本文将对该算法进行原理的简单介绍以及API函数的介绍,同时举出两个例子:基于kmeans对数据的聚类和基于kmeans对图像的分割。
(PS:作为学习Opencv的小白,及时记录所学习的东西,以便以后自己查阅方便。)

一.KMeans方法概述

  1. 无监督学习方法 (不需要人为干预定义什么类,自动执行)
  2. 分类问题,输入分类数目,初始化中心位置
  3. 硬分类方法,以距离度量
  4. 迭代分类为聚类(进行不断地迭代,中心的不断地变化)
    原理介绍:(PS:图片来源于网络)
    在这里插入图片描述
    在这里插入图片描述
    理解:首先根据输入的分类数目K定义K个分类,每个分类选择一个中心点,然后对样本中的中每个数据点做如下操作:计算它与K个中心点之间的距离,计算数据点指定属于K个中心点中距离最近的中心点所属的分类,对K个分类中每个数据点计算平均值得到新的K个中心点,比较新K个中心点之间与一开始中已经存在的K个中心差值。当两者之间的差值没有变化(即中心点位置固定不变)或者小于指定阈值,结束分类;当两者之间的差值或者条件不满足时候,用新计算的中心点值做为K个分类的新中心点,继续前面的步骤,直到条件满足退出。
    网上说法:从数学的角度来说KMeans就是要找到K个分类而且他们的中心点到各个分类中各个数据的之间差值平方和最小化,而实现这个过程就是要通过前面步骤的迭代执行,直到收敛为止。

二.Opencv相关的API
double cv::kmeans(InputArray data,
int K,
OutputArray bestLabels,
TermCriteria criteria,
int attempts,
int flags,
OutputArray centers
)
注:
data:表示需要被聚类的原始数据集合,一行表示一个数据样本,每一个样本的每一列都是一个属性。类型是Mat类型,比如Mat points(count, 2, CV_32F)表示数据集合是二维,浮点数数据集;
K:表示分类的数目,最常见的是K=2表示二分类;
bestLabels:表示计算之后各个数据点的最终的分类索引,是一个INT类型的Mat对象;
criteria:表示的是算法迭代终止条件,即迭代次数的限制条件、阈值,达到最大循环数目或者指定的精度阈值算法就停止继续分类迭代计算;
attempts:表示运行kmeans的次数,取结果最好的那次聚类为最终的聚类,要配合下一个参数flages来使用,一般2至3次;
flags:表示的是聚类初始化的条件。其取值有3种情况,如果为KMEANS_RANDOM_CENTERS,则表示为随机选取初始化中心点;如果为KMEANS_PP_CENTERS则表示使用某一种算法来确定初始聚类的点;如果为KMEANS_USE_INITIAL_LABELS,则表示使用用户自定义的初始点;
centers:表示输出的每个分类的中心点数据;

三.实例一:基于kmeans对数据的聚类
首先先介绍几个API函数,更有益于下面例程的理解。
程序中:Mat img(500, 500, CV_8UC3);
这里是创建一个500500像素的图像,关于CV_8UC3,Mat矩阵对应的参数类型就是CV_8UC1,CV_8UC2,CV_8UC3。(最后的1、2、3表示通道数,如RGB3通道就用CV_8UC3)。这里使用了CV_8UC3创建了一张3通道的图像,因为下面的步骤中我们需要对不同的数据类别指定不同的颜色,因此采用了3通道的图像文件格式。
程序中:rng.fill(pointChunk, RNG::NORMAL, Scalar(center.x, center.y), Scalar(img.cols
0.05, img.rows*0.05));
void RNG::fill(InputOutputArray mat, int distType, InputArray a, InputArray b, bool saturateRange=false )
****这个函数的作用是对矩阵Mat填充随机数。
第一个参数:输入的图像
第二个参数:随机数的产生方式由此参数决定,如果为该参数的类型为RNG::UNIFORM,则表示产生均一分布的随机数,如果为RNG::NORMAL则表示产生高斯分布的随机数。
第三个参数和第四个参数为上面两种随机数产生模型的参数。比如说如果随机数产生模型为均匀分布,则参数a表示均匀分布的下限,参数b表示上限。如果随机数产生模型为高斯模型,则参数a表示均值,参数b表示方程。
第五个参数:默认为false,只有当随机数产生方式为均匀分布时才有效,表示的是是否产生的数据要布满整个范围。
程序中:randShuffle(points, 1, &rng);

randShuffle( InputOutputArray dst,    //输入输出数组(一维) 
             double iterFactor=1 ,   //为随机打乱数据对数的因子,总共打乱的数据对数为:dst.rows*dst.cols*iterFactor,因此如果为0,表示没有打乱数据;
             RNG* rng=0 )    //(可选)随机数产生器,0表示使用默认的随机数产生器,即seed=-1。rng决定了打乱的方法
  //举例:
  Mat randShufM =(Mat_<double>(2,3) << 1,2,3,4,5,6);
 randShuffle(randShufM,7,0);  
 cout << "randShufM = " << endl<<randShufM<< endl << endl;
 //输出:randShufM =
 //        [5, 1, 6;
 //         2, 4, 3]

程序中:kmeans(points, numCluster, labels, TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1), 3,
KMEANS_PP_CENTERS, centers);
OpenCV中的TermCriteria模板类
这个类是作为迭代算法的终止条件的,这这里介绍一下。该类变量需要3个参数:
第一个参数是类型,类型有CV_TERMCRIT_ITER、CV_TERMCRIT_EPS、CV_TERMCRIT_ITER+CV_TERMCRIT_EPS,分别代表着迭代终止条件为达到最大迭代次数终止,迭代到阈值终止,或者两者都作为迭代终止条件。
第二个参数为迭代的最大次数。
第三个参数是特定的阈值。
以上的宏对应的c++的版本分别为TermCriteria::COUNT、TermCriteria::EPS,这里的COUNT也可以写成MAX_ITER。

附上源码:

//程序介绍:将调用Opencv的工具包来对一些随机数进行聚类,并将结果通过Opencv进行可视化。
//主要是生成一些二维样本数据,每个样本数据都服从高斯分布,我们将通过KMeans算法来对结果进行分类。
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv) 
{
 	Mat img(500, 500, CV_8UC3);  
 	RNG rng(12345);  //声明此类用于产生随机数
 	Scalar colorTab[] = {      //定义一个颜色的数组
  		Scalar(0, 0, 255),
  		Scalar(0, 255, 0),
  		Scalar(255, 0, 0),
  		Scalar(0, 255, 255),
  		Scalar(255, 0, 255)
  	
  	int numCluster = rng.uniform(2, 5);  //在[2,5)区间,随机生成一个整数
 	printf("number of clusters : %d\n", numCluster);

	int sampleCount = rng.uniform(5, 1000);
 	Mat points(sampleCount, 1, CV_32FC2);  //定义一个矩阵。
 	//points()函数,第一个参数为像素点,第二个参数为列数,第三个参数为类型
 	Mat labels;
 	Mat centers;
 
 	// 生成随机数
 	for (int k = 0; k < numCluster; k++)
 	{
 	 	Point center;   //中心点
  		center.x = rng.uniform(0, img.cols);    //随机点的抽取
  		center.y = rng.uniform(0, img.rows);
  		//图像上一小部分的点找到了
  		Mat pointChunk = points.rowRange(k*sampleCount / numCluster,k == numCluster - 1 ? sampleCount : (k + 1)*sampleCount / numCluster);
  		rng.fill(pointChunk, RNG::NORMAL, Scalar(center.x, center.y), Scalar(img.cols*0.05, img.rows*0.05));//用随机数填充矩阵
 	}
 	randShuffle(points, 1, &rng); //将原数组(矩阵)打乱
 	// 使用KMeans
 	kmeans(points, numCluster, labels, TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1), 3, KMEANS_PP_CENTERS, centers);
 	// 用不同颜色显示分类
 	img = Scalar::all(255);
 	for (int i = 0; i < sampleCount; i++)  //分类聚类编号,每个点都有编号
 	{
 		int index = labels.at<int>(i);
  		Point p = points.at<Point2f>(i);
  		circle(img, p, 2, colorTab[index], -1, 8);
 	}

	// 每个聚类的中心来绘制圆
 	for (int i = 0; i < centers.rows; i++) 
 	{
 		int x = centers.at<float>(i, 0);
  		int y = centers.at<float>(i, 1);
  		printf("c.x= %d, c.y=%d", x, y);
  		circle(img, Point(x, y), 40, colorTab[i], 1, LINE_AA);
 	}
 	imshow("效果图", img);
 	waitKey(0);
 	return 0;
};

运行结果:
在这里插入图片描述

四.实例二:基于kmeans对图像的分割
附上源码:

//程序思路说明:
//1.将输入图像转换为数据集合
//2.使用KMeans算法对数据实现分类
//3.根据每个数据点的分类索引,对图像重新填充颜色,显示分割后图像
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main(int argc, char** argv) 
{
	Mat src = imread("E:\\图像处理图片\\12.jpg");
 	if (src.empty()) 
 	{
 		printf("could not load image...\n");
  		return -1;
 	}
 	namedWindow("原图", WINDOW_AUTOSIZE);
 	imshow("原图", src);

	Scalar colorTab[] =  //定义五种颜色,用于分类的表示
	{
		Scalar(0, 0, 255),
  		Scalar(0, 255, 0),
  		Scalar(255, 0, 0),
  		Scalar(0, 255, 255),
  		Scalar(255, 0, 255)
	}

	int width = src.cols;
 	int height = src.rows;
 	int dims = src.channels();

	// 初始化定义
 	int sampleCount = width * height;  //总像素点
 	int clusterCount = 4;  //需要被分类的个数,为4
 	Mat points(sampleCount, dims, CV_32F, Scalar(10));
 	//points(多少个像素点,几列,类型,颜色)
 	//Scalar是将图像设置成单一灰度和颜色
 	//例如:Scalar(0)为黑色,Scalar(255)为白色,Scalar(0, 255, 0)为绿色
 	Mat labels;
 	Mat centers(clusterCount, 1, points.type());  //用来存储聚类后的中心点
 	
 	// 把图片的三通道RBG数据转到样本数据中,即挨个像素赋索引值
 	int index = 0;
 	for (int row = 0; row < height; row++) 
 	{
 		for (int col = 0; col < width; col++) 
 		{
 			index = row * width + col;  //为每个像素点赋索引值,一行一行的进行
   			Vec3b bgr = src.at<Vec3b>(row, col);// 8U 类型的 RGB 彩色图像可以使用 <Vec3b>
   			points.at<float>(index, 0) = static_cast<int>(bgr[0]);  //通道的B分量
   			points.at<float>(index, 1) = static_cast<int>(bgr[1]);
   			points.at<float>(index, 2) = static_cast<int>(bgr[2]);
   //cv::mat的成员函数: .at(int y, int x)可以用来存取图像中对应坐标为(x,y)的元素坐标。
 		}
 	}

	// 运行K-Means
 	TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1);
 	kmeans(points, clusterCount, labels, criteria, 3, KMEANS_PP_CENTERS, centers);

	// 显示图像分割结果:将样本中分好类的像素赋值给result图片,三通道赋值
 	Mat result = Mat::zeros(src.size(), src.type()); //初始化图片
 	for (int row = 0; row < height; row++) 
 	{
 		for (int col = 0; col < width; col++) 
 		{
 			index = row * width + col;
   			int label = labels.at<int>(index, 0);
   			result.at<Vec3b>(row, col)[0] = colorTab[label][0];//通道的B分量
   			result.at<Vec3b>(row, col)[1] = colorTab[label][1];//通道的G分量
   			result.at<Vec3b>(row, col)[2] = colorTab[label][2];//通道的R分量
 		}
 	}

	//打印算法执行到最后的四个中心点的最终坐标
 	for (int i = 0; i < centers.rows; i++) 
 	{
 		int x = centers.at<float>(i, 0);
  		int y = centers.at<float>(i, 1);
  		printf("center %d = c.x : %d, c.y : %d\n", i, x, y);
 	}

	imshow("效果图", result);
 	waitKey(0);
 	return 0;
 }

运行结果:
在这里插入图片描述
在这里插入图片描述

可以发现,图片被分割为四种颜色:红、绿、蓝、黄,刚好对应于一开始设置的颜色数组中的四种颜色。
分割成几种颜色取决Kmeans算法中的K的值大小(即第二个参数)

发布了25 篇原创文章 · 获赞 0 · 访问量 470

猜你喜欢

转载自blog.csdn.net/qq_45445740/article/details/103114328