圖像分割是按照一定的原則,將一幅圖像分為若干個互不相交的小局域的過程,它是圖像處理中最為基礎的研究領域之一。目前有很多圖像分割方法,其中分水嶺算法是一種基于區域的圖像分割算法,分水嶺算法因實現方便,已經在醫療圖像,模式識別等領域得到了廣泛的應用。
1.傳統分水嶺算法基本原理
分水嶺比較經典的計算方法是L.Vincent于1991年在PAMI上提出的[1]。傳統的分水嶺分割方法,是一種基于拓撲理論的數學形態學的分割方法,其基本思想是把圖像看作是測地學上的拓撲地貌,圖像中每一像素的灰度值表示該點的海拔高度,每一個局部極小值及其影響區域稱為集水盆地,而集水盆地的邊界則形成分水嶺。分水嶺的概念和形成可以通過模擬浸入過程來說明。在每一個局部極小值表面,刺穿一個小孔,然后把整個模型慢慢浸人水中,隨著浸入的加深,每一個局部極小值的影響域慢慢向外擴展,在兩個集水盆匯合處構筑大壩如下圖所示,即形成分水嶺。

傳統分水嶺算法示意圖
然而基于梯度圖像的直接分水嶺算法容易導致圖像的過分割,產生這一現象的原因主要是由于輸入的圖像存在過多的極小區域而產生許多小的集水盆地,從而導致分割后的圖像不能將圖像中有意義的區域表示出來。所以必須對分割結果的相似區域進行合并。
[1]L.Vincent, P Soille. Watersheds in digital space: An efficientalgorithms based on immersion simulation[J]. IEEE Trans. on Pattern Analysisand Machine Intelligence, 1991, 13(6): 583-598.
2.改進的分水嶺算法基本原理
因為傳統分水嶺算法存在過分割的不足,OpenCV提供了一種改進的分水嶺算法,使用一系列預定義標記來引導圖像分割的定義方式。使用OpenCV的分水嶺算法cv::wathershed,需要輸入一個標記圖像,圖像的像素值為32位有符號正數(CV_32S類型),每個非零像素代表一個標簽。它的原理是對圖像中部分像素做標記,表明它的所屬區域是已知的。分水嶺算法可以根據這個初始標簽確定其他像素所屬的區域。傳統的基于梯度的分水嶺算法和改進后基于標記的分水嶺算法示意圖如下圖所示。

傳統基于梯度的分水嶺算法和基于標記的分水嶺算法原理圖
從上圖可以看出,傳統基于梯度的分水嶺算法由于局部最小值過多造成分割后的分水嶺較多。而基于標記的分水嶺算法,水淹過程從預先定義好的標記圖像(像素)開始,較好的克服了過度分割的不足。本質上講,基于標記點的改進算法是利用先驗知識來幫助分割的一種方法。因此,改進算法的關鍵在于如何獲得準確的標記圖像,即如何將前景物體與背景準確的標記出來。
3.基于標記點的分水嶺算法應用
基于標記點的分水嶺算法應用步驟
● 封裝分水嶺算法類
● 獲取標記圖像
獲取前景像素,并用255標記前景
獲取背景像素,并用128標記背景,未知像素,使用0標記
合成標記圖像
● 將原圖和標記圖像輸入分水嶺算法
● 顯示結果
(1)封裝分水嶺算法類
將分水嶺算法cv::watershed(image,markers)封裝進類WatershedSegmenter,并保存為頭文件以便于操作。(本段封裝代碼參考《OpenCV計算機視覺編程攻略(第二版)》)
#if !defined WATERSHS
#define WATERSHS
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class WatershedSegmenter {
private:
cv::Mat markers;
public:
void setMarkers(const cv::Mat& markerImage) {
// Convert to image of ints
markerImage.convertTo(markers,CV_32S);
}
cv::Mat process(const cv::Mat &image) {
// Apply watershed
cv::watershed(image,markers);
return markers;
}
// Return result in the form of an image
cv::Mat getSegmentation() {
cv::Mat tmp;
// all segment with label higher than 255
// will be assigned value 255
markers.convertTo(tmp,CV_8U);
return tmp;
}
// Return watershed in the form of an image以圖像的形式返回分水嶺
cv::Mat getWatersheds() {
cv::Mat tmp;
//在變換前,把每個像素p轉換為255p+255(在conertTo中實現)
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
}
};
#endif
(2)獲取標記圖像
標記前景
讀取原圖
// Read input image
cv::Mat image1= cv::imread("image.jpg");
if (!image1.data)
return 0;
// Display the color image
cv::resize(image1, image1, cv::Size(), 0.7, 0.7);
cv::namedWindow("Original Image1");
cv::imshow("Original Image1",image1);

