1. 程式人生 > >OpenCV-二值影象連通域分析

OpenCV-二值影象連通域分析

通域分析對於影象處理後面涉及到模式識別的內容來說是基礎

連通區域(Connected Component)一般是指影象中具有相同畫素值且位置相鄰的前景畫素點組成的影象區域(Region,Blob)。連通區域分析(Connected Component Analysis,Connected Component Labeling)是指將影象中的各個連通區域找出並標記。
連通區域分析是一種在CVPR和影象分析處理的眾多應用領域中較為常用和基本的方法。例如:OCR識別中字元分割提取(車牌識別、文字識別、字幕識別等)、視覺跟蹤中的運動前景目標分割與提取(行人入侵檢測、遺留物體檢測、基於視覺的車輛檢測與跟蹤等)、醫學影象處理(感興趣目標區域提取)、等等。也就是說,在需要將前景目標提取出來以便後續進行處理的應用場景中都能夠用到連通區域分析方法,通常連通區域分析處理的物件是一張二值化後的影象。

看了幾篇部落格總結如下:


理論充能任務

任務一

二值影象,顧名思義就是影象的亮度值只有兩個狀態:黑(0)和白(255)。二值影象在影象分析與識別中有著舉足輕重的地位,因為其模式簡單,對畫素在空間上的關係有著極強的表現力。在實際應用中,很多影象的分析最終都轉換為二值影象的分析,比如:醫學影象分析、前景檢測、字元識別,形狀識別。二值化+數學形態學能解決很多計算機識別工程中目標提取的問題。

二值影象分析最重要的方法就是連通區域標記,它是所有二值影象分析的基礎,它通過對二值影象中白色畫素(目標)的標記,讓每個單獨的連通區域形成一個被標識的塊,進一步的我們就可以獲取這些塊的輪廓、外接矩形、質心、不變矩等幾何引數。

任務二

在我們討論連通區域標記的演算法之前,我們先要明確什麼是連通區域,怎樣的畫素鄰接關係構成連通。在影象中,最小的單位是畫素,每個畫素周圍有8個鄰接畫素,常見的鄰接關係有2種:4鄰接與8鄰接。4鄰接一共4個點,即上下左右,如下左圖所示。8鄰接的點一共有8個,包括了對角線位置的點,如下右圖所示。

                                                                                                  

如果畫素點A與B鄰接,我們稱A與B連通,於是我們不加證明的有如下的結論:

如果A與B連通,B與C連通,則A與C連通。

在視覺上看來,彼此連通的點形成了一個區域,而不連通的點形成了不同的區域。這樣的一個所有的點彼此連通點構成的集合,我們稱為一個連通區域。

下面這符圖中,如果考慮4鄰接,則有3個連通區域;如果考慮8鄰接,則有2個連通區域。(注:影象是被放大的效果,影象正方形實際只有4個畫素)



任務三

二、連通區域分析的演算法
從連通區域的定義可以知道,一個連通區域是由具有相同畫素值的相鄰畫素組成畫素集合,因此,我們就可以通過這兩個條件在影象中尋找連通區域,對於找到的每個連通區域,我們賦予其一個唯一的標識(Label),以區別其他連通區域。
連通區域分析有基本的演算法,也有其改進演算法,本文介紹其中的兩種常見演算法:
1)Two-Pass法;2)Seed-Filling種子填充法;

1)Two-Pass(兩遍掃描法)
兩遍掃描法,正如其名,指的就是通過掃描兩遍影象,就可以將影象中存在的所有連通區域找出並標記。思路:第一遍掃描時賦予每個畫素位置一個label,掃描過程中同一個連通區域內的畫素集合中可能會被賦予一個或多個不同label,因此需要將這些屬於同一個連通區域但具有不同值的label合併,也就是記錄它們之間的相等關係;第二遍掃描就是將具有相等關係的equal_labels所標記的畫素歸為一個連通區域並賦予一個相同的label(通常這個label是equal_labels中的最小值)。

下面給出Two-Pass演算法的簡單步驟:
(1)第一次掃描:
訪問當前畫素B(x,y),如果B(x,y) == 1:
a、如果B(x,y)的領域中畫素值都為0,則賦予B(x,y)一個新的label:
label += 1, B(x,y) = label;
b、如果B(x,y)的領域中有畫素值 > 1的畫素Neighbors:
1)將Neighbors中的最小值賦予給B(x,y):
B(x,y) = min{Neighbors} 
2)記錄Neighbors中各個值(label)之間的相等關係,即這些值(label)同屬同一個連通區域;
 labelSet[i] = { label_m, .., label_n },labelSet[i]中的所有label都屬於同一個連通區域(注:這裡可以有多種實現方式,只要能夠記錄這些具有相等關係的label之間的關係即可)
(2)第二次掃描:
訪問當前畫素B(x,y),如果B(x,y) > 1:
a、找到與label = B(x,y)同屬相等關係的一個最小label值,賦予給B(x,y);
完成掃描後,影象中具有相同label值的畫素就組成了同一個連通區域。


下面這張圖動態地演示了Two-pass演算法:


具體實現還是得一步一步的看程式碼

2)Seed Filling(種子填充法)
種子填充方法來源於計算機圖形學,常用於對某個圖形進行填充。思路:選取一個前景畫素點作為種子,然後根據連通區域的兩個基本條件(畫素值相同、位置相鄰)將與種子相鄰的前景畫素合併到同一個畫素集合中,最後得到的該畫素集合則為一個連通區域。


下面給出基於種子填充法的連通區域分析方法:
(1)掃描影象,直到當前畫素點B(x,y) == 1:
a、將B(x,y)作為種子(畫素位置),並賦予其一個label,然後將該種子相鄰的所有前景畫素都壓入棧中;
b、彈出棧頂畫素,賦予其相同的label,然後再將與該棧頂畫素相鄰的所有前景畫素都壓入棧中;
c、重複b步驟,直到棧為空;
此時,便找到了影象B中的一個連通區域,該區域內的畫素值被標記為label;
(2)重複第(1)步,直到掃描結束;
掃描結束後,就可以得到影象B中所有的連通區域;


下面這張圖動態地演示了Seed-Filling演算法:


完整測試程式碼如下:

#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <map>
#include <stack>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>


using namespace std;
using namespace cv;


//------------------------------【兩步法新改進版】----------------------------------------------
// 對二值影象進行連通區域標記,從1開始標號
void  Two_PassNew( const Mat &bwImg, Mat &labImg )
{
	assert( bwImg.type()==CV_8UC1 );
	labImg.create( bwImg.size(), CV_32SC1 );   //bwImg.convertTo( labImg, CV_32SC1 );
	labImg = Scalar(0);
	labImg.setTo( Scalar(1), bwImg );
	assert( labImg.isContinuous() );
	const int Rows = bwImg.rows - 1, Cols = bwImg.cols - 1;
	int label = 1;
	vector<int> labelSet;
	labelSet.push_back(0);
	labelSet.push_back(1);
	//the first pass
	int *data_prev = (int*)labImg.data;   //0-th row : int* data_prev = labImg.ptr<int>(i-1);
	int *data_cur = (int*)( labImg.data + labImg.step ); //1-st row : int* data_cur = labImg.ptr<int>(i);
	for( int i = 1; i < Rows; i++ )
	{
		data_cur++;
		data_prev++;
		for( int j=1; j<Cols; j++, data_cur++, data_prev++ )
		{
			if( *data_cur!=1 )
				continue;
			int left = *(data_cur-1);
			int up = *data_prev;
			int neighborLabels[2];
			int cnt = 0;
			if( left>1 )
				neighborLabels[cnt++] = left;
			if( up > 1)
				neighborLabels[cnt++] = up;
			if( !cnt )
			{
				labelSet.push_back( ++label );
				labelSet[label] = label;
				*data_cur = label;
				continue;
			}
			int smallestLabel = neighborLabels[0];
			if( cnt==2 && neighborLabels[1]<smallestLabel )
				smallestLabel = neighborLabels[1];
			*data_cur = smallestLabel;
			// 儲存最小等價表
			for( int k=0; k<cnt; k++ )
			{
				int tempLabel = neighborLabels[k];
				int& oldSmallestLabel = labelSet[tempLabel];  //這裡的&不是取地址符號,而是引用符號
				if( oldSmallestLabel > smallestLabel )
				{
					labelSet[oldSmallestLabel] = smallestLabel;
					oldSmallestLabel = smallestLabel;
				}
				else if( oldSmallestLabel<smallestLabel )
					labelSet[smallestLabel] = oldSmallestLabel;
			}
		}
		data_cur++;
		data_prev++;
	}
	//更新等價隊列表,將最小標號給重複區域
	for( size_t i = 2; i < labelSet.size(); i++ )
	{
		int curLabel = labelSet[i];
		int prelabel = labelSet[curLabel];
		while( prelabel != curLabel )
		{
			curLabel = prelabel;
			prelabel = labelSet[prelabel];
		}
		labelSet[i] = curLabel;
	}
	//second pass
	data_cur = (int*)labImg.data;
	for( int i = 0; i < Rows; i++ )
	{
		for( int j = 0; j < bwImg.cols-1; j++, data_cur++)
			*data_cur = labelSet[ *data_cur ];
		data_cur++;
	}
}

//-------------------------------【老版兩步法】-------------------------------------------
void Two_PassOld(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
	//connected component analysis (4-component)
	//use two-pass algorithm
	//1. first pass: label each foreground pixel with a label
	//2. second pass: visit each labeled pixel and merge neighbor label
	//
	//foreground pixel: _binImg(x,y) = 1
	//background pixel: _binImg(x,y) = 0

	if(_binImg.empty() || _binImg.type() != CV_8UC1)
	{
		return;
	}

	// 1. first pass
	_lableImg.release();
	_binImg.convertTo(_lableImg, CV_32SC1 );

	int label = 1;  // start by 2
	std::vector<int> labelSet;
	labelSet.push_back(0);   //background: 0
	labelSet.push_back(1);   //foreground: 1

	int rows = _binImg.rows - 1;
	int cols = _binImg.cols - 1;
	for( int i = 1; i < rows; i++)
	{
		int* data_preRow = _lableImg.ptr<int>(i-1);
		int* data_curRow = _lableImg.ptr<int>(i);
		for(int j = 1; j < cols; j++)
		{
			if(data_curRow[j] == 1)
			{
				std::vector<int> neighborLabels;
				neighborLabels.reserve(2); //reserve(n)  預分配n個元素的儲存空間
				int leftPixel = data_curRow[j-1];
				int upPixel = data_preRow[j];
				if( leftPixel > 1)
				{
					neighborLabels.push_back(leftPixel);
				}
				if( upPixel > 1)
				{
					neighborLabels.push_back(upPixel);
				}
				if(neighborLabels.empty())
				{
					labelSet.push_back(++label);   //assign to a new label
					data_curRow[j] = label;
					labelSet[label] = label;
				}
				else
				{
					std::sort(neighborLabels.begin(),neighborLabels.end());
					int smallestLabel = neighborLabels[0];
					data_curRow[j] = smallestLabel;

					//save equivalence
					for(size_t k = 1; k < neighborLabels.size();k++)
					{
						int tempLabel = neighborLabels[k];
						int& oldSmallestLabel = labelSet[tempLabel];
						if(oldSmallestLabel > smallestLabel)
						{
							labelSet[oldSmallestLabel] = smallestLabel;
							oldSmallestLabel = smallestLabel;
						}
						else if(oldSmallestLabel < smallestLabel)
						{
							labelSet[smallestLabel] = oldSmallestLabel;
						}
					} 
				}

			}
		}
	}
	//update equivalent labels
	//assigned with the smallest label in each equivalent label set
	for(size_t i = 2; i < labelSet.size();i++)
	{
		int curLabel = labelSet[i];
		int prelabel = labelSet[curLabel];
		while (prelabel != curLabel )
		{
			curLabel = prelabel;
			prelabel = labelSet[prelabel];
		}
		labelSet[i] = curLabel;
	}

	//2. second pass
	for( int i = 0; i < rows; i++ )
	{
		int *data = _lableImg.ptr<int>(i);
		for(int j = 0; j < cols; j++ )
		{
			int& pixelLabel = data[j];
			pixelLabel = labelSet[pixelLabel];
		}
	}
}


