OpenCV4学习笔记(51)——基于K-Means聚类算法实现的图像分割

在上一篇博文《OpenCV4学习笔记(50)》中整理记录了关于K-Means聚类算法的一些相关内容,那么本次笔记就来针对K-Means算法在图像分割方面的应用进行一些小小的整理。

图像分割是指将一幅图像分割成若干互不相交区域的集合,其实质可以看成是一种像素的聚类过程,先来看一下图像分割在维基百科上的定义:

在计算机视觉领域,图像分割(segmentation)指的是将数字图像细分为多个图像子区域(像素的集合)(也被称作超像素)的过程。图像分割的目的是简化或改变图像的表示形式,使得图像更容易理解和分析。图像分割通常用于定位图像中的物体和边界(线,曲线等)。更精确的,图像分割是对图像中的每个像素加标签的一个过程,这一过程使得具有相同标签的像素具有某种共同视觉特性。图像分割的结果是图像上子区域的集合(这些子区域的全体覆盖了整个图像),或是从图像中提取的轮廓线的集合(例如边缘检测)。一个子区域中的每个像素在某种特性的度量下或是由计算得出的特性都是相似的,例如颜色、亮度、纹理。邻接区域在某种特性的度量下有很大的不同。

从中可以看出,图像分割就是在对图像中的像素点进行分类的过程,而如果需要划分的类别标签是我们预先不清楚的,那么这就是一个聚类的过程。
K-Means聚类算法正是一种可以将图像分割成K个聚类的迭代算法,对像素点进行聚类的基本流程如下:
(1)首先从一幅图像中任意选取 k 个像素点作为初始K个聚类的中心像素点,K值可以手动选取、随机选取、或其它方式得到;
(2)对于所剩下其它像素点,则根据它们与当前聚类中心像素点的相似度(距离)来进行度量,分别将它们分配给与其最相近的聚类中心像素点所代表的聚类。这里的相似度(距离)指某一像素点与聚类中心像素点之间的绝对偏差或偏差的平方,偏差通常用像素颜色、亮度、纹理、位置等属性,或这些属性的加权组合进行计算。
(3)然后再计算每个新聚类的聚类中心像素点(该聚类中所有像素点的均值);
(4)重复第2和3步骤,直至收敛(聚类不再发生变化)。

注意:使用K-Means算法进行聚类虽然能够保证收敛,但它返回的结果可能不是最佳的图像分割方案,其图像分割结果的质量取决于最初选取的聚类中心像素点和K值。

在经常使用到的图像分割方法中,可以分为基于边缘的分割方法和基于区域的分割方法,而使用K-Means聚类算法来实现图像分割则是属于基于区域的分割方法。

在OpenCV中,基于K-Means聚类算法实现图像分割主要包含以下几个步骤:
(1)将图像转化为数据集,也就是把图像中每个像素点作为一个数据样本形成一个1列、N行、3通道的Mat对象(N为像素点总数量),其每一行为一个样本数据;
注意:需要将数据集转换为浮点型,因为K-Means聚类算法只适用于连续性数据,否则会出现中心像素点漂移出样本集范围的错误
(2)使用K-Means聚类算法对数据集进行像素点聚类。得到的聚类中心结果centers中,每一行是一个类别的中心像素点,总共有K行,也就是K类,每一列是中心像素点的一个通道值,{0,1,2}三列分别对应{B,G,R}三通道,而且centers中所有元素都是float类型,如果使用intuchar类型返回,会导致数值错误,也即是获得的中心像素值出错;
(3)将聚类结果中不同类别的像素点都赋予不同的RGB值,而同一类别的像素点赋予同一RGB值,每一类别的代表RGB值可以是该类别中心像素点的RGB值。

