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

最後效果如下:

在這裏插入圖片描述

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