//---------------------------------【種子填充法老版】-------------------------------
void SeedFillOld(const cv::Mat& binImg, cv::Mat& lableImg)   //種子填充法
{
	// 4鄰接方法


	if (binImg.empty() ||
		binImg.type() != CV_8UC1)
	{
		return;
	}

	lableImg.release();
	binImg.convertTo(lableImg, CV_32SC1);

	int label = 1;  

	int rows = binImg.rows - 1;  
	int cols = binImg.cols - 1;
	for (int i = 1; i < rows-1; i++)
	{
		int* data= lableImg.ptr<int>(i);
		for (int j = 1; j < cols-1; j++)
		{
			if (data[j] == 1)
			{
				std::stack<std::pair<int,int>> neighborPixels;   
				neighborPixels.push(std::pair<int,int>(i,j));     // 畫素位置: <i,j>
				++label;  // 沒有重複的團,開始新的標籤
				while (!neighborPixels.empty())
				{
					std::pair<int,int> curPixel = neighborPixels.top(); //如果與上一行中一個團有重合區域,則將上一行的那個團的標號賦給它
					int curX = curPixel.first;
					int curY = curPixel.second;
					lableImg.at<int>(curX, curY) = label;

					neighborPixels.pop();

					if (lableImg.at<int>(curX, curY-1) == 1)
					{//左邊
						neighborPixels.push(std::pair<int,int>(curX, curY-1));
					}
					if (lableImg.at<int>(curX, curY+1) == 1)
					{// 右邊
						neighborPixels.push(std::pair<int,int>(curX, curY+1));
					}
					if (lableImg.at<int>(curX-1, curY) == 1)
					{// 上邊
						neighborPixels.push(std::pair<int,int>(curX-1, curY));
					}
					if (lableImg.at<int>(curX+1, curY) == 1)
					{// 下邊
						neighborPixels.push(std::pair<int,int>(curX+1, curY));
					}
				}       
			}
		}
	}

}




//-------------------------------------------【種子填充法新版】---------------------------
void SeedFillNew(const cv::Mat& _binImg, cv::Mat& _lableImg )
{
	// connected component analysis(4-component)
	// use seed filling algorithm
	// 1. begin with a forgeground pixel and push its forground neighbors into a stack;
	// 2. pop the pop pixel on the stack and label it with the same label until the stack is empty
	// 
	//  forground pixel: _binImg(x,y)=1
	//  background pixel: _binImg(x,y) = 0


	if(_binImg.empty() ||
		_binImg.type()!=CV_8UC1)
	{
		return;
	} 

	_lableImg.release();
	_binImg.convertTo(_lableImg,CV_32SC1);

	int label = 0; //start by 1

	int rows = _binImg.rows;
	int cols = _binImg.cols;

	Mat mask(rows, cols, CV_8UC1);
	mask.setTo(0);
	int *lableptr;
	for(int i=0; i < rows; i++)
	{
		int* data = _lableImg.ptr<int>(i);
		uchar *masKptr = mask.ptr<uchar>(i);
		for(int j = 0; j < cols; j++)
		{
			if(data[j] == 255&&mask.at<uchar>(i,j)!=1)
			{
				mask.at<uchar>(i,j)=1;
				std::stack<std::pair<int,int>> neighborPixels;
				neighborPixels.push(std::pair<int,int>(i,j)); // pixel position: <i,j>
				++label; //begin with a new label
				while(!neighborPixels.empty())
				{
					//get the top pixel on the stack and label it with the same label
					std::pair<int,int> curPixel =neighborPixels.top();
					int curY = curPixel.first;
					int curX = curPixel.second;
					_lableImg.at<int>(curY, curX) = label;

					//pop the top pixel
					neighborPixels.pop();

					//push the 4-neighbors(foreground pixels)

					if(curX-1 >= 0)
					{
						if(_lableImg.at<int>(curY,curX-1) == 255&&mask.at<uchar>(curY,curX-1)!=1) //leftpixel
						{
							neighborPixels.push(std::pair<int,int>(curY,curX-1));
							mask.at<uchar>(curY,curX-1)=1;
						}
					}
					if(curX+1 <=cols-1)
					{
						if(_lableImg.at<int>(curY,curX+1) == 255&&mask.at<uchar>(curY,curX+1)!=1)
							// right pixel
						{
							neighborPixels.push(std::pair<int,int>(curY,curX+1));
							mask.at<uchar>(curY,curX+1)=1;
						}
					}
					if(curY-1 >= 0)
					{
						if(_lableImg.at<int>(curY-1,curX) == 255&&mask.at<uchar>(curY-1,curX)!=1)
							// up pixel
						{
							neighborPixels.push(std::pair<int,int>(curY-1, curX));
							mask.at<uchar>(curY-1,curX)=1;
						}  
					}
					if(curY+1 <= rows-1)
					{
						if(_lableImg.at<int>(curY+1,curX) == 255&&mask.at<uchar>(curY+1,curX)!=1)
							//down pixel
						{
							neighborPixels.push(std::pair<int,int>(curY+1,curX));
							mask.at<uchar>(curY+1,curX)=1;
						}
					}
				}
			}
		}
	}
}


