下一节地址:https://blog.csdn.net/qq_40515692/article/details/102788157
OpenCv专栏:https://blog.csdn.net/qq_40515692/article/details/102885061
超像素(SuperPixel),就是把原本多个像素点,组合成一个大的像素。比如,原本的图片有二十多万个像素,用超像素处理之后,就只有几千个像素了。后面做直方图等处理就会方便许多。经常作为图像处理的预处理步骤。
这一节讲的是用C++实现超像素,下一节讲在超像素基础上用Kmeans分类进行分割,代码先根据超像素SLIC算法编写,后参考github的代码优化了一些地方,然后根据老师说的有更改了一些地方,欢迎大家一起讨论。
题目如下:
简单解法(HSV 直方图阈值)如下(至于为什么不用Matlab了,因为作为C系程序员,写c++真滴好爽呀):
https://blog.csdn.net/qq_40515692/article/details/102749271
这一节先讲SLIC超像素算法,下一节讲在超像素基础上用Kmeans分类进行分割,参考博客如下:
https://www.jianshu.com/p/d0ef931b3ddf
https://blog.csdn.net/duyue3052/article/details/82149877
效果如下(虽然还是有些可以更好的地方,但是可以看到已经分得很不错了,当然还有缺陷更少的算法可以更加好的分割比如像素点较少的蓝色线等的算法),完整代码附在下一节了:
在写较复杂的程序时,前期的百度、google参考别人思路、考虑算法的步骤十分关键,甚至应该用一半实现代码以上的时间。
如果看了之前的颜色阈值分割程序就会发现,之前的阈值分割没有考虑更高维的颜色数据,更重要的是没有利用各个像素点的位置信息(比如相邻的像素点更有可能属于一张分割图片),所以我们使用超像素算法,在保留图像像素的位置、颜色信息的同时,简化问题。
撒种子。将K个超像素中心分布到图像的像素点上。(这里我的实现里面直接先根据图像大小和超像素的数目,均匀发布)
微调种子的位置。以K为中心的3×3范围内,移动超像素中心到这9个点中梯度最小的点上。这样是为了避免超像素点落到噪点或者边界上。(这里我也进行了实现,但是对于最终结果貌似没有太大影响,篇幅有限就不进行讲解)
初始化数据。取一个数组label保存每一个像素点属于哪个超像素。dis数组保存像素点到它属于的那个超像素中心的距离。
对每一个超像素中心x,它2S范围内的点:如果点到超像素中心x的距离(5维,马上会讲)小于这个点到它原来属于的超像素中心的距离,那么说明这个点属于超像素x。更新dis,更新label。
对每一个超像素中心,重新计算它的位置(根据的是属于该超像素的所有像素的位置中心)以及其LAB值(马上会讲)。
重复4 5 两步。
其中关键的4,5步其实用到了kmeans算法的思想,如下图所示,假设有两个超像素点(红点、蓝点),一系列像素点(绿色),首先对每个像素点计算应该归属与哪一个超像素点、分类(如图片b、c所示)。
然后进行第五步计算中心,让超像素移动到中心,不断重复,最终成功划分。
但是应注意实际的SLIC算法和kmeans算法有区别,为了加快计算速度,在进行第4步时只计算了超像素中心有限范围内的点。(这是我实现算法时的理解,如果有误希望指出)
需要注意的是这里的“距离”可以是多维的数据距离,而不一定是比如像素之间的row、col之间的距离(比如RGB的欧式距离等)。
这里先简单介绍LAB色彩空间。Lab色彩模型是由亮度(L)和有关色彩的a, b三个要素组成。L表示亮度(Luminosity),L的值域由0(黑色)到100(白色)。a表示从洋红色至绿色的范围(a为负值指示绿色而正值指示品红),b表示从黄色至蓝色的范围(b为负值指示蓝色而正值指示黄色)。
Lab色彩模型的绝妙之处还在于它弥补了RGB色彩模型色彩分布不均的不足,因为RGB模型在蓝色到绿色之间的过渡色彩过多,而在绿色到红色之间又缺少黄色和其他色彩。如果我们想在数字图形的处理中保留尽量宽阔的色域和丰富的色彩,最好选择Lab。
然后就是如何计算”距离“,距离计算方法如下,其中,dc代表颜色距离,ds代表空间距离,Ns是类内最大空间距离,Nc为最大的颜色距离:
第一步还是先包含头文件,还有定义需要用到的变量,需要配置opencv,在VS上的配置可以参考:
https://blog.csdn.net/qq_40515692/article/details/81042303
#include
#include
using namespace cv;
using namespace std;
#define sqrtK 128 // 超像素个数128*128
#define sqrtN 512 // 图像格式512*512
int label[sqrtN][sqrtN]; // 图像各像素点归属
int dis[sqrtN][sqrtN]; // 图像各像素点距离
struct cluster{
int row, col, l, a, b;
};
cluster clusters[sqrtK*sqrtK]; // 存储超像素的像素坐标
我们先定义好大致框架,首先是读取图片,转换为LAB色彩空间,然后把上面提到的步骤分步定义为函数。
int main(){
// 注意修改文件位置
Mat src = imread("C:\\Users\\ttp\\Desktop\\map.bmp"), lab;
// resize图片并滤波
resize(src, src, Size(sqrtN, sqrtN));
// GaussianBlur(src, src, Size(3, 3), 1, 1);
// 得到Lab色彩空间,需要注意的是:
// 1.opencv里面默认为BGR排列方式
// 2.LAB通道范围取决于转换前的通道范围,这样其实也方便处理
// 例如:开始是0-255,转换后也是0-255,而不是LAB规定的[127,-128]
cvtColor(src, lab, CV_BGR2Lab);
int N = sqrtN * sqrtN; // 像素总数 512*512
int K = sqrtK * sqrtK; // 超像素个数 128*128
int S = sqrt(N / K); // 相邻种子点距离(超像素边长) 4
// 1.初始化像素
init_clusters(lab,S);
cout << "1-初始化像素-完成\n";
// 2.微调种子的位置 貌似好一点,没有太大区别
// 所以这里就直接注释了
// move_clusters(lab);
// cout << "2-微调种子的位置-完成\n";
for (int i = 0; i < 5; i++) {
// 3.4.初始化数据
update_pixel(lab, 2*S);
cout << "3-初始化数据-完成\n";
// 5.让超像素位于正中间
updaye_clusters(lab);
cout << "4-让超像素位于正中间-完成\n";
// -------------------这两个函数主要是帮助显示结果的
// 6.标识超像素
draw_clusters(src.clone());
cout << "5-标识超像素-完成\n";
// 7.绘制超像素结果图
final_draw(lab, lab.clone());
cout << "6-绘制超像素结果图-完成\n";
// opencv的函数,每1000ms更新一下,动态显示图片
waitKey(1000);
// -----------------------------------------------
}
imshow("原图", src);
waitKey(0);
}
init_clusters函数就是我们的第一步了,传入的参数为lab的色彩空间和S。
需要注意的是opencv里面Mat类的赋值并不是直接把Mat的数据全部拷贝一份赋值。
而是类似于C++的引用赋值(比如:Mat a,b; b=a; 改变b也会改变a)。
如果想赋值得到一个全新的图像矩阵,可以使用b=a.clone();这种方式。
所以这里就直接传lab了,效率应该不会低。
fill函数用于对一段空间赋值,这里即将矩阵dis赋-1。(在非opencv程序也可以使用)
void init_clusters(const Mat lab,int S) {
// 初始化每一个超像素的坐标
for (int i = 0; i < sqrtK; i++) {
int temp_row = S / 2 + i * S;
for (int j = 0; j < sqrtK; j++) {
clusters[i * sqrtK + j].row = temp_row;
clusters[i * sqrtK + j].col = S / 2 + j * S;
// cout << clusters[i * sqrtK + j].row << "\t" << clusters[i * sqrtK + j].col
// << "\t" << clusters[i * sqrtK + j].h << endl;
}
}
// 初始化每一个像素的label(即属于哪一个超像素)
for (int i = 0; i < sqrtN; i++) {
int cluster_row = i / S;
for (int j = 0; j < sqrtN; j++) {
label[i][j] = cluster_row * sqrtK + j / S;
// cout << cluster_row * sqrtK + j / S << endl;
}
}
// 像素与超像素的距离先假设为-1
fill(dis[0], dis[0] + (sqrtN * sqrtN), -1);
}
首先我们还是实现距离计算函数吧,这个函数传入参数为lab,clusters_index表示超像素的索引,
i,j表示像素的横纵坐标。
lab.at(row,col)属于opencv里面的写法,用于访问矩阵lab在坐标(row,col)的值
Vec3b表示3通道,每个通道为uchar类型(0-255)。为什么是Vec3b,参考完成大致框架里面的代码注释。
代码和上面的公式几乎没区别(权重取得有点随意)。
inline int get_distance(const Mat lab,int clusters_index,int i,int j) {
int dl = clusters[clusters_index].l - lab.at<Vec3b>(i, j)[0];
int da = clusters[clusters_index].a - lab.at<Vec3b>(i, j)[1];
int db = clusters[clusters_index].b - lab.at<Vec3b>(i, j)[2];
int dx = clusters[clusters_index].row - i;
int dy = clusters[clusters_index].col - j;
int h_distance = dl * dl + da * da + db * db;
int xy_distance = dx * dx + dy * dy;
//cout << h_distance << "\t" << xy_distance * 100 << endl;
return h_distance + xy_distance * 100;
}
然后就可以完成update_pixel函数了
void update_pixel(const Mat lab,int s) {
for (int i = 0; i < sqrtK * sqrtK; i++) {
// 对于每一个超像素
int clusters_x = clusters[i].row;
int clusters_y = clusters[i].col;
for (int x = -s; x <= s; x++) {
// 在它周围-s到s的范围内
for (int y = -s; y <= s; y++) {
int now_x = clusters_x + x;
int now_y = clusters_y + y;
if (now_x < 0 || now_x >= sqrtN || now_y < 0 || now_y >= sqrtN)
continue;
int new_dis = get_distance(lab, i, now_x, now_y);
// 如果为-1(还没有更新过)或者新的距离更小,就更换当前像素属于的超像素
if (dis[now_x][now_y] > new_dis || dis[now_x][now_y] == -1) {
dis[now_x][now_y] = new_dis;
label[now_x][now_y] = i;
}
}
}
}
}
这个函数就是根据当前超像素的所有归属像素来更新位置。
需要注意的是C++用new申请空间时后面加上()会自动初始化申请的空间。
还有就是记得delete
void updaye_clusters(const Mat lab) {
int *sum_count = new int[sqrtK * sqrtK]();
int *sum_i = new int[sqrtK * sqrtK]();
int *sum_j = new int[sqrtK * sqrtK]();
int* sum_l = new int[sqrtK * sqrtK]();
int* sum_a = new int[sqrtK * sqrtK]();
int* sum_b = new int[sqrtK * sqrtK]();
for (int i = 0; i < sqrtN; i++) {
for (int j = 0; j < sqrtN; j++) {
sum_count[label[i][j]]++;
sum_i[label[i][j]] += i;
sum_j[label[i][j]] += j;
sum_l[label[i][j]] += lab.at<Vec3b>(i, j)[0];
sum_a[label[i][j]] += lab.at<Vec3b>(i, j)[1];
sum_b[label[i][j]] += lab.at<Vec3b>(i, j)[2];
}
}
for (int i = 0; i < sqrtK * sqrtK; i++) {
if (sum_count[i] == 0) {
continue;
}
clusters[i].row = round(sum_i[i] / sum_count[i]);
clusters[i].col = round(sum_j[i] / sum_count[i]);
clusters[i].l = round(sum_l[i] / sum_count[i]);
clusters[i].a = round(sum_a[i] / sum_count[i]);
clusters[i].b = round(sum_b[i] / sum_count[i]);
}
delete[] sum_count;
delete[] sum_i;
delete[] sum_j;
delete[] sum_l;
delete[] sum_a;
delete[] sum_b;
}
OK, 到了这一步其实算法已经完成了。我们在实现一下用于显示的函数吧。draw_clusters函数就是画出每一个超像素点,final_draw函数就是绘制一张超像素分割图。
void draw_clusters(const Mat copy) {
for (int index = 0; index < sqrtK * sqrtK; index++) {
Point p(clusters[index].row, clusters[index].col);
circle(copy, p, 1, Scalar(0, 0, 255), 1); // 画半径为1的圆(画点)
}
imshow("超像素示意图", copy);
}
void final_draw(const Mat lab,Mat copy) {
for (int i = 0; i < sqrtN; i++) {
for (int j = 0; j < sqrtN; j++) {
int index = label[i][j];
copy.at<Vec3b>(i, j)[0] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[0];
copy.at<Vec3b>(i, j)[1] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[1];
copy.at<Vec3b>(i, j)[2] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[2];
}
}
cvtColor(copy, copy, CV_Lab2BGR);
imshow("分割图", copy);
}
2020 7.3更新:
完成了超像素框绘制的功能,这里给出汇总代码:
#include
#include
using namespace cv;
using namespace std;
#define sqrtK 32 // 超像素个数32*32
#define sqrtN 512 // 图像格式512*512
int label[sqrtN][sqrtN]; // 图像各像素点归属
int dis[sqrtN][sqrtN]; // 图像各像素点距离
struct cluster {
int row, col, l, a, b;
};
cluster clusters[sqrtK * sqrtK]; // 存储超像素的像素坐标、颜色
/**
* 初始化每一个超像素的坐标
* 初始化每一个像素的label(即属于哪一个超像素)
* 像素与超像素的距离先假设为-1
*/
void init_clusters(const Mat lab, int S) {
for (int i = 0; i < sqrtK; i++) {
int temp_row = S / 2 + i * S;
for (int j = 0; j < sqrtK; j++) {
clusters[i * sqrtK + j].row = temp_row;
clusters[i * sqrtK + j].col = S / 2 + j * S;
}
}
for (int i = 0; i < sqrtN; i++) {
int cluster_row = i / S;
for (int j = 0; j < sqrtN; j++) {
label[i][j] = cluster_row * sqrtK + j / S;
}
}
fill(dis[0], dis[0] + (sqrtN * sqrtN), -1);
}
inline int get_distance(const Mat lab, int clusters_index, int i, int j) {
int dl = clusters[clusters_index].l - lab.at<Vec3b>(i, j)[0];
int da = clusters[clusters_index].a - lab.at<Vec3b>(i, j)[1];
int db = clusters[clusters_index].b - lab.at<Vec3b>(i, j)[2];
int dx = clusters[clusters_index].row - i;
int dy = clusters[clusters_index].col - j;
int h_distance = dl * dl + da * da + db * db;
int xy_distance = dx * dx + dy * dy;
//cout << h_distance << "\t" << xy_distance * 100 << endl;
return h_distance + xy_distance * 100;
}
void update_pixel(const Mat lab, int s) {
for (int i = 0; i < sqrtK * sqrtK; i++) {
// 对于每一个超像素
int clusters_x = clusters[i].row;
int clusters_y = clusters[i].col;
for (int x = -s; x <= s; x++) {
// 在它周围-s到s的范围内
for (int y = -s; y <= s; y++) {
int now_x = clusters_x + x;
int now_y = clusters_y + y;
if (now_x < 0 || now_x >= sqrtN || now_y < 0 || now_y >= sqrtN)
continue;
int new_dis = get_distance(lab, i, now_x, now_y);
// 如果为-1(还没有更新过)或者新的距离更小,就更换当前像素属于的超像素
if (dis[now_x][now_y] > new_dis || dis[now_x][now_y] == -1) {
dis[now_x][now_y] = new_dis;
label[now_x][now_y] = i;
}
}
}
}
}
void updaye_clusters(const Mat lab) {
int* sum_count = new int[sqrtK * sqrtK]();
int* sum_i = new int[sqrtK * sqrtK]();
int* sum_j = new int[sqrtK * sqrtK]();
int* sum_l = new int[sqrtK * sqrtK]();
int* sum_a = new int[sqrtK * sqrtK]();
int* sum_b = new int[sqrtK * sqrtK]();
for (int i = 0; i < sqrtN; i++) {
for (int j = 0; j < sqrtN; j++) {
sum_count[label[i][j]]++;
sum_i[label[i][j]] += i;
sum_j[label[i][j]] += j;
sum_l[label[i][j]] += lab.at<Vec3b>(i, j)[0];
sum_a[label[i][j]] += lab.at<Vec3b>(i, j)[1];
sum_b[label[i][j]] += lab.at<Vec3b>(i, j)[2];
}
}
for (int i = 0; i < sqrtK * sqrtK; i++) {
if (sum_count[i] == 0) {
continue;
}
clusters[i].row = round(sum_i[i] / sum_count[i]);
clusters[i].col = round(sum_j[i] / sum_count[i]);
clusters[i].l = round(sum_l[i] / sum_count[i]);
clusters[i].a = round(sum_a[i] / sum_count[i]);
clusters[i].b = round(sum_b[i] / sum_count[i]);
}
delete[] sum_count;
delete[] sum_i;
delete[] sum_j;
delete[] sum_l;
delete[] sum_a;
delete[] sum_b;
}
void draw_clusters(const Mat copy) {
for (int index = 0; index < sqrtK * sqrtK; index++) {
Point p(clusters[index].row, clusters[index].col);
circle(copy, p, 1, Scalar(0, 0, 255), 1); // 画半径为1的圆(画点)
}
imshow("超像素示意图", copy);
}
void final_draw(const Mat lab, Mat copy) {
for (int i = 0; i < sqrtN; i++) {
for (int j = 0; j < sqrtN; j++) {
int index = label[i][j];
copy.at<Vec3b>(i, j)[0] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[0];
copy.at<Vec3b>(i, j)[1] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[1];
copy.at<Vec3b>(i, j)[2] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[2];
}
}
cvtColor(copy, copy, CV_Lab2BGR);
imshow("分割图", copy);
}
void draw_edge(const Mat lab, Mat copy) {
// 这里的代码和上面的函数几乎一样,都是同标签的绘制相应的超像素颜色,因为方便用户自己选用绘制函数所以没有调用上面的函数
for (int i = 0; i < sqrtN; i++) {
for (int j = 0; j < sqrtN; j++) {
int index = label[i][j];
copy.at<Vec3b>(i, j)[0] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[0];
copy.at<Vec3b>(i, j)[1] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[1];
copy.at<Vec3b>(i, j)[2] = lab.at<Vec3b>(clusters[index].row, clusters[index].col)[2];
}
}
// 这里的思路是4个方向,一旦有标签不同,就设置为黑色
static int X[] = {
0,0,-1,1 };
static int Y[] = {
1,-1,0,0 };
cvtColor(copy, copy, CV_Lab2BGR); // 改成BGR,方便后面设置边框的颜色。
for (int i = 0; i < sqrtN; i++) {
for (int j = 0; j < sqrtN; j++) {
int index = label[i][j];
for (int k = 0; k < 4; k++) {
if (index != label[i + X[k]][j + X[k]])
copy.at<Vec3b>(i, j)[0] = copy.at<Vec3b>(i, j)[1] = copy.at<Vec3b>(i, j)[2] = 0;
}
}
}
imshow("超像素边界", copy);
}
int main() {
// 注意修改文件位置
Mat src = imread("C:\\Users\\ttp\\Desktop\\map.bmp"), lab;
// resize图片并高斯滤波(可选)
resize(src, src, Size(sqrtN, sqrtN));
// GaussianBlur(src, src, Size(3, 3), 1, 1);
/**
得到Lab色彩空间,需要注意的是:
1.opencv里面默认为BGR排列方式
2.LAB通道范围取决于转换前的通道范围,这样其实也方便处理
例如:开始是0-255,转换后也是0-255,而不是LAB规定的[127,-128]
*/
cvtColor(src, lab, CV_BGR2Lab);
int N = sqrtN * sqrtN; // 像素总数 512*512
int K = sqrtK * sqrtK; // 超像素个数 128*128
int S = sqrt(N / K); // 相邻种子点距离(超像素边长) 4
// 1.初始化像素
init_clusters(lab, S);
cout << "1-初始化像素-完成\n";
// 2.微调种子的位置 貌似好一点,没有太大区别,所以这里就直接省略了
for (int i = 0; i < 5; i++) {
// 3.4.初始化数据
update_pixel(lab, 2 * S);
cout << "3-初始化数据-完成\n";
// 5.让超像素位于正中间
updaye_clusters(lab);
cout << "4-让超像素位于正中间-完成\n";
// -------------------这两个函数主要是帮助显示结果的
// 6.标识超像素
draw_clusters(src.clone());
cout << "5-标识超像素-完成\n";
// 7.绘制超像素结果图
final_draw(lab, lab.clone());
cout << "6-绘制超像素结果图-完成\n";
draw_edge(lab, lab.clone());
// opencv的函数,每1000ms更新一下,动态显示图片
waitKey(30);
// -----------------------------------------------
}
imshow("原图", src);
waitKey(0);
}
运行结果: