Opencv之分水嶺原理和實現
在很多實際應用中,我們需要分割影象,分割方法有多種經典的分割方法:
一、常見影象分割方法:
1、基於邊緣檢測的方法:
此方法主要是通過檢測區域的邊緣進行分割,利用區域之間的特徵的不一致性,首先檢測影象中的邊緣點,然後按照一定的方法把這些邊緣點進行全部連線起來,從而構成分割區域。影象中的邊緣通常是灰度,顏色或者紋理,其中基於灰度的方法很普遍,許多邊緣檢測運算元利用灰度來檢測影象的梯度,Roberts 運算元、Laplace 運算元、Prewitt 運算元、Sobel 運算元、Rosonfeld運算元、Kirsch 運算元以及Canny 。邊緣檢測演算法比較適合邊緣灰度值過渡比較顯著且噪聲較小的簡單影象的分割。對於邊緣比較複雜以及存在較強噪聲的影象,則面臨抗噪性和檢測精度的矛盾。若提高檢測精度,則噪聲產生的偽邊緣會導致不合理的輪廓:若提高抗噪性,則會產生輪廓漏檢和位置偏差。
2.閾值分割方法
閾值分割是最古老的分割技術,也是最簡單實用的。許多情況下,影象中目標區域與背景區域或者說不同區域之間其灰度值存在差異,此時可以將灰度的均一性作為依據進行分割。閾值分割即通過一個或幾個閾值將影象分割成不同的區域。閾值分割方法的核心在於如何尋找適當的閾值。最常用的閾值方法是基於灰度直方圖的方法,如最大類間方差法(OTSU)、最小誤差法、最大熵法等。此類方法通常對整幅影象使用固定的全域性閾值,如果影象中有陰影或亮度分佈不均等現象,分割效果會受到影響。基於區域性閾值的分割方法對影象中的不同區域採用不同的閾值,相對於全域性閾值方法具有更好的分割效果,該方法又稱為自適應閾值方法。
3.區域生長
區域生長方法[46]也是一種常用的區域分割技術,其基本思路是首先定義一個生長準則,然後在每個分割區域內尋找一個種子畫素,通過對影象進行掃描,依次在種子點周圍鄰域內尋找滿足生長準則的畫素並將其合併到種子所在的區域,然後再檢查該區域的全部相鄰點,並把滿足生長準則的點合併到該區域,不斷重複該過程直到找不到滿足條件的畫素為止。該方法的關鍵在於種子點的位置、生長準則和生長順序。
4.分水嶺演算法
是以數學形態學作為基礎的一種區域分割方法。其基本思想是將梯度影象看成是假想的地形表面,每個畫素的梯度值表示該點的海拔高度。原圖中的平坦區域梯度較小,構成盆地,邊界處梯度較大構成分割盆地的山脊。分水嶺演算法模擬水的滲入過程,假設水從最低窪的地方滲入,隨著水位上升,較小的山脊被淹沒,而在較高的山脊上築起水壩,防止兩區域合併。當水位達到最高山脊時,演算法結束,每一個孤立的積水盆地構成一個分割區域。由於受到影象噪聲和目標區域內部的細節資訊等因素影響,使用分水嶺演算法通常會產生過分割現象,分水嶺演算法一般是作為一種預分割方法,與其它分割方法結合使用,以提高演算法的效率或精度。
二、分水嶺演算法
在上面的水嶺演算法示意圖中區域性極小值、積水盆地,分水嶺線以及水壩的概念可以描述為:
(1)區域極小值:導數為0的點,區域性範圍內的最小值點;
(2)集水盆(匯水盆地):當“水”落到匯水盆地時,“水”會自然而然地流到匯水盆地中的區域極小值點處。每一個匯水盆地中有且僅有一個區域極小值點;
(3)分水嶺:當“水”處於分水嶺的位置時,會等概率地流向多個與它相鄰的匯水盆地中;
(4)水壩:人為修建的分水嶺,防止相鄰匯水盆地之間的“水”互相交匯影響。
OpenCV提供了函式watershed()來實現分水嶺演算法,它採用的是Meyer在1994年提出的基於距離函式的演算法,具體的論文大家可以搜尋“Meyer, F. Color Image Segmentation, ICIP92, 1992”。
函式原型:
void watershed(InputArray image, InputOutputArray markers)
第一個引數:輸入圖src,需要8位三通道的彩圖;
第二個引數較複雜:“markers中儲存了影象的大致輪廓,<span style="color:#ff0000;">32位單通道影象</span>,並且以值1,2,3..分別表示各個components.markers通常由函式結合使用來獲得。markers相
當於watershed()執行時的種子引數。markers中,不屬於輪廓(outlined regions)的點的值應置為0.函式執行後,
影象中的畫素如果是在由某個輪廓種子生成的區域中,那麼其值就置為這個種子的編號,如果畫素不在輪廓種子生成的區域中(邊界),則置為-1。
網上的經典程式整理,但部分函式沒看懂。。。。
標頭檔案:
#include <opencv2/opencv.hpp>
using namespace cv;
class WatershedSegmenter {
private:
Mat markers;
public:
void setMarkers(const Mat& markerImage) {
// Convert to image of ints
markerImage.convertTo(markers, CV_32S);
}
Mat process(const Mat &image) {
// Apply watershed
watershed(image, markers);
return markers;
}
// Return result in the form of an image
Mat getSegmentation() {
Mat tmp;
// all segment with label higher than 255
// will be assigned value 255
<span style="color:#ff0000;">markers.convertTo(tmp, CV_8U);</span>//這裡沒看懂,CV_32S轉化成CV_8U.
return tmp;
}
// Return watershed in the form of an image
Mat getWatersheds() {
Mat tmps;
//在設定標記影象,即執行process()後,maskers邊緣的畫素會被賦值為-1,其他的用正整數表示
//下面的這個轉換可以讓邊緣畫素變為-1*255+255=0,即黑色,其餘的溢位,賦值為255,即白色。
<span style="color:#ff0000;">markers.convertTo(tmps, CV_8U, 255, 255);
</span>
return tmps;
}
};
getSegmentation()和GetWatersgeds()還沒理解,基礎太差!
#include <iostream>
#include <opencv2/opencv.hpp>
#include "watershedSegmentation.h"
using namespace std;
using namespace cv;
int main()
{
Mat image = imread("D://vvoo//group.jpg");
if (!image.data)
return 0;
imshow("Original Image", image);
// Get the binary map
Mat binary;
cvtColor(image, binary,CV_BGR2GRAY);
threshold(binary, binary, 80, 255, CV_THRESH_BINARY); //閾值化操作
imshow("Binary Image", binary);
// Eliminate noise and smaller objects
Mat fg, bg;
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
erode(binary, fg, element);
imshow("Foreground Image", fg);
dilate(binary, bg, element);
threshold(bg, bg, 1, 128, THRESH_BINARY_INV);
imshow("Background Image", bg);
// Show markers image
Mat marker(binary.size(), CV_8U, Scalar(0));
marker = fg + bg;
imshow("Markers", marker);
// Create watershed segmentation object
WatershedSegmenter segmenter;
// Set marker and process
segmenter.setMarkers(marker);
segmenter.process(image);
// Display segmentation result
imshow("Segmentation", segmenter.getSegmentation());
// Display watersheds
imshow("Watersheds", segmenter.getWatersheds());
waitKey(0);
return 0;
}
個人理解:
其中markers是前景和背景進行相加得到,感覺因為前景和背景的畫素值不同,相加後就得到了“地形圖”,然後想這些地形圖灌水,如圖:
我的意思是:其中牛的中心灰度是128(小山嶺),白色為255(大山嶺),黑色為0(盆地),makers灰度最低的是0(盆地),然後我們向黑色區域(盆地)灌水,慢慢地灌溉到灰色區域(與灰色區域水位持平)時,灌溉結束後就是上面右圖。
imshow("Watersheds", segmenter.getWatersheds()),顯示的結果是最終的圖(左邊),右邊的圖是原圖,別大家看了結果圖後不知分割出來的東東是什麼:
三、grabCut()分割介紹
grabcut是在graph cut基礎上改進的一種影象分割演算法,它同樣是基於圖割理論。稍微看了下grabcut方面的論文,論文中一般都是在graph cut上作改進,比如說引入了GMM模型等。同graph cut一樣,在使用grabcut是也是需要人機互動的,即人工先給定一定區域的目標或者背景,然後送給grabcut演算法來分割。通過實驗發現,其分割效果一般般,且分割速度比較慢,一張普通大小的圖片差不多需要1s左右的時間,
void cv::grabCut( const Mat& img, Mat& mask, Rect rect, Mat& bgdModel, Mat& fgdModel, int iterCount, int mode )
img——待分割的源影象,必須是8位3通道(CV_8UC3)影象,在處理的過程中不會被修改;
mask——掩碼影象,如果使用掩碼進行初始化,那麼mask儲存初始化掩碼資訊;在執行分割的時候,也可以將使用者互動所設定的前景與背景儲存到mask中,然後再傳入grabCut函式;在處理結束之後,mask中會儲存結果。mask只能取以下四種值:
GCD_BGD(=0),背景;
GCD_FGD(=1),前景;
GCD_PR_BGD(=2),可能的背景;
GCD_PR_FGD(=3),可能的前景。
如果沒有手工標記GCD_BGD或者GCD_FGD,那麼結果只會有GCD_PR_BGD或GCD_PR_FGD;
rect——用於限定需要進行分割的影象範圍,只有該矩形視窗內的影象部分才被處理;
bgdModel——背景模型,如果為null,函式內部會自動建立一個bgdModel;bgdModel必須是單通道浮點型(CV_32FC1)影象,且行數只能為1,列數只能為13x5;
fgdModel——前景模型,如果為null,函式內部會自動建立一個fgdModel;fgdModel必須是單通道浮點型(CV_32FC1)影象,且行數只能為1,列數只能為13x5;
iterCount——迭代次數,必須大於0;
mode——用於指示grabCut函式進行什麼操作,可選的值有:
GC_INIT_WITH_RECT(=0),用矩形窗初始化GrabCut;
GC_INIT_WITH_MASK(=1),用掩碼影象初始化GrabCut;
GC_EVAL(=2),執行分割。
小程式:用時時間太長,7s左右,分割效果不好
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
Mat srcImage = imread("D://vvoo//group.jpg");
Mat mask;
Mat foreground;
Rect rect(0, 100, 500, 200);
Mat bkgModel, fgrModel; // the models (internally used)
// GrabCut segmentation
grabCut(srcImage, // input image
mask, // segmentation result
rect, bkgModel, fgrModel, 5, GC_INIT_WITH_RECT);
// Get the pixels marked as likely foreground
mask = mask & 1;//try to find forgeground
foreground.create(srcImage.size(), CV_8UC3);
foreground.setTo(Scalar(255, 255, 255));
srcImage.copyTo(foreground, mask); // pixels of background are not copied;pixels of srcImage is copied if mask isn't zero
// draw rectangle on original image
rectangle(srcImage, rect, Scalar(255, 255, 255), 1);
imshow("Original Image", srcImage);
// display result
imshow("Foreground objects", foreground);
waitKey();
return 0;
}
結果:還有好多草