//---------------------------------【顏色標記程式】-----------------------------------
//彩色顯示
cv::Scalar GetRandomColor()
{
	uchar r = 255 * (rand()/(1.0 + RAND_MAX));
	uchar g = 255 * (rand()/(1.0 + RAND_MAX));
	uchar b = 255 * (rand()/(1.0 + RAND_MAX));
	return cv::Scalar(b,g,r);
}


void LabelColor(const cv::Mat& labelImg, cv::Mat& colorLabelImg) 
{
	int num = 0;
	if (labelImg.empty() ||
		labelImg.type() != CV_32SC1)
	{
		return;
	}

	std::map<int, cv::Scalar> colors;

	int rows = labelImg.rows;
	int cols = labelImg.cols;

	colorLabelImg.release();
	colorLabelImg.create(rows, cols, CV_8UC3);
	colorLabelImg = cv::Scalar::all(0);

	for (int i = 0; i < rows; i++)
	{
		const int* data_src = (int*)labelImg.ptr<int>(i);
		uchar* data_dst = colorLabelImg.ptr<uchar>(i);
		for (int j = 0; j < cols; j++)
		{
			int pixelValue = data_src[j];
			if (pixelValue > 1)
			{
				if (colors.count(pixelValue) <= 0)
				{
					colors[pixelValue] = GetRandomColor();
					num++;
				}

				cv::Scalar color = colors[pixelValue];
				*data_dst++   = color[0];
				*data_dst++ = color[1];
				*data_dst++ = color[2];
			}
			else
			{
				data_dst++;
				data_dst++;
				data_dst++;
			}
		}
	}
	printf("color num : %d \n", num );
}

//------------------------------------------【測試主程式】-------------------------------------
int main()
{

	cv::Mat binImage = cv::imread("ltc2.jpg", 0);
	//cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY);
	cv::Mat labelImg;
	double time;
	time= getTickCount();
	//對應四種方法,需要哪一種,則呼叫哪一種
	//Two_PassOld(binImage, labelImg);
	//Two_PassNew(binImage, labelImg);
	//SeedFillOld(binImage, labelImg);
	//SeedFillNew(binImage, labelImg);
	time = 1000*((double)getTickCount() - time)/getTickFrequency();
    cout<<std::fixed<<time<<"ms"<<endl;
	//彩色顯示
	cv::Mat colorLabelImg;
	LabelColor(labelImg, colorLabelImg);
	cv::imshow("colorImg", colorLabelImg);
	//灰度顯示
	cv::Mat grayImg;
	labelImg *= 10;
	labelImg.convertTo(grayImg, CV_8UC1);
	cv::imshow("labelImg", grayImg);
	double minval, maxval;
	minMaxLoc(labelImg,&minval,&maxval);
	cout<<"minval"<<minval<<endl;
	cout<<"maxval"<<maxval<<endl;
	cv::waitKey(0);
	return 0;
}



上面是一個綜合程式碼示例,有四種方法,但是有些缺陷,表述如下:

老的兩步法:速度很慢

新的兩步法:速度很快 (Debug下。快了至少有10倍,與圖片的大小有關係)

種子填充法無論是新的還是老的都有缺陷

對於圖一:


可以看一下幾種方法的執行結果:

1.two_passOld



可以很明顯的看出新版方法快很多

其他方法得到的圖片類似,注意新版種子填充法並不能得到這樣的結果,有問題。

使用新版種子填充法則需要注意遮蔽閾值化那一句語句


對於這樣的圖片


對於這樣一張測試圖:

側檢視經過matlab 的bwlabel函式測是,在4連通情況下有3087個連通域,8連通下有2471個連通域。

對於新版與舊版的兩步法,沒啥區別,就是時間上有很大差別



