[opencv][ml][cpp] 学习手册3:K-Means 聚类算法
24_kmeans聚类算法.cpp
文章目录
注:部分概念截取自教程,侵删!
1. kmeans聚类.cpp
1. 概念
K-均值聚类算法
-
著名的划分聚类分割方法。
-
基本思想:
- 给定一个有N个元组或者纪录的数据集,
- 分裂法将构造K个分组,每一个分组就代表一个聚类,K<N。
- 而且这K个分组满足下列条件:
- 每一个分组至少包含一个数据纪录
- 每一个数据纪录属于且仅属于一个分组
-
对于给定的K,算法首先给出一个初始的分组方法,以后通过反复迭代的方法改变分组,
使得每一次改进之后的分组方案都较前一次好 -
好的标准就是:同一分组中的记录越近越好,而不同分组中的纪录越远越好。
原理
- 首先随机从数据集中选取 K个点作为初始聚类中心
- 然后计算各个样本到聚类中心的距离,把样本归到离它最近的那个聚类中心所在的类。
- 计算新形成的每一个聚类的数据对象的平均值来得到新的聚类中心,
- 如果相邻两次的聚类中心没有任何变化,说明样本调整结束,聚类准则函数 已经收敛。
-
算法特点:
- 每次迭代中都要考察每个样本的分类是否正确。若不正确,就要调整,在全部样本调整完后,再修改聚类中心,进入下一次迭代。
- 这个过程将不断重复直到满足某个终止条件。
-
终止条件:
- 没有对象被重新分配给不同的聚类
- 聚类中心不再发生变化
- 误差平方和局部最小
一般步骤
- 从 n个数据对象任意选择 k 个对象作为初始聚类中心;
- 循环(3)到(4)直到每个聚类不再发生变化为止;
- 根据每个聚类对象的均值(中心对象),计算每个对象与这些中心对象的距离;
并根据最小距离重新对相应对象进行划分; - 重新计算每个(有变化)聚类的均值(中心对象),直到聚类中心不再变化。
2. 熟悉 cv::kmeans()
需求
使用 cv kmeans api 对一幅色块图像进行聚类(4类),并对每一个群集打上标签。
cv函数
终止条件:
- COUNT: 达到最大迭代数,或最大计算数
- MAX_ITER:达到最大迭代数
- EPS:达到需要的精度,或迭代算法终止时的参数的改变
终止条件构造函数:
- type:终止条件类型,见上
- maxCount:最大迭代数,或最大计算数
- epsilon:精度
kmeans 标记
- 每一次尝试选择随机中心
- 看paper
- 第一次尝试使用给定标签,而不是使用计算出的初始中心。之后的尝试,使用随机或半随机中心。
3. 代码实现:
/*
* created by jacob 01/04/21
*/
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/utils/logger.hpp>
using namespace std;
namespace cvlog = cv::utils::logging;
int main(int argc, char **argv) {
cvlog::setLogLevel(cvlog::LOG_LEVEL_INFO);
string filename = "../img/color_block_2.jpg";
cv::Mat src = cv::imread(filename, cv::IMREAD_COLOR);
cv::resize(src, src, cv::Size(src.cols / 4, src.rows / 4));
cv::imshow("src", src);
CV_LOG_INFO(NULL, "src.size()" << src.size())
// 用于存放样本数据
cv::Mat sampleData(src.rows * src.cols, 1, CV_32FC3);
// 将原图像行列数据 压扁
for (int row = 0; row < src.rows; ++row) {
for (int col = 0; col < src.cols; ++col) {
cv::Vec3b color = src.at<cv::Vec3b>(row, col);
int index = row * src.cols + col;
// 将颜色值存放到data
sampleData.at<cv::Vec3f>(index) = color;
}
}
// 指定聚类算法 终止条件
cv::TermCriteria termCriteria(cv::TermCriteria::EPS | cv::TermCriteria::COUNT, 14, 0.1);
cv::Mat labels;
int K = 4;
cv::kmeans(sampleData, K, labels, termCriteria, 3, cv::KMEANS_RANDOM_CENTERS);
CV_LOG_INFO(NULL, "labels.type(), size(): " << labels.type() << labels.size())
CV_LOG_INFO(NULL, "labels.type(): \n" << labels.reshape(src.rows))
// 给每一类标记分配相同颜色
cv::RNG rng(1234132);
vector<cv::Vec3b> colors = {
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
};
cv::Mat kmeansResult(src.size(), CV_8UC3);
for (int row = 0; row < src.rows; ++row) {
for (int col = 0; col < src.cols; ++col) {
// 获取当前位置的label 信息 labels [0,1,23,456] src[row,col]
int index = row * src.cols + col;
// 获取对应的label标记
int label = labels.at<int>(index);
// 绘制输出图像
kmeansResult.at<cv::Vec3b>(row, col) = colors[label];
}
}
cv::imshow("result", kmeansResult);
// pending
cv::waitKey(0);
return 0;
}
运行结果
原始图像为6色块图,结果图像为聚类后的4色块图
解释说明(代码)
- 读取 src,对 src 进行缩放(保持三通道不变)
- 创建一个样本矩阵 sampleData(三通道),长度为 src 的 rows * cols,类型为 CV_32FC3
- 使用 for-for 将 src 每个像素点的值(cv::Vec3b)存放进 sampleData
二维索引转一维索引:index = row * cols + colint index = row * src.cols + col;
- 进行聚类运算
- 定义终止条件
cv::TermCriteria termCriteria(cv::TermCriteria::EPS | cv::TermCriteria::COUNT, 14, 0.1);
参数解释见 cv函数
- 调用 cv::kmeans api 进行聚类计算
cv::kmeans(sampleData, K, labels, termCriteria, 3, cv::KMEANS_RANDOM_CENTERS);
K:聚类数
labels:标签矩阵
attempts:算法执行次数
flags:见cv函数
- 定义终止条件
- 创建三通道结果矩阵 kmeansResult 用来存放聚类后的矩阵
cv::RNG rng(1234132); vector<cv::Vec3b> colors = { cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), }; cv::Mat kmeansResult(src.size(), CV_8UC3);
2. 附加功能实现(更改背景颜色、抠图)
1. 代码实现
代码片段
{
// 上接
// 制作二值图掩膜,用来更改背景颜色
cv::Vec3b backgroundColor = kmeansResult.at<cv::Vec3b>(10,10);
cv::Mat binary(src.size(),CV_8UC1);
for (int row = 0; row < src.rows; ++row) {
for (int col = 0; col < src.cols; ++col) {
cv::Vec3b color = kmeansResult.at<cv::Vec3b>(row,col);
if(color == backgroundColor){
binary.at<uchar>(row,col) = 0;
}else{
binary.at<uchar>(row,col) = 255;
}
}
}
imshow("binary",binary);
// 使用掩膜、蒙版 抠出彩图
cv::Mat copyResult;
src.copyTo(copyResult,binary);
imshow("copy",copyResult);
for (int row = 0; row < copyResult.rows; ++row) {
for (int col = 0; col < copyResult.cols; ++col) {
cv::Vec3b color = copyResult.at<cv::Vec3b>(row,col);
if(color == cv::Vec3b(0,0,0)){
copyResult.at<cv::Vec3b>(row,col)=colors[1];
}
}
}
imshow("result1",copyResult);
// 下接
}
运行结果
解释说明(代码)
- 在制作二值图掩膜(mask)代码块中,使用 cv::Vect3b 类型接收三通道像素值
- 使用 copyTo api 对原图像进行带掩膜的拷贝
src.copyTo(copyResult,binary); // src: 原图像C3,copyResult:目标图像,binary:掩膜
使用 kmeans 方式抠图,可以达到像素级别。