下面来看代码演示:

	//读取图像,并获取该图像的宽、高、总像素数
	Mat test_image = imread("D:\\opencv_c++\\opencv_tutorial\\data\\images\\me.jpg");
	resize(test_image, test_image, Size(500, 600));
	imshow("test_image", test_image);
	int width = test_image.cols;
	int height = test_image.rows;
	int data_counts = width * height;
	//将图像转化成数据集,是一个列数为1、行数为总像素数的Mat对象,每一行的元素是一个像素点样本,具有三通道
	Mat data = test_image.reshape(3, data_counts);
	data.convertTo(data, CV_32F);

	Mat bestLabels, centers;
	int K = 10;			//聚类的类别数,类别数越大则结果越接近原图像
	TermCriteria criteria = TermCriteria(TermCriteria::Type::COUNT + TermCriteria::Type::EPS, 10, 0.01);
	kmeans(data, K, bestLabels, criteria, K, KMEANS_PP_CENTERS, centers);

	//获取每一个类别的中心点的BGR值,形成一个分类别像素值索引向量
	//聚类中心结果centers中,每一行是一个类别的中心像素点,总共有K行,也就是K类;
	//每一列是中心像素点的一个通道值,{0,1,2}三列分别对应{B,G,R}三通道值;
	//centers中所有元素都是float类型,如果使用int类型返回,会导致数值错误,则获得的中心像素值出错;
	vector<Vec3b> centers_bgr(K);
	for (int i = 0; i < K; i++)
	{
		uchar b = uchar(centers.at<float>(i,0));
		uchar g = uchar(centers.at<float>(i, 1));
		uchar r = uchar(centers.at<float>(i, 2));
		Vec3b bgr = { b,g,r };
		centers_bgr[i] = bgr;
	}
	
	//将输出结果图中的每一个像素点,按照其所属的类别来改变其像素值
	Mat dst = Mat::zeros(test_image.size(), test_image.type());
	for (int row = 0; row < height; row++)
	{
		for (int col = 0; col < width; col++)
		{
			int data_index = row * width + col;		//表示该像素点在数据集中所在的行索引,也就是第几个样本像素点
			int label = bestLabels.at<int>(data_index, 0);			//获取该像素点所属的类别标签
			dst.at<Vec3b>(row, col) = centers_bgr[label];		//将该像素点所属类别的代表像素值(类别中心像素值)赋给该像素点
		}
	}
	imshow("dst", dst);

在演示过程中,分别使用了K=2、K=3、K=5、K=10、K=15、K=20共六种不同的K值进行测试,下面是六种测试的结果:

  1. K=2时,可见整幅图像就只有两种颜色,看起来就好像是黑白图像。
    K=2
  2. K=3时,看起来多了些许层次感,但仍然看不出明显色彩。
    在这里插入图片描述
  3. K=5时,很明显看得出多种色彩了,给人一种油画感。
    在这里插入图片描述
  4. K=10时,图像变得更加充实了,不同物体也能依靠色彩分辫出来,相比之前多了些许立体感。
    在这里插入图片描述
  5. K=15时,这时候的分割图像在整体色彩方面已经很接近原图像了,在阴影方面也因为暗部被分割出来而变得更为立体。
    在这里插入图片描述
  6. K=20时,和之前相比已经没有很大的区别,但是更加地逼近原图像,甚至将原图像中某些区域的一些细微差别也给放大、分割成不同类别。
    在这里插入图片描述

从上面的演示图可以看出,K-Means聚类算法对于K值的不同取值是非常敏感的,能够对结果产生很大的影响。那么针对实际使用的时候,就应该依据实际的图像色彩来选取K值,假如我们要分割出的目标物体与背景的色彩差距比较大,那么我们就尽可能地减少K值,尽量的把图像分割为背景、目标前景这两大类别,这有助于我们对目标物体的定位、提取。

好了,本次笔记到此结束,谢谢阅读~

PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!

原创文章 58 获赞 118 访问量 7414

猜你喜欢

转载自blog.csdn.net/weixin_45224869/article/details/105799728