【OpenCv3】 VS C++ (五):SLIC超像素分割算法

下一节地址: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参考别人思路、考虑算法的步骤十分关键,甚至应该用一半实现代码以上的时间。

  • 为什么使用超像素?

如果看了之前的颜色阈值分割程序就会发现,之前的阈值分割没有考虑更高维的颜色数据,更重要的是没有利用各个像素点的位置信息(比如相邻的像素点更有可能属于一张分割图片),所以我们使用超像素算法,在保留图像像素的位置、颜色信息的同时,简化问题。

  • 超像素示意图:
    在这里插入图片描述
    这里贴出SLIC的步骤:
  1. 撒种子。将K个超像素中心分布到图像的像素点上。(这里我的实现里面直接先根据图像大小和超像素的数目,均匀发布)

  2. 微调种子的位置。以K为中心的3×3范围内,移动超像素中心到这9个点中梯度最小的点上。这样是为了避免超像素点落到噪点或者边界上。(这里我也进行了实现,但是对于最终结果貌似没有太大影响,篇幅有限就不进行讲解)

  3. 初始化数据。取一个数组label保存每一个像素点属于哪个超像素。dis数组保存像素点到它属于的那个超像素中心的距离。

  4. 对每一个超像素中心x,它2S范围内的点:如果点到超像素中心x的距离(5维,马上会讲)小于这个点到它原来属于的超像素中心的距离,那么说明这个点属于超像素x。更新dis,更新label。

  5. 对每一个超像素中心,重新计算它的位置(根据的是属于该超像素的所有像素的位置中心)以及其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 <opencv2/opencv.hpp>
#include <iostream>  

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);
}

三、各函数实现

1.init_clusters函数

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);
}

2.update_pixel函数

首先我们还是实现距离计算函数吧,这个函数传入参数为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;
				}
			}
		}
	}
}

3. updaye_clusters函数

这个函数就是根据当前超像素的所有归属像素来更新位置。

需要注意的是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;
}

4. 显示函数

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);
}

最后效果如下:

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章