kmeans为无监督聚类最重要的算法,本文用kmeans算法对图像进行分割。算法原理参考:
https://blog.csdn.net/u013719780/article/details/51755124
https://blog.csdn.net/qq_36134318/article/details/80408658
https://blog.csdn.net/xiligey1/article/details/82457271
https://blog.csdn.net/wangxiaopeng0329/article/details/53542606
以上文章对Kmeans解释得很清楚,这里我主要说一下实例代码。
核心思想:kmeans以k为参数,把样本分为k个族(对于图像,每个像素点灰度值就是样本),使族内具有较高的相似度,而族与族之间相似度较低。
核心步骤:假如要分为2类,则
一:随机定义2个中心点,P1与P2。 并且P1代表A族,P2代表B族。
二:所有像素点值分别与这2个中心点计算距离(用欧式距离),与哪个中心点最近,则该像素点属于该中心点所对应的族
三:通过二:把图像所有像素点都分为A和B两类。属于A类的像素点计算中心位置,该中心位置就是更新的P1。属于B类的像素点计算中心位置,该中心位置就是更新的P2。
四:回到第二步,知道满足结束条件,即循环次数大于阈值,或者中心点位置移动的距离小于阈值。
注意:对于数据来说,计算距离就是二维空间X,Y的欧式距离
但是对于图像而言,计算距离是三维空间的欧式距离,X,Y值代表像素点的位置,Z轴代表该像素点的灰度值。
由此可见,基于kmeans的图像分割,可以看作为颜色分割,即把相似颜色(相似灰度值)分为一类。
下面是opencv C++代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main(int argc, char** argv) {
Mat SrcImage = imread("C:/Users/zhang/Desktop/1.png");
imshow("原图", SrcImage);
int width = SrcImage.cols;//图像宽
int height = SrcImage.rows;//图像高
int dims = SrcImage.channels();//图像通道数
//kmeans的输入数据:对于图像而言,每一个像素点都是一个数据,
int sampleCount = width * height;//图像数据点的个数
int clusterCount = 3;//分类数目
Mat points(sampleCount, dims, CV_32F, Scalar(10));//输入数据,与原图像有着相同通道
Mat labels;//输出数据,为各个数据点最终的分类索引
Mat centers(clusterCount, 1, points.type());//每个分类的中心点
Scalar colorbar[] =//每个分类的颜色
{
Scalar(0,0,255),
Scalar(203,192,255),
Scalar(255,0,255),
Scalar(0,255,0)
};
////RGB数据转换到样本数据
int index = 0;
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
index = row * width + col;//每个像素点
//把RGB图像的三个通道的各个像素点值分别赋给points的三个通道
Vec3b rgb = SrcImage.at<Vec3b>(row, col);
points.at<float>(index, 0) = static_cast<int>(rgb[0]);
points.at<float>(index, 1) = static_cast<int>(rgb[1]);
points.at<float>(index, 2) = static_cast<int>(rgb[2]);
}
}
TermCriteria criteria = TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 10, 1.0);
//10代表最大循环数目,1.0代表阈值
kmeans(points, clusterCount, labels, criteria, 3, KMEANS_PP_CENTERS, centers);
//第一个参数:表示输入的数据集合,可以一维或者多维数据,类型是Mat类型,
//比如Mat points(count, 2, CV_32F)表示数据集合是二维,浮点数数据集;
//第二个参数:表示分类的数目,K = 2时即表示二分类;
//第三个参数:表示计算之后各个数据点的最终的分类索引,是一个INT类型的Mat对象,类型和长宽与原图像一致
//第四个参数:表示算法终止的条件,达到最大循环数目或者指定的精度阈值算法就停止继续分类迭代计算;
//第五个参数:表示为了获得最佳的分类效果,算法要不同的初始分类尝试次数;
//第六个参数:表示表示选择初始中心点选择方法用哪一种方法:
//KMEANSRANDOMCENTERS 表示随机选择中心点
//KMEANSPPCENTERS 基于中心化算法选择
//KMEANSUSEINITIAL_LABELS第一次分类中心点用输入的中心点;
//第七个参数:表示输出的每个分类的中心点数据;
//显示图像分割结果//
Mat result = Mat::zeros(SrcImage.size(), SrcImage.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] = colorbar[label][0];
result.at<Vec3b>(row, col)[1] = colorbar[label][1];
result.at<Vec3b>(row, col)[2] = colorbar[label][2];
}
}
imshow("聚类结果", result);
imwrite("C:/Users/zhang/Desktop/xiannv5.png", result);
//centers按行保存着每个分类的终点坐标,每一行有一个坐标
for (int i = 0; i<centers.rows; i++)
{
int x = centers.at<float>(i, 0);
int y = centers.at<float>(i, 1);
cout << "x="<<x << "," << "y=" << y << endl;
}
waitKey(0);
return 0;
}
结果:
kmeans的进阶:
1:如何解决 开始随机定义中心点可能陷入局部最佳解的问题?
2:如何判断图片应该分为几类?
3:如何评估模型好坏?
以上三个问题,可以参考上面的链接
kmeans的缺点:kmeans能把所有样本都找到归属的族,这就造成了过度分割,即不能消除噪声。