Opencv之图像分割 --- KMeans方法_数据聚类&图像分割

一、 KMeans方法概述

1. 无监督学习方法(不需要人为的干预)

2. 分类问题,输入分类数目,初始化中心位置

3. 硬分类方法,以距离度量
(    硬分类:以距离为度量,距离离哪个中心点越近,他就被标记为哪个分类的编号;
    以距离度量:计算两个点之间的距离,如平面上x,y;空间上x,y,z;对RGB图像来说就是R、G、B三个通道,每两个像素点之间的差值
 )

4. 迭代分类为聚类

二、基本流程

1. 根据输入的分类数目定义K个分类,每个分类选择一个中心点

2. 对DS(Data Set)中每个数据点做如下操作:

        -计算它与K个中心点之间的距离

        -把数据点指定属于K个中心点中距离最近的中心点所属的分类

3. 对K个分类中每个数据点计算平均值得到新的K个中心点

4. 比较新K个中心点之间与第一步中已经存在的K个中心差值

        -当两者之间的差值没有变化或者小于指定阈值,或者迭代次数小于指定的次数,就结束分类

        -当两者之间的差值或者条件不满足时候,用新计算的中心点值做为K个分类的新中心点,继续执行2~4步。直到条件满足退出。

        从数学的角度来说KMeans就是要找到K个分类而且他们的中心点到各个分类中各个数据的之间差值平方和最小化,而实现这个过程就是要通过上述2~4步不断的迭代执行,直到收敛为止。公式表示如下:

 图解:

 

 

注意:
1. 初始的K个分类中每个分类的中心点选择,多数的算法实现都是支持随机选择与人工指定两种方式,OpenCV中的KMeans实现同样支持这两种方式。

2. 多维数据支持,多数时候我们要分类的特征对象的描述数据不止一个数据特征,而是一个特征向量来表示,OpenCV中通过Mat对象构建实现对多维数据KMeans分类支持。

3. 收敛条件 - 一般情况下在达到指定的迭代次数或者两次RSS差值小于给定阈值的情况下,结束执行分类处理,输出最终分类结果。

三、Opencv中相关API

double kmeans( InputArray data,	// data - 用于聚类的数据。需要具有浮点坐标的N维点数组。此数组的示例可以是:
      								- Mat points(count, 2, CV_32F);
      								- Mat points(count, 1, CV_32FC2);
    									- Mat points(1, count, CV_32FC2);
    									- std::vector<cv::Point2f> points(sampleCount);

					int K, //用来分割集合的集群数。常见的K = 2
					InputOutputArray bestLabels, //输入、输出整数数组,用于存储每个样本的聚类索引。
					TermCriteria criteria,  //算法终止标准,即最大迭代次数和/或所需精度。精度被指定为criteria.epsilon。
											一旦每个聚类中心在某个迭代上移动的距离小于criteria.epsilon,该算法就会停止。
					int attempts, //用于指定使用不同的初始标签执行算法的次数的标志。一般设定为2-3次,
								  该算法返回产生最佳紧凑性的标签(请参见最后一个功能参数)。
					int flags,  //可以采用以下值的标志
    							 KMEANS_RANDOM_CENTERS - 在每次尝试中选择随机的初始中心。
    						     KMEANS_PP_CENTERS - 使用Arthur和Vassilvitskii进行的kmeans ++中心初始化。
      						  KMEANS_USE_INITIAL_LABELS - 在第一次(可能也是唯一的)尝试期间,请使用用户提供的标签,而不要从初始中心进行
					OutputArray centers=noArray()
					)

 对矩阵Mat填充随机数:

void cv::RNG::fill( 
		InputOutputArray mat,
		int      distType, // 类型为RNG::UNIFORM,则表示产生均匀分布的随机数,如果为RNG::NORMAL则表示产生高斯分布的随机数
		InputArray      a, // 如果随机数产生模型为均匀分布,则参数a表示均匀分布的下限,参数b表示上限。
		InputArray      b, // 如果随机数产生模型为高斯模型,则参数a表示均值,参数b表示方差。
		bool   saturateRange = false // 只有当随机数产生方式为均匀分布时才有效,表示的是是否产生的数据要布满整个范围
	)

将原数组(矩阵)打乱 :

randShuffle(			
			InputOutputArray dst,    // 输入输出数组(一维)
			double iterFactor=1. ,   // 表示随机交换元素的数量的缩放因子,总的交换次数dst.rows*dst.cols*iterFactor
			RNG* rng=0               //(可选)随机数产生器,0表示使用默认的随机数产生器,即seed=-1。
									rng决定了打乱的方法
	)
	

 四、代码展示

1. 数据分类

代码:

#include<iostream>
#include<opencv2/opencv.hpp>

