圖像分割作為圖像識別的基礎,在圖像處理中占有重要地位,通常需要在進行圖像分割算法前找到輪廓或分割線,因此傳統的分割算法主要集中在邊緣檢測、閾值處理等。
- 分水嶺算法
分水嶺算法是一種圖像區域分割法,在分割的過程中,它會把跟臨近像素間的相似性作為重要的參考依據,從而將在空間位置上相近並且灰度值相近的像素點互相連接起來構成一個封閉的輪廓,封閉性是分水嶺算法的一個重要特征。其他圖像分割方法,如閾值,邊緣檢測等都不會考慮像素在空間關系上的相似性和封閉性這一概念,彼此像素間互相獨立,沒有統一性。分水嶺算法較其他分割方法更具有思想性,更符合人眼對圖像的印象。
在上面的水嶺算法示意圖中局部極小值、積水盆地,分水嶺線以及水壩的概念可以描述為:
1.區域極小值:導數為0的點,局部范圍內的最小值點;
2.集水盆(匯水盆地):當“水”落到匯水盆地時,“水”會自然而然地流到匯水盆地中的區域極小值點處。每一個匯水盆地中有且僅有一個區域極小值點;
3.分水嶺:當“水”處於分水嶺的位置時,會等概率地流向多個與它相鄰的匯水盆地中;
4.水壩:人為修建的分水嶺,防止相鄰匯水盆地之間的“水”互相交匯影響。
OpenCV中的watershed可以實現分水嶺的功能,函數原型如下:
1 void watershed( InputArray image, InputOutputArray markers );
注意:第一個參數 image,必須是一個8bit 3通道彩色圖像矩陣序列,可以將單通道圖像進行擴充,比如OpenCV函數內置CV_GRAY2BGR函數可以將單通道圖像擴展成三通道圖像。
第二個參數,在執行分水嶺函數watershed之前,必須對第二個參數markers進行處理,它應該包含不同區域的輪廓,每個輪廓有一個自己唯一的編號,輪廓的定位可以通過Opencv中findContours方法實現,這個是執行分水嶺之前的要求。算法會根據markers傳入的輪廓作為種子(也就是所謂的注水點),對圖像上其他的像素點根據分水嶺算法規則進行判斷,並對每個像素點的區域歸屬進行划定,直到處理完圖像上所有像素點。而區域與區域之間的分界處的值被置為“-1”,以做區分。
每一個線條代表了一個種子,線條的不同灰度值其實代表了對不同注水種子的編號,有多少不同灰度值的線條,就有多少個種子,圖像最后分割后就有多少個區域。
傳入watershed中的makers圖像如下圖所示:
findcountours后尋找的輪廓如下圖所示:
從上面兩幅圖中我們從圖像底部往上,線條的灰度值是越來越高的,並且merkers圖像底部部分線條的灰度值由於太低,已經觀察不到了,但相互連接在一起的線條灰度值是一樣的。
1 #include "opencv2/imgproc/imgproc.hpp" 2 #include "opencv2/highgui/highgui.hpp" 3 4 #include <iostream> 5 6 using namespace cv; 7 using namespace std; 8 9 Vec3b RandomColor(int value); //生成隨機顏色函數 10 11 int main( int argc, char* argv[] ) 12 { 13 Mat image=imread(argv[1]); //載入RGB彩色圖像 14 imshow("Source Image",image); 15 16 //灰度化,濾波,Canny邊緣檢測 17 Mat imageGray; 18 cvtColor(image,imageGray,CV_RGB2GRAY);//灰度轉換 19 GaussianBlur(imageGray,imageGray,Size(5,5),2); //高斯濾波 20 imshow("Gray Image",imageGray); 21 Canny(imageGray,imageGray,80,150); 22 imshow("Canny Image",imageGray); 23 24 //查找輪廓 25 vector<vector<Point>> contours; 26 vector<Vec4i> hierarchy; 27 findContours(imageGray,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point()); 28 Mat imageContours=Mat::zeros(image.size(),CV_8UC1); //輪廓 29 Mat marks(image.size(),CV_32S); //Opencv分水嶺第二個矩陣參數 30 marks=Scalar::all(0); 31 int index = 0; 32 int compCount = 0; 33 for( ; index >= 0; index = hierarchy[index][0], compCount++ ) 34 { 35 //對marks進行標記,對不同區域的輪廓進行編號,相當於設置注水點,有多少輪廓,就有多少注水點 36 drawContours(marks, contours, index, Scalar::all(compCount+1), 1, 8, hierarchy); 37 drawContours(imageContours,contours,index,Scalar(255),1,8,hierarchy); 38 } 39 40 //我們來看一下傳入的矩陣marks里是什么東西 41 Mat marksShows; 42 convertScaleAbs(marks,marksShows); 43 imshow("marksShow",marksShows); 44 imshow("輪廓",imageContours); 45 watershed(image,marks); 46 47 //我們再來看一下分水嶺算法之后的矩陣marks里是什么東西 48 Mat afterWatershed; 49 convertScaleAbs(marks,afterWatershed); 50 imshow("After Watershed",afterWatershed); 51 52 //對每一個區域進行顏色填充 53 Mat PerspectiveImage=Mat::zeros(image.size(),CV_8UC3); 54 for(int i=0;i<marks.rows;i++) 55 { 56 for(int j=0;j<marks.cols;j++) 57 { 58 int index=marks.at<int>(i,j); 59 if(marks.at<int>(i,j)==-1) 60 { 61 PerspectiveImage.at<Vec3b>(i,j)=Vec3b(255,255,255); 62 } 63 else 64 { 65 PerspectiveImage.at<Vec3b>(i,j) =RandomColor(index); 66 } 67 } 68 } 69 imshow("After ColorFill",PerspectiveImage); 70 71 //分割並填充顏色的結果跟原始圖像融合 72 Mat wshed; 73 addWeighted(image,0.4,PerspectiveImage,0.6,0,wshed); 74 imshow("AddWeighted Image",wshed); 75 76 waitKey(); 77 } 78 79 Vec3b RandomColor(int value) <span style="line-height: 20.8px; font-family: sans-serif;">//生成隨機顏色函數</span> 80 { 81 value=value%255; //生成0~255的隨機數 82 RNG rng; 83 int aa=rng.uniform(0,value); 84 int bb=rng.uniform(0,value); 85 int cc=rng.uniform(0,value); 86 return Vec3b(aa,bb,cc); 87 }
按照分水嶺算法對下圖進行分割:
對上圖進行分水嶺分割后發現結果並不好:
原因是很多目標物體距離太近,findcountours后導致無法分離,分水嶺的注水點太少,因此應該先將目標物體分離,可以考慮腐蝕等形態學操作,這里采用距離變換來細化目標物體的大小,從而分離目標,增加分水嶺注水點。
- 距離變換/distanceTransform函數
distanceTransform方法用於計算圖像中每一個非零點距離離自己最近的零點的距離,distanceTransform的第二個Mat矩陣參數dst保存了每一個點與最近的零點的距離信息,圖像上越亮的點,代表了離零點的距離越遠。可以根據距離變換的這個性質,經過簡單的運算,用於細化字符的輪廓。
1 Mat imageThin(imageGray.size(),CV_32FC1); //定義保存距離變換結果的Mat矩陣 2 distanceTransform(imageGray,imageThin,CV_DIST_L2,3); //距離變換 3 Mat distShow; 4 distShow=Mat::zeros(imageGray.size(),CV_8UC1); //定義細化后的字符輪廓 5 for(int i=0;i<imageThin.rows;i++) 6 { 7 for(int j=0;j<imageThin.cols;j++) 8 { 9 if(imageThin.at<float>(i,j)>maxValue) 10 { 11 maxValue=imageThin.at<float>(i,j); //獲取距離變換的極大值 12 } 13 } 14 } 15 for(int i=0;i<imageThin.rows;i++) 16 { 17 for(int j=0;j<imageThin.cols;j++) 18 { 19 if(imageThin.at<float>(i,j)>maxValue/1.9) 20 { 21 distShow.at<uchar>(i,j)=255; //符合距離大於最大值一定比例條件的點設為255 22 } 23 } 24 }
對毛氈柱的灰度圖進行距離變換,結果如下圖:
對距離變換之后的圖像進行findcontours,距離變換,結果如下圖:
結果顯示,基於標記的分水嶺比傳統分水嶺的分割效果要好很多。