1. 程式人生 > >連通域標記演算法(二) 基於深度優先搜尋的連通域標記演算法(opencv C++實現)

連通域標記演算法(二) 基於深度優先搜尋的連通域標記演算法(opencv C++實現)

        上一篇我們講到了MATLAB中的bwlabel連通域標記演算法的C++實現https://blog.csdn.net/Dhane/article/details/81633723,今天我來講一講另一種相對比較容易想到的連通域標記演算法。簡單點說就是每次以一個需要標記的畫素點為種子,然後不斷向其周圍擴散,找出其他的與其相連通的可標記的畫素點,這樣就能標記出一個連通域,然後再以另一個連通域中的某一個需要標記的點為種子點,如此不斷迴圈,直到把整幅圖都遍歷一遍,所有連通域也就都標記出來了。

        可能因為我的語言表達能力有限,所以上面的簡單描述一些同學還是沒能夠太明白,不要著急,下面我們再來把這個過程分析一遍。

        還是先上一張老圖,這樣說起來更清晰一點:

        

上圖中,共有1,2,3三個連通成分,我們需要找出這張圖中的所有連通成分,那麼至少需要把影象中的每一個畫素都遍歷一遍,並且遍歷的時候還有判定該畫素點的值是否在集合V中。我們可以大概分為以下幾步來實現:

  1. 從左到右,從上到下遍歷每一個畫素點,然後判斷該畫素的值是否在集合V中(這裡是一張二值圖,我們假設黑色的值為0)。
  2. 如果遇到一個值為0的點(在上圖中為第2行第4個畫素點p[1][3]),說明在該點附近可能存在與該點相連通的畫素點,即可能存在連通域。那麼我們就暫時停止之前的遍歷,開始以該點為種子點,查詢該點附近的鄰域中是否存在與其連通的畫素點,若與該點連通,則將其存到一個堆疊中,並將該點的標籤賦值為與種子點p[1][3]相同的標籤,並對訪問過的畫素點置一個表示已訪問的標誌,以避免後面對其重複訪問。若以上圖為例,檢視p[1][3]的四鄰域(p[1][2]、p[0][3]、p[1][4]、p[2][3])的值是否也為0,很明顯,只有p[2][3]的值為0,說明p[1][3]和p[2][3]在同一個連通成分中,把p[2][3]的位置push到一個堆疊cdd中。
  3. 那麼我們接下來就從堆疊cdd中取出點p[2][3],然後檢視該點的四鄰域,發現p[2][4]和p[3][3]都與p[2][3]相連通,則依次把這兩個點push到堆疊cdd中。
  4. 下一次繼續從堆疊中取出點,並遍歷該點的四鄰域,如此迴圈,直到堆疊變為空,則說明已經遍歷完了所有的與我們的初始點p[1][3]相連通的連通成分。標籤label加1,以便對後面的連通成分標籤和該連通成分標籤加以區分。
  5. 接下來就可以繼續之前的從左到右、從上到下的遍歷工作了,我們開始從點p[1][4]開始訪問後面的點,直到訪問到p[2][3]時,這個點我們之前訪問過,其訪問標誌為1,所以不再進行對其訪問,直接跳過,其他之前訪問過的也同理。
  6. 當訪問到一個新的在集合V內的畫素點時,則又以該點為種子點進行四鄰域的遍歷迴圈,並標記上相應的標籤和訪問標誌,直到標記完整個連通成分,則標籤加1,繼續後面的遍歷。如此迴圈往復。直到訪問完最後一個畫素點,我們的連通成分也標記完了。

        這種方法當時也是我想出來的,為了完成任務嘛,就拿張紙在圖上亂畫,當時也沒學什麼演算法,後來在學習一些演算法的時候發現,這應該是屬於經典的深度優先搜尋演算法。又自戀了一下下,哈哈哈哈,每次發現自己所想的演算法和一些前輩們想出的演算法雷同的時候都特別興奮。我就想今後是不是我也能夠研究出一些新的經典演算法,哈哈哈。

        好了,不自戀了。下面簡單說一下我的程式碼實現。我是用一個數組來儲存所嗅探到的可標記畫素點的,相當於一個堆疊吧,然後再不斷從堆疊中拿出來作為新的種子點。不多說了,直接上程式碼吧。


