【OpenCv3】 VS C++ (五):SLIC超像素分割算法
下一節(jié)地址:https://blog.csdn.net/qq_40515692/article/details/102788157
OpenCv專欄:https://blog.csdn.net/qq_40515692/article/details/102885061
超像素(SuperPixel),就是把原本多個(gè)像素點(diǎn),組合成一個(gè)大的像素。比如,原本的圖片有二十多萬個(gè)像素,用超像素處理之后,就只有幾千個(gè)像素了。后面做直方圖等處理就會(huì)方便許多。經(jīng)常作為圖像處理的預(yù)處理步驟。
這一節(jié)講的是用C++實(shí)現(xiàn)超像素,下一節(jié)講在超像素基礎(chǔ)上用Kmeans分類進(jìn)行分割,代碼先根據(jù)超像素SLIC算法編寫,后參考github的代碼優(yōu)化了一些地方,然后根據(jù)老師說的有更改了一些地方,歡迎大家一起討論。
題目如下:
簡(jiǎn)單解法(HSV 直方圖閾值)如下(至于為什么不用Matlab了,因?yàn)樽鳛镃系程序員,寫c++真滴好爽呀):
https://blog.csdn.net/qq_40515692/article/details/102749271
這一節(jié)先講SLIC超像素算法,下一節(jié)講在超像素基礎(chǔ)上用Kmeans分類進(jìn)行分割,參考博客如下:
https://www.jianshu.com/p/d0ef931b3ddf
https://blog.csdn.net/duyue3052/article/details/82149877
效果如下(雖然還是有些可以更好的地方,但是可以看到已經(jīng)分得很不錯(cuò)了,當(dāng)然還有缺陷更少的算法可以更加好的分割比如像素點(diǎn)較少的藍(lán)色線等的算法),完整代碼附在下一節(jié)了:
一、分析
在寫較復(fù)雜的程序時(shí),前期的百度、google參考別人思路、考慮算法的步驟十分關(guān)鍵,甚至應(yīng)該用一半實(shí)現(xiàn)代碼以上的時(shí)間。
- 為什么使用超像素?
如果看了之前的顏色閾值分割程序就會(huì)發(fā)現(xiàn),之前的閾值分割沒有考慮更高維的顏色數(shù)據(jù),更重要的是沒有利用各個(gè)像素點(diǎn)的位置信息(比如相鄰的像素點(diǎn)更有可能屬于一張分割圖片),所以我們使用超像素算法,在保留圖像像素的位置、顏色信息的同時(shí),簡(jiǎn)化問題。
- 超像素示意圖:
這里貼出SLIC的步驟:
撒種子。將K個(gè)超像素中心分布到圖像的像素點(diǎn)上。(這里我的實(shí)現(xiàn)里面直接先根據(jù)圖像大小和超像素的數(shù)目,均勻發(fā)布)
微調(diào)種子的位置。以K為中心的3×3范圍內(nèi),移動(dòng)超像素中心到這9個(gè)點(diǎn)中梯度最小的點(diǎn)上。這樣是為了避免超像素點(diǎn)落到噪點(diǎn)或者邊界上。(這里我也進(jìn)行了實(shí)現(xiàn),但是對(duì)于最終結(jié)果貌似沒有太大影響,篇幅有限就不進(jìn)行講解)
初始化數(shù)據(jù)。取一個(gè)數(shù)組label保存每一個(gè)像素點(diǎn)屬于哪個(gè)超像素。dis數(shù)組保存像素點(diǎn)到它屬于的那個(gè)超像素中心的距離。
對(duì)每一個(gè)超像素中心x,它2S范圍內(nèi)的點(diǎn):如果點(diǎn)到超像素中心x的距離(5維,馬上會(huì)講)小于這個(gè)點(diǎn)到它原來屬于的超像素中心的距離,那么說明這個(gè)點(diǎn)屬于超像素x。更新dis,更新label。
對(duì)每一個(gè)超像素中心,重新計(jì)算它的位置(根據(jù)的是屬于該超像素的所有像素的位置中心)以及其LAB值(馬上會(huì)講)。
重復(fù)4 5 兩步。
其中關(guān)鍵的4,5步其實(shí)用到了kmeans算法的思想,如下圖所示,假設(shè)有兩個(gè)超像素點(diǎn)(紅點(diǎn)、藍(lán)點(diǎn)),一系列像素點(diǎn)(綠色),首先對(duì)每個(gè)像素點(diǎn)計(jì)算應(yīng)該歸屬與哪一個(gè)超像素點(diǎn)、分類(如圖片b、c所示)。
然后進(jìn)行第五步計(jì)算中心,讓超像素移動(dòng)到中心,不斷重復(fù),最終成功劃分。
但是應(yīng)注意實(shí)際的SLIC算法和kmeans算法有區(qū)別,為了加快計(jì)算速度,在進(jìn)行第4步時(shí)只計(jì)算了超像素中心有限范圍內(nèi)的點(diǎn)。(這是我實(shí)現(xiàn)算法時(shí)的理解,如果有誤希望指出)
需要注意的是這里的“距離”可以是多維的數(shù)據(jù)距離,而不一定是比如像素之間的row、col之間的距離(比如RGB的歐式距離等)。
- 然后就需要考慮如何針對(duì)一張圖像,度量”距離“?
這里先簡(jiǎn)單介紹LAB色彩空間。Lab色彩模型是由亮度(L)和有關(guān)色彩的a, b三個(gè)要素組成。L表示亮度(Luminosity),L的值域由0(黑色)到100(白色)。a表示從洋紅色至綠色的范圍(a為負(fù)值指示綠色而正值指示品紅),b表示從黃色至藍(lán)色的范圍(b為負(fù)值指示藍(lán)色而正值指示黃色)。
Lab色彩模型的絕妙之處還在于它彌補(bǔ)了RGB色彩模型色彩分布不均的不足,因?yàn)镽GB模型在藍(lán)色到綠色之間的過渡色彩過多,而在綠色到紅色之間又缺少黃色和其他色彩。如果我們想在數(shù)字圖形的處理中保留盡量寬闊的色域和豐富的色彩,最好選擇Lab。
然后就是如何計(jì)算”距離“,距離計(jì)算方法如下,其中,dc代表顏色距離,ds代表空間距離,Ns是類內(nèi)最大空間距離,Nc為最大的顏色距離:
二、讀取圖片,完成大致框架
第一步還是先包含頭文件,還有定義需要用到的變量,需要配置opencv,在VS上的配置可以參考:
https://blog.csdn.net/qq_40515692/article/details/81042303
我們先定義好大致框架,首先是讀取圖片,轉(zhuǎn)換為LAB色彩空間,然后把上面提到的步驟分步定義為函數(shù)。
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里面默認(rèn)為BGR排列方式// 2.LAB通道范圍取決于轉(zhuǎn)換前的通道范圍,這樣其實(shí)也方便處理// 例如:開始是0-255,轉(zhuǎn)換后也是0-255,而不是LAB規(guī)定的[127,-128]cvtColor(src, lab, CV_BGR2Lab);int N = sqrtN * sqrtN; // 像素總數(shù) 512*512int K = sqrtK * sqrtK; // 超像素個(gè)數(shù) 128*128int S = sqrt(N / K); // 相鄰種子點(diǎn)距離(超像素邊長) 4// 1.初始化像素init_clusters(lab,S);cout << "1-初始化像素-完成\n";// 2.微調(diào)種子的位置 貌似好一點(diǎn),沒有太大區(qū)別// 所以這里就直接注釋了// move_clusters(lab);// cout << "2-微調(diào)種子的位置-完成\n";for (int i = 0; i < 5; i++) {// 3.4.初始化數(shù)據(jù)update_pixel(lab, 2*S);cout << "3-初始化數(shù)據(jù)-完成\n";// 5.讓超像素位于正中間updaye_clusters(lab);cout << "4-讓超像素位于正中間-完成\n";// -------------------這兩個(gè)函數(shù)主要是幫助顯示結(jié)果的// 6.標(biāo)識(shí)超像素draw_clusters(src.clone());cout << "5-標(biāo)識(shí)超像素-完成\n";// 7.繪制超像素結(jié)果圖final_draw(lab, lab.clone());cout << "6-繪制超像素結(jié)果圖-完成\n";// opencv的函數(shù),每1000ms更新一下,動(dòng)態(tài)顯示圖片waitKey(1000);// -----------------------------------------------}imshow("原圖", src);waitKey(0); }三、各函數(shù)實(shí)現(xiàn)
1.init_clusters函數(shù)
init_clusters函數(shù)就是我們的第一步了,傳入的參數(shù)為lab的色彩空間和S。
需要注意的是opencv里面Mat類的賦值并不是直接把Mat的數(shù)據(jù)全部拷貝一份賦值。
而是類似于C++的引用賦值(比如:Mat a,b; b=a; 改變b也會(huì)改變a)。
如果想賦值得到一個(gè)全新的圖像矩陣,可以使用b=a.clone();這種方式。
所以這里就直接傳lab了,效率應(yīng)該不會(huì)低。
fill函數(shù)用于對(duì)一段空間賦值,這里即將矩陣dis賦-1。(在非opencv程序也可以使用)
void init_clusters(const Mat lab,int S) {// 初始化每一個(gè)超像素的坐標(biāo)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;}}// 初始化每一個(gè)像素的label(即屬于哪一個(gè)超像素)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;}}// 像素與超像素的距離先假設(shè)為-1fill(dis[0], dis[0] + (sqrtN * sqrtN), -1); }2.update_pixel函數(shù)
首先我們還是實(shí)現(xiàn)距離計(jì)算函數(shù)吧,這個(gè)函數(shù)傳入?yún)?shù)為lab,clusters_index表示超像素的索引,
i,j表示像素的橫縱坐標(biāo)。
lab.at(row,col)屬于opencv里面的寫法,用于訪問矩陣lab在坐標(biāo)(row,col)的值
Vec3b表示3通道,每個(gè)通道為uchar類型(0-255)。為什么是Vec3b,參考完成大致框架里面的代碼注釋。
代碼和上面的公式幾乎沒區(qū)別(權(quán)重取得有點(diǎn)隨意)。
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函數(shù)了
void update_pixel(const Mat lab,int s) {for (int i = 0; i < sqrtK * sqrtK; i++) { // 對(duì)于每一個(gè)超像素int clusters_x = clusters[i].row;int clusters_y = clusters[i].col;for (int x = -s; x <= s; x++) { // 在它周圍-s到s的范圍內(nèi)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(還沒有更新過)或者新的距離更小,就更換當(dāng)前像素屬于的超像素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函數(shù)
這個(gè)函數(shù)就是根據(jù)當(dāng)前超像素的所有歸屬像素來更新位置。
需要注意的是C++用new申請(qǐng)空間時(shí)后面加上()會(huì)自動(dòng)初始化申請(qǐng)的空間。
還有就是記得delete
4. 顯示函數(shù)
OK, 到了這一步其實(shí)算法已經(jīng)完成了。我們?cè)趯?shí)現(xiàn)一下用于顯示的函數(shù)吧。draw_clusters函數(shù)就是畫出每一個(gè)超像素點(diǎn),final_draw函數(shù)就是繪制一張超像素分割圖。
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的圓(畫點(diǎn))}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 <opencv2/opencv.hpp> #include <iostream> using namespace cv; using namespace std;#define sqrtK 32 // 超像素個(gè)數(shù)32*32 #define sqrtN 512 // 圖像格式512*512int label[sqrtN][sqrtN]; // 圖像各像素點(diǎn)歸屬 int dis[sqrtN][sqrtN]; // 圖像各像素點(diǎn)距離struct cluster {int row, col, l, a, b; }; cluster clusters[sqrtK * sqrtK]; // 存儲(chǔ)超像素的像素坐標(biāo)、顏色/*** 初始化每一個(gè)超像素的坐標(biāo)* 初始化每一個(gè)像素的label(即屬于哪一個(gè)超像素)* 像素與超像素的距離先假設(shè)為-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++) { // 對(duì)于每一個(gè)超像素int clusters_x = clusters[i].row;int clusters_y = clusters[i].col;for (int x = -s; x <= s; x++) { // 在它周圍-s到s的范圍內(nèi)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(還沒有更新過)或者新的距離更小,就更換當(dāng)前像素屬于的超像素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的圓(畫點(diǎn))}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) {// 這里的代碼和上面的函數(shù)幾乎一樣,都是同標(biāo)簽的繪制相應(yīng)的超像素顏色,因?yàn)榉奖阌脩糇约哼x用繪制函數(shù)所以沒有調(diào)用上面的函數(shù)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個(gè)方向,一旦有標(biāo)簽不同,就設(shè)置為黑色static int X[] = { 0,0,-1,1 };static int Y[] = { 1,-1,0,0 };cvtColor(copy, copy, CV_Lab2BGR); // 改成BGR,方便后面設(shè)置邊框的顏色。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里面默認(rèn)為BGR排列方式2.LAB通道范圍取決于轉(zhuǎn)換前的通道范圍,這樣其實(shí)也方便處理例如:開始是0-255,轉(zhuǎn)換后也是0-255,而不是LAB規(guī)定的[127,-128]*/cvtColor(src, lab, CV_BGR2Lab);int N = sqrtN * sqrtN; // 像素總數(shù) 512*512int K = sqrtK * sqrtK; // 超像素個(gè)數(shù) 128*128int S = sqrt(N / K); // 相鄰種子點(diǎn)距離(超像素邊長) 4// 1.初始化像素init_clusters(lab, S);cout << "1-初始化像素-完成\n";// 2.微調(diào)種子的位置 貌似好一點(diǎn),沒有太大區(qū)別,所以這里就直接省略了for (int i = 0; i < 5; i++) {// 3.4.初始化數(shù)據(jù)update_pixel(lab, 2 * S);cout << "3-初始化數(shù)據(jù)-完成\n";// 5.讓超像素位于正中間updaye_clusters(lab);cout << "4-讓超像素位于正中間-完成\n";// -------------------這兩個(gè)函數(shù)主要是幫助顯示結(jié)果的// 6.標(biāo)識(shí)超像素draw_clusters(src.clone());cout << "5-標(biāo)識(shí)超像素-完成\n";// 7.繪制超像素結(jié)果圖final_draw(lab, lab.clone());cout << "6-繪制超像素結(jié)果圖-完成\n";draw_edge(lab, lab.clone());// opencv的函數(shù),每1000ms更新一下,動(dòng)態(tài)顯示圖片waitKey(30);// -----------------------------------------------}imshow("原圖", src);waitKey(0); }運(yùn)行結(jié)果:
總結(jié)
以上是生活随笔為你收集整理的【OpenCv3】 VS C++ (五):SLIC超像素分割算法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Eclipse使用:Eclipse安装中
- 下一篇: c语言入门-程序运行的过程