原圖
以下代碼目的是獲取前景物體的像素,并用255標記。這里使用閾值分割初步分割前景和背景,接著使用形態學閉運算連接二值圖像中前景的各個部分,并平滑邊緣。如何更好的獲取前景像素,需要根據實際圖像的情況靈活處理。
// Identify image pixels with object
Mat binary;
cv::cvtColor(image1,binary,COLOR_BGRA2GRAY);
cv::threshold(binary,binary,30,255,THRESH_BINARY_INV);//閾值分割原圖的灰度圖,獲得二值圖像
// Display the binary image
cv::namedWindow("binary Image1");
cv::imshow("binary Image1",binary);
waitKey();
// CLOSE operation
cv::Mat element5(5,5,CV_8U,cv::Scalar(1));//5*5正方形,8位uchar型,全1結構元素
cv::Mat fg1;
cv::morphologyEx(binary, fg1,cv::MORPH_CLOSE,element5,Point(-1,-1),1);// 閉運算填充物體內細小空洞、連接鄰近物體
// Display the foreground image
cv::namedWindow("Foreground Image");
cv::imshow("Foreground Image",fg1);
waitKey();

閾值分割原圖像的灰度圖

閉運算獲取前景
標記背景和未知區域
在上面閾值分割得到的二值圖像binary的基礎上,通過對白色前景的深度膨脹運算獲得一個超過前景實際大小的物體,緊接著用反向閾值將深度膨脹后的圖像中的黑色部分轉換成128,即完成了對背景像素的標記。實際上,在0~255范圍內,任意不為0或255的值均可作為背景的標記。當然如果有其他類型的物體,可以使用另外一個數值作為其標記。也就是說,多個目標可以有多個標記來幫助分水嶺算法正確分割圖像。
// Identify image pixels without objects
cv::Mat bg1;
cv::dilate(binary,bg1,cv::Mat(),cv::Point(-1,-1),4);//膨脹4次,錨點為結構元素中心點
cv::threshold(bg1,bg1,1,128,cv::THRESH_BINARY_INV);//>=1的像素設置為128(即背景)
// Display the background image
cv::namedWindow("Background Image");
cv::imshow("Background Image",bg1);
waitKey();

將背景設置為128,未知區域設置為0
合成標記圖像
將前景、背景及未知區域合成為一個標記圖像。則標記圖像中通過255標記前景物體,通過128標記背景,通過0標記未知區域。
//Get markers image
Mat markers1 = fg1 + bg1; //使用Mat類的重載運算符+來合并圖像。
cv::namedWindow("markers Image");
cv::imshow("markers Image",markers1);
waitKey();

標記圖像
(3)分水嶺算法分割圖像
將標記圖像和原圖輸入分水嶺算法封裝的類WatershedSegmenter,執行分水嶺算法,并顯示算法運行的結果。
// Apply watershed segmentation
WatershedSegmenter segmenter1; //實例化一個分水嶺分割方法的對象
segmenter1.setMarkers(markers1);//設置算法的標記圖像,使得水淹過程從這組預先定義好的標記像素開始
segmenter1.process(image1); //傳入待分割原圖
// Display segmentation result
cv::namedWindow("Segmentation1");
cv::imshow("Segmentation1",segmenter1.getSegmentation());//將修改后的標記圖markers轉換為可顯示的8位灰度圖并返回分割結果(白色為前景,灰色為背景,0為邊緣)
waitKey();
// Display watersheds
cv::namedWindow("Watersheds1");
cv::imshow("Watersheds1",segmenter1.getWatersheds());//以圖像的形式返回分水嶺(分割線條)
waitKey();
代碼segmenter1.process(image)將修改標記圖像markers,每個值為0的像素都會被賦予一個輸入標簽,而邊緣處的像素賦值為-1,得到的標簽圖像如下圖所示。

顯示分水嶺分割圖像

分水嶺分割線顯示
(4)顯示結果圖像
本步驟的目的是將前景物體的分割結果在黑/白底色中顯示出來。背景顏色由黑轉白時使用了Mat矩陣掃描的.ptr方法與指針運算。
// Get the masked image
Mat maskimage = segmenter1.getSegmentation();
cv::threshold(maskimage,maskimage,250,1,THRESH_BINARY);
cv::cvtColor(maskimage,maskimage,COLOR_GRAY2BGR);
maskimage = image1.mul(maskimage);
cv::namedWindow("maskimage");
cv::imshow("maskimage",maskimage);
waitKey();
// Turn background (0) to white (255)
int nl= maskimage.rows; // number of lines
int nc= maskimage.cols * maskimage.channels(); // total number of elements per line
for (int j=0; j<nl; j++) {
uchar* data= maskimage.ptr<uchar>(j);
for (int i=0; i<nc; i++)
{
// process each pixel ---------------------
if (*data==0) //將背景由黑色改為白色顯示
*data=255;
data++;//指針操作:如為uchar型指針則移動1個字節,即移動到下1列
}
}
cv::namedWindow("result");
cv::imshow("result",maskimage);
waitKey();

原圖的前景分割圖(黑色背景)

原圖的前景分割圖(白色背景)
從上圖的分割結果可以看出,基于標記圖像的分水嶺算法較好的實現了復雜背景下前景目標分割。算法應用的關鍵步驟為標記圖像的獲取,目前很多文獻提出了各類獲取標記圖像的方法,如何使用還需要根據所處理的圖像來量身確定。

貼出實驗原始圖像:)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。