using namespace std;
using namespace cv;

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, 10);  //随机分类总数
	cout << "number of clusters:" << numCluster << endl;

	int sampleCount = rng.uniform(2, 1000); //随机生成样本总数

	//样本矩阵为points
	Mat points(sampleCount, 1, CV_32FC2);  //创建samplecount行1列的矩阵,因为待分类的数据只有一种类型,一维的,所以用1列 
											//双通道是存储坐标(x,y),其实就是元素类型为point2f的数据

	Mat labels; //存放表示每个簇的标签,是一个整数,从0开始的索引整数
				//行数跟points一样,每一行中储存对于Points行数中的数据是属于哪一种的分类

	Mat centers;//存放的是kmeans算法结束后每个簇的中心位置

	//随机生成样本
	//样本总数为sampleCount, 类别总数为numCluster
	//样本矩阵为points,先按类别分成numCluster份 每一份数据的个数为sampleCount / numCluster
	//然后按类别随机矩阵填充样本矩阵,第一类填充矩阵的0~sampleCount/numCluster行
	//第二类填充样本矩阵的sampleCount/numCluster~(sample_count/cluster_count)*2 行,
	//以此类推直到填充满所有矩阵,每一类的样本个数是一样的
	for (int k = 0; k < numCluster; k++)
	{
		//随机出中心点
		Point center;
		center.x = rng.uniform(0, img.cols);
		center.y = rng.uniform(0, img.rows);


		// Mat.rowRange 函数从 Mat 中抽取 startrow行 到 endrow 行的数据并返回(类型与mat一致),索取的位置是左闭右开,不包含endrow
		// Mat.rowRange 的返回值只是浅拷贝,指针指向还是原Mat,所以下面的 rng.fill 是对 points 的填充

		//返回数组在一定跨度的行
		//points为输入数组,pointChunk返回数组
		//开始行为 k*sampleCount/numCluster
		//结束行为 当k== numCluster - 1时 为 第sampleCount行 否则为(k + 1) * sampleCount / numCluster
		
		//得到不同小块
		Mat pointChunk = points.rowRange(k * sampleCount / numCluster, 
			k == numCluster - 1 ? sampleCount : (k + 1) * sampleCount / numCluster);
		
		//用随机数对小块点进行填充
		//在center坐标周围上下左右 img.cols*0.05, img.rows*0.05 的方差内生成高斯分布的随机数,最后赋值给pointChunk
		rng.fill(pointChunk, RNG::NORMAL, Scalar(center.x, center.y), Scalar(img.cols*0.05, img.rows*0.05));
	}
	
	//打乱points中的顺序
	randShuffle(points, 1, &rng);


	//使用KMeans
	kmeans(points, numCluster, labels, TermCriteria(TermCriteria::EPS + TermCriteria::COUNT, 10, 0.1), 3,
		KMEANS_PP_CENTERS, centers); 
		//TermCriteria类是用来作为迭代算法的终止条件的,参数:类型(EPS表示迭代到阈值终止),第二个参数为迭代的最大次数,最后一个是特定的阈值

	//用不同颜色显示分类
	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);

		cout << "c.x = " << x << "c.y = " << y << endl;
		circle(img, Point(x, y), 40, colorTab[i], 1, LINE_AA);
	}

	imshow("KMeans-Data-Demo:", img);

	waitKey(0);
	destroyAllWindows();
	return 0;
	 
}

 结果:

2. 图像分割

代码:

#include<iostream>
#include<opencv2/opencv.hpp>

using namespace std;
using namespace cv;

int main(int argc, char** argv)
{
	Mat src = imread("E:/技能学习/opencv图像分割/test.jpg");

	if (src.empty())
	{
		cout << "could not load image !" << endl;
		return -1;
	}

	namedWindow("input image", WINDOW_AUTOSIZE);
	imshow("input image",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; //类别
	Mat points(sampleCount, dims, CV_32F, Scalar(10)); //输入数组
	Mat labels; //存放表示每个簇的标签
	Mat centers(clusterCount,1,points.type()); //存放的是kmeans算法结束后每个簇的中心位置

	//RGB数据转换到样本数据
	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);
			points.at<float>(index, 0) = static_cast<int>(bgr[0]);
			points.at<float>(index, 1) = static_cast<int>(bgr[1]);
			points.at<float>(index, 2) = static_cast<int>(bgr[2]);
		} 
	}

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

	//显示图像分割结果
	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);
			result.at<Vec3b>(row, col)[0] = colorTab[label][0]; //对结果图中的每一个通道进行赋值
			result.at<Vec3b>(row, col)[1] = colorTab[label][1];
			result.at<Vec3b>(row, col)[2] = colorTab[label][2];
		}
	}

	//显示中心点位置
	for (int i = 0; i < centers.rows; i++)
	{
		int x = centers.at<float>(i, 0);
		int y = centers.at<float>(i, 1);
		cout << "c.x = " << x << " , "<< "c.y = " << y << endl;
		
	}

	imshow("KMeans Image Segmentation Demo", result);

	waitKey(0);
	destroyAllWindows();
	return 0;
}

结果:

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值