使用老版的種子填充法會出現邊界問題,使用新版種子填充法則需要注意遮蔽閾值化那一句語句


 好的,這些方法到此就結束了,其中細節我也需要好好弄弄,但是時間有限,希望有大神朋友留言告知 接下來再給出幾個連通域相關的程式碼: 程式碼一:
#include <stdio.h>
#include <cv.h>
#include <highgui.h>
#include <time.h>
#include <stdlib.h>
#include <Windows.h>

int main( int argc, char** argv )  
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_INTENSITY | FOREGROUND_GREEN);
	IplImage* src = cvLoadImage("test.jpg", CV_LOAD_IMAGE_GRAYSCALE);
	IplImage* dst = cvCreateImage(cvGetSize(src), 8, 3);
	CvMemStorage* storage = cvCreateMemStorage(0);
	CvSeq* contour = 0;
	cvThreshold(src, src,120, 255, CV_THRESH_BINARY);	// 二值化
	cvNamedWindow("Source", 1);
	cvShowImage("Source", src);
	// 提取輪廓
	clock_t start, finish;
	start = clock();
	int contour_num = cvFindContours(src, storage, &contour, sizeof(CvContour), CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
	cvZero(dst);		// 清空陣列
	CvSeq *_contour = contour; 
	double maxarea = 0;
	double minarea = 100;
	int m = 0;
	for( ; contour != 0; contour = contour->h_next )  
	{  

		double tmparea = fabs(cvContourArea(contour));
		if(tmparea < minarea)   
		{  
			cvSeqRemove(contour, 0); // 刪除面積小於設定值的輪廓
			continue;
		}  
		CvRect aRect = cvBoundingRect( contour, 0 ); 
		if ((aRect.width/aRect.height)<1)  
		{  
			cvSeqRemove(contour, 0); //刪除寬高比例小於設定值的輪廓
			continue;
		}  
		if(tmparea > maxarea)  
		{  
			maxarea = tmparea;
		}  
		m++;
		// 建立一個色彩值
		CvScalar color = CV_RGB( 0, 255, 255 );

		//max_level 繪製輪廓的最大等級。如果等級為0,繪製單獨的輪廓。如果為1,繪製輪廓及在其後的相同的級別下輪廓
		//如果值為2,所有的輪廓。如果等級為2,繪製所有同級輪廓及所有低一級輪廓,諸此種種
		//如果值為負數,函式不繪製同級輪廓,但會升序繪製直到級別為abs(max_level)-1的子輪廓
		cvDrawContours(dst, contour, color, color, -1, 1, 8);	//繪製外部和內部的輪廓
	}  
	contour = _contour;
	int count = 0;
	for(; contour != 0; contour = contour->h_next)
	{  
		count++;
		double tmparea = fabs(cvContourArea(contour));
		if (tmparea == maxarea)  
		{  
			CvScalar color = CV_RGB( 255, 0, 0);
			cvDrawContours(dst, contour, color, color, -1, 1, 8);
		}  
	} 
	finish = clock();
	double duration;
	duration = (double)(finish - start) / CLOCKS_PER_SEC;
	printf("The total number of contours is:%d \n", count);
	printf("The total time is:%f seconds\n", duration);
	cvNamedWindow("Components", 1);
	cvShowImage("Components", dst);
	cvWaitKey(0);
	cvDestroyWindow("Source");
	cvReleaseImage(&src);
	cvDestroyWindow("Components");
	cvReleaseImage(&dst);

	return 0;
}  
第一張比較簡單的測試圖,時間如下:

第二張比較複雜的側檢視八連通,時間如下:

速度之快,令人恐怖 程式碼二:
#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
using namespace cv;

typedef struct _Feather
{
    int label;              // 連通域的label值
    int area;               // 連通域的面積
    Rect boundingbox;       // 連通域的外接矩形框
} Feather;

/* 
Input: 
    src: 待檢測連通域的二值化影象
Output:
    dst: 標記後的影象
    featherList: 連通域特徵的清單
return: 
    連通域數量。
*/
int bwLabel(Mat & src, Mat & dst, vector<Feather> & featherList)
{
    int rows = src.rows;
    int cols = src.cols;

    int labelValue = 0;
    Point seed, neighbor;
    stack<Point> pointStack;    // 堆疊

    int area = 0;               // 用於計算連通域的面積
    int leftBoundary = 0;       // 連通域的左邊界,即外接最小矩形的左邊框,橫座標值,依此類推
    int rightBoundary = 0;
    int topBoundary = 0;
    int bottomBoundary = 0;
    Rect box;                   // 外接矩形框
    Feather feather;

    featherList.clear();    // 清除陣列

    dst.release();
    dst = src.clone();
    for( int i = 0; i < rows; i++)
    {
        uchar *pRow = dst.ptr<uchar>(i);
        for( int j = 0; j < cols; j++)
        {
            if(pRow[j] == 255)
            {
                area = 0;
                labelValue++;           // labelValue最大為254,最小為1.
                seed = Point(j, i);     // Point(橫座標,縱座標)
                dst.at<uchar>(seed) = labelValue;
                pointStack.push(seed);

                area++;
                leftBoundary = seed.x;
                rightBoundary = seed.x;
                topBoundary = seed.y;
                bottomBoundary = seed.y;

                while(!pointStack.empty())
                {
                    neighbor = Point(seed.x+1, seed.y);
                    if((seed.x != (cols-1)) && (dst.at<uchar>(neighbor) == 255))
                    {
                        dst.at<uchar>(neighbor) = labelValue;
                        pointStack.push(neighbor);

                        area++;
                        if(rightBoundary < neighbor.x)
                            rightBoundary = neighbor.x;
                    }

                    neighbor = Point(seed.x, seed.y+1);
                    if((seed.y != (rows-1)) && (dst.at<uchar>(neighbor) == 255))
                    {
                        dst.at<uchar>(neighbor) = labelValue;
                        pointStack.push(neighbor);

                        area++;
                        if(bottomBoundary < neighbor.y)
                            bottomBoundary = neighbor.y;

                    }

                    neighbor = Point(seed.x-1, seed.y);
                    if((seed.x != 0) && (dst.at<uchar>(neighbor) == 255))
                    {
                        dst.at<uchar>(neighbor) = labelValue;
                        pointStack.push(neighbor);

                        area++;
                        if(leftBoundary > neighbor.x)
                            leftBoundary = neighbor.x;
                    }

                    neighbor = Point(seed.x, seed.y-1);
                    if((seed.y != 0) && (dst.at<uchar>(neighbor) == 255))
                    {
                        dst.at<uchar>(neighbor) = labelValue;
                        pointStack.push(neighbor);

                        area++;
                        if(topBoundary > neighbor.y)
                            topBoundary = neighbor.y;
                    }

                    seed = pointStack.top();
                    pointStack.pop();
                }
                box = Rect(leftBoundary, topBoundary, rightBoundary-leftBoundary, bottomBoundary-topBoundary);
                rectangle(src, box, 255);
                feather.area = area;
                feather.boundingbox = box;
                feather.label = labelValue;
                featherList.push_back(feather);
            }
        }
    }
    return labelValue;
}

int main(int argc, char *argv[])
{
    Mat src(imread("ltc2.jpg", 0));
    if(src.empty())
        exit(-1);
    threshold(src, src, 127, 255, THRESH_BINARY);   // 二值化影象
    vector<Feather> featherList;                    // 存放連通域特徵
    Mat dst;
    cout << "連通域數量: " << bwLabel(src, dst, featherList) << endl;

    // 為了方便觀察,可以將label“放大”
    for( int i = 0; i < dst.rows; i++)
    {
        uchar *p = dst.ptr<uchar>(i);
        for( int j = 0; j < dst.cols; j++)
        {
            p[j] = 30*p[j];
        }
    }

    cout << "標號" << "\t" << "面積" << endl;
    for(vector<Feather>::iterator it = featherList.begin(); it < featherList.end(); it++)
    {
        cout << it->label << "\t" << it->area << endl;
        rectangle(dst, it->boundingbox, 255);
    }

    imshow("src", src);
    imshow("dst", dst);

    waitKey();
    destroyAllWindows();

    system("pause");
    return 0;
}



對於第一張測試圖,結果如下
第二張,就跑不出來了,畢竟畫外包絡太複雜了,哈哈! 由於生病幾天了,躺在醫院一個星期,不得不感慨,程式設計師必須好好的照顧自己的身體。