圖像的分水嶺變換是一種流行的圖像處理算法,用於快速將圖像分割成多個同質區域。
它基於這樣的思想:如果把圖像看作一個拓撲地貌,那麼同類區域就相當於陡峭的邊緣內相對平坦的盆地。
使用圖像的分水嶺分割算法,函數爲:
CV_EXPORTS_W void watershed( InputArray image, InputOutputArray markers );
注意:
image: 輸入圖像,需爲8位的三通道彩色圖像;
markers: 參數調用後的結果,輸入/輸出32位單通道圖像標記結果,需和原圖一樣的大小。
過程如下:
以下處理中,使用的原圖像爲:
首先讀取灰度圖像,並設置閾值,將其轉化爲二值圖像,如下圖所示:
在形態學中,習慣用高像素值(白色)表示前景物體,用低像素值(黑色)表示背景物體,故對圖像做反向處理。
我們需要從二值圖像中識別出屬於前景(樓房、道路)以及屬於背景(天空)的像素。這裏我們把前景像素標記爲255,背景像素標記爲128(該數字是隨意選擇的,任何不等於255的數字都可以使用)。其他標籤是未知的,標記爲0.
現在對圖像做深度腐蝕運算,只保留屬於重點物體的像素:
得到的圖像如下所示:
然後,我們通過對原二值圖像做一次大幅度的膨脹運算來選中一些背景像素:
//標示不含物體的圖像像素
cv::Mat bg;
cv::dilate(binary,bg, cv::Mat(),cv::Point(-1,-1),4);
cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
cv::namedWindow("dilate image");
cv::imshow("dilate image",bg);
waitKey(0);
得到的黑色像素對應背景像素。因此在膨脹後,要立即通過閾值化運算將它們賦值爲128,得到的圖像如下圖所示:
合併這兩個圖像得到標記圖像,代碼如下:
//創建標記圖像
cv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));
markers=fg+bg;
注意:這裏運用重載運算符“+”來合併圖像。得到下面的圖像,將會被輸入分水嶺算法:
在上幅圖像中,白色區域屬於前景物體,灰色區域屬於背景,而黑色區域帶有未知標籤。
接下來,可用下面的方法來分割圖像:
//創建分水嶺分割類的對象
WatershedSegmenter segmenter;
//設置標記圖像,然後執行分割過程
segmenter.setMarkers(markers);
segmenter.process(image1);
//顯示分割結果
cv::namedWindow("Segmentation");
cv::imshow("Segmentation",segmenter.getSegmentation());
waitKey(0);
以上代碼會修改標記圖像,每個值爲0的像素都會被賦予一個輸入標籤,而邊緣處的像素賦值爲-1。得到的標籤圖像如下所示:
邊緣圖像如下:
原理:
用分水嶺算法分割圖像的原理是從高度0開始逐步用洪水淹沒圖像。當“水”的高度逐步增加時(到1、2、3等),會形成聚水的盆地。隨着盆地面積逐步變大,兩個不同盆地的水最終會匯合到一起。這時就要創建一個分水嶺,用來分割這兩個盆地。當水位達到最大高度時,創建的盆地和分水嶺就組成了分水嶺分割圖。
可以想象,在水淹過程的開始階段會創建很多細小的獨立盆地。當所有盆地匯合時,就會創建很多分水嶺線條,導致圖像被過度分割。要解決這個問題,就要對這個算法進行修改,使得水淹的過程從一組預先定義好的標記像素開始。每個用標記創建的盆地,都按照初始標記的值加上標籤。如果兩個標籤相同的盆地匯合,就不創建分水嶺,以避免過度分割。
調用cv::watershed函數時就執行了這些過程。輸入的標記圖像會被修改,用以生成最終的分水嶺分割圖。輸入的標記
圖像可以含有任意數值的標籤,未知標籤的像素值爲0。標記圖像的類型選用32位有符號整數,以便定義超過255個的標籤。另外,可以把分水嶺的對應像素設爲特殊值-1。這是由cv::watershed函數返回的。
完整代碼如下:
#ifndef WatershedSegmenter_h
#define WatershedSegmenter_h
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class WatershedSegmenter {
private:
//用來表示標記(圖)
cv::Mat marker;
public:
//設置標記圖
void setMarkers(const cv::Mat& markers)
{
//watershed()的輸入參數必須爲一個32位有符號的標記,所以要先進行轉換
markers.convertTo(marker, CV_32S);
}
//watershed()
cv::Mat process(const cv::Mat &image)
{
marker.convertTo(marker, CV_32S);
cv::watershed(image, marker);
return marker;
}
cv::Mat getSegmentation() {
cv::Mat tmp;
// 從32S到8U(0-255)會進行飽和運算,所以像素高於255的一律複製爲255
marker.convertTo(tmp,CV_8U);//
return tmp;
}
cv::Mat getWatersheds()
{
cv::Mat tmp;
//在設置標記圖像,即執行setMarkers()後,邊緣的像素會被賦值爲-1,其他的用正整數表示
//下面的這個轉換可以讓邊緣像素變爲-1*255+255=0,即黑色,其餘的溢出,賦值爲255,即白色。
marker.convertTo(tmp, CV_8U,255,255);
return tmp;
}
};
#endif /* WatershedSegmenter_h */
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include "WatershedSegmenter.h"
using namespace cv;
using namespace std;
int main( )
{
Mat image=imread("/Users/zhangxiaoyu/Desktop/1.png",0);//讀取圖像
Mat image1=imread("/Users/zhangxiaoyu/Desktop/1.png");//讀取圖像
if(image.empty())
{
cout<<"Error!cannot be read...../n";
return -1;
}
cv::Mat binary;
cv::threshold(image, binary, 105, 255, THRESH_BINARY_INV);
//消除噪聲和細小物體
cv::Mat fg;
cv::erode(binary, fg, cv::Mat(),cv::Point(-1,-1),4);
//標示不含物體的圖像像素
cv::Mat bg;
cv::dilate(binary,bg, cv::Mat(),cv::Point(-1,-1),4);
cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
//創建標記圖像
cv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));
markers=fg+bg;
//創建分水嶺分割類的對象
WatershedSegmenter segmenter;
//設置標記圖像,然後執行分割過程
segmenter.setMarkers(markers);
segmenter.process(image1);
//顯示分割結果
cv::namedWindow("watersheds image");
cv::imshow("watersheds image",segmenter.getWatersheds());
waitKey(0);
}