//Based on opencv
//Created by HeQiang on 2018/03/03 in Wuhan 
//

#include <iostream>
#include <opencv2\highgui\highgui.hpp>
#include <opencv2\imgproc\imgproc.hpp>
#include <vector>

void getConnectedDomain2(cv::Mat& src_img, cv::Mat &flag_img, int iFlag)//
{
	int img_row = src_img.rows;
	int img_col = src_img.cols;
	int postemp1, postemp2;
	flag_img = cv::Mat::zeros(cv::Size(img_col, img_row), CV_8UC1);//標誌矩陣,為0則當前畫素點未訪問過
	uchar *ptrsrc = src_img.data;
	uchar *ptrflag = flag_img.data;
	cv::Point2f cdd[80000];                  //大小可根據實際影象大小來設定
	long int cddi = 0;
	int next_label = 1;    //連通域標籤
	int tflag;
	if (iFlag == 0)
		tflag = 0;
	else
		tflag = 255;       //需標記的畫素點所滿足的條件,可修改
	for (int i = 0; i < img_row; i++)
	{
		for (int j = 0; j < img_col; j++)
		{
			postemp1 = i*img_col + j;
			if (ptrsrc[postemp1] == tflag && ptrflag[postemp1] == 0)   //滿足條件且未被訪問過
			{
				cdd[cddi++] = cv::Point2f(j, i);
				ptrflag[postemp1] = next_label;
				while (cddi != 0)
				{
					cv::Point2f tmp = cdd[cddi - 1];
					cddi--;
					cv::Point2f p[4];//鄰域畫素點,這裡用的四鄰域
					p[0] = cv::Point2f(tmp.x - 1 > 0 ? tmp.x - 1 : 0, tmp.y);
					p[1] = cv::Point2f(tmp.x + 1 < img_col - 1 ? tmp.x + 1 : img_row - 1, tmp.y);
					p[2] = cv::Point2f(tmp.x, tmp.y - 1 > 0 ? tmp.y - 1 : 0);
					p[3] = cv::Point2f(tmp.x, tmp.y + 1 < img_row - 1 ? tmp.y + 1 : img_row - 1);
					for (int m = 0; m < 4; m++)
					{
						postemp2 = p[m].y*img_col + p[m].x;
						if (ptrsrc[postemp2] == tflag && ptrflag[postemp2] == 0)
						{
							cdd[cddi++] = p[m];
							ptrflag[postemp2] = next_label;
						}
					}
				}
				next_label++;
			}
		}
	}

	std::cout << "output_img data : " << std::endl;
	for (int i = 0; i < flag_img.rows; i++)
	{
		uchar *opt_img = flag_img.ptr<uchar>(i);
		for (int j = 0; j < flag_img.cols; j++)
		{
			std::cout << (int)opt_img[j] << " ";
		}
		std::cout << std::endl;
	}
}


int main()
{
	cv::Mat flag_img;
	cv::Mat src = cv::imread("test.png");        //輸入影象
	cv::resize(src, src, cv::Size(28, 28));                 //方便觀察,這裡把影象resize到28*28
//	flag_img = cv::Mat::zeros(src.size(), src.type());
	cv::cvtColor(src, src, CV_BGR2GRAY);    //這一句很重要,必須保證輸入的是單通道的圖,否則所讀取的資料是錯誤的
	getConnectedDomain2(src,flag_img,1);

	cv::imshow("src", src);
	cv::imshow("flag_img", flag_img);   //如果要觀察影象,可以給不同標籤附上不同顏色顯示

	cv::waitKey();

	return 0;
}

        上面程式碼用到了opencv來讀取影象,實際上如果只對一個矩陣進行處理的話應該也是可行的。深度優先搜尋的方法還可以使用遞迴來實現,當時也寫了一個遞迴實現的函式,由於遞迴次數過多,傳參容易出問題,程式容易崩潰,這裡就不把程式碼放上來了。大家也可以自己實現看一下。上面的程式碼經過了我的多次驗證。如果有什麼問題大家可以及時和我聯絡。

        非常希望能夠給大家一些啟發,也希望能夠互相交流,一起進步。