1. 程式人生 > >Opencv分水嶺演算法——watershed自動影象分割用法

Opencv分水嶺演算法——watershed自動影象分割用法

分水嶺演算法是一種影象區域分割法,在分割的過程中,它會把跟臨近畫素間的相似性作為重要的參考依據,從而將在空間位置上相近並且灰度值相近的畫素點互相連線起來構成一個封閉的輪廓,封閉性是分水嶺演算法的一個重要特徵

其他影象分割方法,如閾值,邊緣檢測等都不會考慮畫素在空間關係上的相似性和封閉性這一概念,彼此畫素間互相獨立,沒有統一性。分水嶺演算法較其他分割方法更具有思想性,更符合人眼對影象的印象。

其他關於分水嶺“聚水盆地”、“水壩”、“分水線”等概念不準備贅述,只探討一下Opencv中分水嶺演算法的實現方法watershed——這個“簡單”到只有兩個引數的函式是如何工作的。

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. }  

第一幅影象分割效果:


按比例跟原始影象融合:


第二幅影象原始圖:


分割效果:


按比例跟原始影象融合: