1. 程式人生 > >分水嶺演算法(Watershed algorithm)與OpenCV實現

分水嶺演算法(Watershed algorithm)與OpenCV實現

前言

分水嶺演算法主要用於影象的分割!

         這個演算法需要輸入一個灰度圖,在接下來的洪水漫堤過程中,相鄰的積水盆地之間的分水嶺便慢慢構建起來。一般情況下,這會引起過分割,尤其是具有噪聲的影象。

         影象必須要預處理,以消除噪聲;分割結果必須要基於一些根據進行區域合併,以減小過分割造成的影響!

技術
         假設我們要將下面這幅影象進行分割,需要幾步?分三步,第一步,冰箱門開啟,第二步。。。打住,- -| 進入正題,我們要用OpenCV來實現,不就一步麼?呼叫那個watershed函式不就行了?實際上,可沒那麼簡單!且聽我細細道來 ~-~!



OpenCV實現了一個基於標記圖層的分水嶺演算法,所謂基於標記圖層,就是不用手動選擇種子點了,直接輸入一個包含標記點(種子點)的影象即可。函式原型如下:

C++: void watershed(InputArray image, InputOutputArray markers)

其中,image代表輸入影象(必須是8-bit3通道影象,也就是說是灰度影象),這也就是說,要對一個彩色影象進行分割,就要先將image轉換為灰度圖,幸運的是這可以通過cvtColor()(原型為C++: void cvtColor( InputArray src,OutputArray dst, int code, int dstCn=0 ),code代表轉換型別,具體的所有程式碼在這裡可以找到)做到;markers是輸入的標記圖層(包含標記點的圖層,這些標記點就是進行洪水的入口,一般是區域極小值點)。

利用cvtColor將原圖轉換為灰度圖如下:

這就是說要利用分水嶺演算法對一幅影象進行分割,必須提供其對應的標記圖層。那麼問題來了?挖掘機技術哪家強?。。。額,串詞了 - -|  那麼markers圖層從哪裡來呢?不用怕,需要的markers圖層可以通過findContours()函式勾勒出來,findContours()函式的原型如下:

C++: void findContours( InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierarchy, int mode, int method, Point offset=Point())
C++: void findContours( InputOutputArray image, OutputArrayOfArrays contours, int mode, int method, Point offset=Point())

其中,image引數輸入的影象必須是8-bit單通道的影象。可以使用compare() , inRange(), threshold() , adaptiveThreshold() , Canny() 從彩色或者灰度圖轉換成二值圖。
這裡我們使用adaptiveThreshold()方法轉化一個灰度圖到二值圖,以使用findContours()方法。adaptiveThreshold()的原型是:
C++: void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)

src是8-bit 單通道的影象;dst和src是同一型別的影象;maxValue是條件滿足時的賦值;adaptiveMethod是確定合適的閾值的演算法,有兩個選項:ADAPTIVE_THRESH_MEAN_C 或者ADAPTIVE_THRESH_GAUSSIAN_C,分別對應於均值和高斯演算法;thresholdType是指閾值分割型別,必須是THRESH_BINARY或者THRESH_BINARY_INV,如果是THRESH_BINARY,那麼大於閾值的將會賦值為maxValue(一般為255),否則就賦值為0,THRESH_BINARY_INV是相反的規則!

利用以上思路轉換為二值圖後的結果如下:

findContours結果如下:

OK,將markers檔案傳入watershed函式的引數,最終結果如下:

程式碼我給附上:

/**
* 定義一個使用分水嶺演算法的輔助類
*/
class WatershedSegmenter{
private:
    cv::Mat markers;
public:
    void setMarkers(cv::Mat& markerImage)
    {
        markerImage.convertTo(markers, CV_32S);
    }

    cv::Mat process(cv::Mat &image)
    {
        cv::watershed(image, markers);
        markers.convertTo(markers,CV_8U);
        return markers;
    }
};

/**
* 接受一個影象引數,顯示出分水嶺演算法分割的結果
*/
void watershedSegment (Mat img){
	Mat gray(img.rows, img.cols,CV_8UC1);
	cvtColor(img, gray, CV_BGR2GRAY);	//轉換為8-bit,3通道的灰度圖
	Mat binary = Mat::zeros(gray.rows, gray.cols, CV_8UC1);
	adaptiveThreshold(gray, binary, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV, 5, 10);		//將灰度圖轉換為二值圖
	Mat markers = Mat::zeros(gray.rows, gray.cols, CV_8UC1);

	//使用findContour()函式找出影象的輪廓
	vector<vector<Point> > contours;
	vector<Vec4i> hierarchy;
	findContours(binary, contours, hierarchy, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

	//將contours結果放入到markers中,便於訪問
	int idx = 0;
	for( ; idx >= 0; idx = hierarchy[idx][0]){
		Scalar color(rand()&255, rand()&255, rand()&255);
		drawContours(markers, contours, idx, color, CV_FILLED, 8, hierarchy);
	}

	//呼叫分水嶺演算法分割影象
	WatershedSegmenter segmenter;
        segmenter.setMarkers(markers);
        cv::Mat result = segmenter.process(img);

	//顯示分割結果
	namedWindow("segmentation_result", 0);
	imshow("segmentation_result", result);
}

總結

Watershed是一個很好的演算法,效率也很高。在影象分割中,應用的比較廣泛,據說很多商業軟體都有實現!

但是其缺點也很明顯——過分割現象。怎麼辦呢?可以有兩種思路,從源頭或者過後收拾殘局:從源頭?就是在分割過程中控制分割行為,這個好複雜的說!要修改演算法,以兄弟我現在的水平,可是力不從心,哈哈;那麼我們可以採取第二種思路,將過分割的地物物件進行合併分類,聽起來好像很有道理的樣子哦,我們下次討論一下哈。