1. 程式人生 > >影象處理——分水嶺演算法

影象處理——分水嶺演算法

首先感謝以下兩位的博文幫助我的理解:

(1)邁克老狼2012   https://www.cnblogs.com/mikewolf2002/p/3304118.html

(2)-牧野-              http://blog.csdn.net/dcrmg/article/details/52498440

分水嶺演算法是一種影象區域分割法,在分割的過程中,它會把跟臨近畫素間的相似性作為重要的參考依據,從而將在空間位置上相近並且灰度值相近(求梯度)的畫素點互相連線起來構成一個封閉的輪廓。分水嶺演算法常用的操作步驟:彩色影象灰度化,然後再求梯度圖,最後在梯度圖的基礎上進行分水嶺演算法,求得分段影象的邊緣線。

        下面左邊的灰度圖,可以描述為右邊的地形圖,地形的高度是由灰度圖的灰度值決定,灰度為0對應地形圖的地面,灰度值最大的畫素對應地形圖的最高點。

ima1 (2)ima2

我們可以自己程式設計實現灰度圖的地形圖顯示,工程FirstOpenCV6就實現了簡單的這個功能,比如上邊的灰度圖,顯示為:

image

對灰度圖的地形學解釋,我們我們考慮三類點

1. 區域性最小值點,該點對應一個盆地的最低點,當我們在盆地裡滴一滴水的時候,由於重力作用,水最終會匯聚到該點。注意:可能存在一個最小值面,該平面內的都是最小值點。

2. 盆地的其它位置點,該位置滴的水滴會匯聚到區域性最小點。

3. 盆地的邊緣點,是該盆地和其它盆地交接點,在該點滴一滴水,會等概率的流向任何一個盆地。

image

       假設我們在盆地的最小值點,打一個洞,然後往盆地裡面注水,並阻止兩個盆地的水彙集,我們會在兩個盆地的水彙集的時刻,在交接的邊緣線上(也即分水嶺線),建一個壩,來阻止兩個盆地的水彙集成一片水域。這樣影象就被分成2個畫素集,一個是注水盆地畫素集,一個是分水嶺線畫素集。

      下面的gif圖很好的演示了分水嶺演算法的效果:

lpe1 (1)ima3 (1)

     在真實影象中,由於噪聲點或者其它干擾因素的存在,使用分水嶺演算法常常存在過度分割的現象,這是因為很多很小的區域性極值點的存在,比如下面的影象,這樣的分割效果是毫無用處的。

ima7ima7b

      為了解決過度分割的問題,可以使用基於標記(mark)影象的分水嶺演算法,就是通過先驗知識,來指導分水嶺演算法,以便獲得更好的影象分段效果。通常的mark影象,都是在某個區域定義了一些灰度層級,在這個區域的洪水淹沒過程中,水平面都是從定義的高度開始的,這樣可以避免一些很小的噪聲極值區域的分割。

      下面的gif圖很好的演示了基於mark的分水嶺演算法過程:

ima4lpe2ima5

      上面的過度分段影象,我們通過指定mark區域,可以得到很好的分段效果:

ima8ima9

Opencv 中 watershed函式原型:

[cpp] view plain copy  print?
  1. void watershed( InputArray image, InputOutputArray markers );  

第一個引數 image,必須是一個8bit 3通道彩色影象矩陣序列,第一個引數沒什麼要說的。關鍵是第二個引數 markers,Opencv官方文件的說明如下:

Before passing the image to the function, you have to roughly outline the desired regions in the image markers with positive (>0) indices. So, every region is represented as one or more connected components with the pixel values 1, 2, 3, and so on. Such markers can be retrieved from a binary mask using findContours() and drawContours(). The markers are “seeds” of the future image regions. All the other pixels in markers , whose relation to the outlined regions is not known and should be defined by the algorithm, should be set to 0’s. In the function output, each pixel in markers is set to a value of the “seed” components or to -1 at boundaries between the regions.

就不一句一句翻譯了,大意說的是在執行分水嶺函式watershed之前,必須對第二個引數markers進行處理,它應該包含不同區域的輪廓,每個輪廓有一個自己唯一的編號,輪廓的定位可以通過Opencv中findContours方法實現,這個是執行分水嶺之前的要求。

接下來執行分水嶺會發生什麼呢?演算法會根據markers傳入的輪廓作為種子(也就是所謂的注水點),對影象上其他的畫素點根據分水嶺演算法規則進行判斷,並對每個畫素點的區域歸屬進行劃定,直到處理完影象上所有畫素點。而區域與區域之間的分界處的值被置為“-1”,以做區分。

簡單概括一下就是說第二個入參markers必須包含了種子點資訊。Opencv官方例程中使用滑鼠劃線標記,其實就是在定義種子,只不過需要手動操作,而使用findContours可以自動標記種子點。而分水嶺方法完成之後並不會直接生成分割後的影象,還需要進一步的顯示處理,如此看來,只有兩個引數的watershed其實並不簡單。

下邊通過圖示來看一下watershed函式的第二個引數markers在演算法執行前後發生了什麼變化。對於一個原圖:


經過灰度化、濾波、Canny邊緣檢測、findContours輪廓查詢、輪廓繪製等步驟後終於得到了符合Opencv要求的merkers,我們把merkers轉換成8bit單通道灰度圖看看它裡邊到底是什麼內容:

這個是分水嶺運算前的merkers:

這個是findContours檢測到的輪廓:


看效果,基本上跟影象的輪廓是一樣的,也是簡單的勾勒出了物體的外形。但如果仔細觀察就能發現,影象上不同線條的灰度值是不同的,底部略暗,越往上灰度越高。由於這幅影象邊緣比較少,對比不是很明顯,再來看一幅輪廓數量較多的圖效果:

這個是分水嶺運算前的merkers:

這個是findContours檢測到的輪廓:


從這兩幅圖對比可以很明顯看到,從影象底部往上,線條的灰度值是越來越高的,並且merkers影象底部部分線條的灰度值由於太低,已經觀察不到了相互連線在一起的線條灰度值是一樣的,這些線條和不同的灰度值又能說明什麼呢?

答案是:每一個線條代表了一個種子,線條的不同灰度值其實代表了對不同注水種子的編號,有多少不同灰度值的線條,就有多少個種子,影象最後分割後就有多少個區域。


再來看一下執行完分水嶺方法之後merkers裡邊的內容發生了什麼變化:


可以看到,執行完watershed之後,merkers裡邊被分割出來的區域已經非常明顯了,空間上臨近並且灰度值上相近的區域被劃分為一個區域,灰度值是一樣,不同區域間被劃分開,這其實就是分水嶺對影象的分割效果了。

總的概括一下watershed影象自動分割的實現步驟:

1. 影象灰度化、濾波、Canny邊緣檢測

2. 查詢輪廓,並且把輪廓資訊按照不同的編號繪製到watershed的第二個入參merkers上,相當於標記注水點。

3. watershed分水嶺運算

4. 繪製分割出來的區域,視覺控還可以使用隨機顏色填充,或者跟原始影象融合以下,以得到更好的顯示效果。

以下是Opencv分水嶺演算法watershed實現的完整過程:

[cpp] view plain copy  print?
  1. #include "opencv2/imgproc/imgproc.hpp"
  2. #include "opencv2/highgui/highgui.hpp"
  3. #include <iostream>
  4. usingnamespace cv;  
  5. usingnamespace std;  
  6. Vec3b RandomColor(int value);  //生成隨機顏色函式
  7. int main( int argc, char* argv[] )  
  8. {  
  9.     Mat image=imread(argv[1]);    //載入RGB彩色影象
  10.     imshow("Source Image",image);  
  11.     //灰度化,濾波,Canny邊緣檢測
  12.     Mat imageGray;  
  13.     cvtColor(image,imageGray,CV_RGB2GRAY);//灰度轉換
  14.     GaussianBlur(imageGray,imageGray,Size(5,5),2);   //高斯濾波
  15.     imshow("Gray Image",imageGray);   
  16.     Canny(imageGray,imageGray,80,150);    
  17.     imshow("Canny Image",imageGray);  
  18.     //查詢輪廓
  19.     vector<vector<Point>> contours;    
  20.     vector<Vec4i> hierarchy;    
  21.     findContours(imageGray,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point());    
  22.     Mat imageContours=Mat::zeros(image.size(),CV_8UC1);  //輪廓   
  23.     Mat marks(image.size(),CV_32S);   //Opencv分水嶺第二個矩陣引數
  24.     marks=Scalar::all(0);  
  25.     int index = 0;  
  26.     int compCount = 0;  
  27.     for( ; index >= 0; index = hierarchy[index][0], compCount++ )   
  28.     {  
  29.         //對marks進行標記,對不同區域的輪廓進行編號,相當於設定注水點,有多少輪廓,就有多少注水點
  30.         drawContours(marks, contours, index, Scalar::all(compCount+1), 1, 8, hierarchy);  
  31.         drawContours(imageContours,contours,index,Scalar(255),1,8,hierarchy);    
  32.     }  
  33.     //我們來看一下傳入的矩陣marks裡是什麼東西
  34.     Mat marksShows;  
  35.     convertScaleAbs(marks,marksShows);  
  36.     imshow("marksShow",marksShows);  
  37.     imshow("輪廓",imageContours);  
  38.     watershed(image,marks);  
  39.     //我們再來看一下分水嶺演算法之後的矩陣marks裡是什麼東西
  40.     Mat afterWatershed;  
  41.     convertScaleAbs(marks,afterWatershed);  
  42.     imshow("After Watershed",afterWatershed);  
  43.     //對每一個區域進行顏色填充
  44.     Mat PerspectiveImage=Mat::zeros(image.size(),CV_8UC3);  
  45.     for(int i=0;i<marks.rows;i++)  
  46.     {  
  47.         for(int j=0;j<marks.cols;j++)  
  48.         {  
  49.             int index=marks.at<int>(i,j);  
  50.             if(marks.at<int>(i,j)==-1)  
  51.             {  
  52.                 PerspectiveImage.at<Vec3b>(i,j)=Vec3b(255,255,255);  
  53.             }              
  54.             else
  55.             {  
  56.                 PerspectiveImage.at<Vec3b>(i,j) =RandomColor(index);  
  57.             }  
  58.         }  
  59.     }  
  60.     imshow("After ColorFill",PerspectiveImage);  
  61.     //分割並填充顏色的結果跟原始影象融合
  62.     Mat wshed;  
  63.     addWeighted(image,0.4,PerspectiveImage,0.6,0,wshed);  
  64.     imshow("AddWeighted Image",wshed);  
  65.     waitKey();  
  66. }  
  67. Vec3b RandomColor(int value)    <span style="line-height: 20.8px; font-family: sans-serif;">//生成隨機顏色函式</span>
  68. {  
  69.     value=value%255;  //生成0~255的隨機數
  70.     RNG rng;  
  71.     int aa=rng.uniform(0,value);  
  72.     int bb=rng.uniform(0,value);  
  73.     int cc=rng.uniform(0,value);  
  74.     return Vec3b(aa,bb,cc);  
  75. }  

第一幅影象分割效果:


按比例跟原始影象融合:


第二幅影象原始圖:


分割效果:


按比例跟原始影象融合: