分水嶺算法是一種圖像區域分割法,在分割的過程中,它會把跟臨近像素間的相似性作為重要的參考依據,從而將在空間位置上相近并且灰度值相近的像素點互相連接起來構成一個封閉的輪廓,封閉性是分水嶺算法的一個重要特征。
其他圖像分割方法,如閾值,邊緣檢測等都不會考慮像素在空間關系上的相似性和封閉性這一概念,彼此像素間互相獨立,沒有統一性。分水嶺算法較其他分割方法更具有思想性,更符合人眼對圖像的印象。
其他關于分水嶺“聚水盆地”、“水壩”、“分水線”等概念不準備贅述,只探討一下Opencv中分水嶺算法的實現方法watershed——這個“簡單”到只有兩個參數的函數是如何工作的。
Opencv 中 watershed函數原型:
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實現的完整過程:
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"#include <iostream>using namespace cv;
using namespace std;Vec3b RandomColor(int value); //生成隨機顏色函數int main( int argc, char* argv[] )
{Mat image=imread(argv[1]); //載入RGB彩色圖像imshow("Source Image",image);//灰度化,濾波,Canny邊緣檢測Mat imageGray;cvtColor(image,imageGray,CV_RGB2GRAY);//灰度轉換GaussianBlur(imageGray,imageGray,Size(5,5),2); //高斯濾波imshow("Gray Image",imageGray); Canny(imageGray,imageGray,80,150); imshow("Canny Image",imageGray);//查找輪廓vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(imageGray,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point()); Mat imageContours=Mat::zeros(image.size(),CV_8UC1); //輪廓 Mat marks(image.size(),CV_32S); //Opencv分水嶺第二個矩陣參數marks=Scalar::all(0);int index = 0;int compCount = 0;for( ; index >= 0; index = hierarchy[index][0], compCount++ ) {//對marks進行標記,對不同區域的輪廓進行編號,相當于設置注水點,有多少輪廓,就有多少注水點drawContours(marks, contours, index, Scalar::all(compCount+1), 1, 8, hierarchy);drawContours(imageContours,contours,index,Scalar(255),1,8,hierarchy); }//我們來看一下傳入的矩陣marks里是什么東西Mat marksShows;convertScaleAbs(marks,marksShows);imshow("marksShow",marksShows);imshow("輪廓",imageContours);watershed(image,marks);//我們再來看一下分水嶺算法之后的矩陣marks里是什么東西Mat afterWatershed;convertScaleAbs(marks,afterWatershed);imshow("After Watershed",afterWatershed);//對每一個區域進行顏色填充Mat PerspectiveImage=Mat::zeros(image.size(),CV_8UC3);for(int i=0;i<marks.rows;i++){for(int j=0;j<marks.cols;j++){int index=marks.at<int>(i,j);if(marks.at<int>(i,j)==-1){PerspectiveImage.at<Vec3b>(i,j)=Vec3b(255,255,255);} else{PerspectiveImage.at<Vec3b>(i,j) =RandomColor(index);}}}imshow("After ColorFill",PerspectiveImage);//分割并填充顏色的結果跟原始圖像融合Mat wshed;addWeighted(image,0.4,PerspectiveImage,0.6,0,wshed);imshow("AddWeighted Image",wshed);waitKey();
}Vec3b RandomColor(int value) <span style="line-height: 20.8px; font-family: sans-serif;">//生成隨機顏色函數</span>
{value=value%255; //生成0~255的隨機數RNG rng;int aa=rng.uniform(0,value);int bb=rng.uniform(0,value);int cc=rng.uniform(0,value);return Vec3b(aa,bb,cc);
}
第一幅圖像分割效果:
按比例跟原始圖像融合:
第二幅圖像原始圖:
分割效果:
按比例跟原始圖像融合: