1.32【OpenCV圖像處理】基於距離變換和分水嶺的圖像分割
圖像分割、距離變換distanceTransform()、分水嶺分割watershed()
1.32.1 圖像分割(Image Segmentation)
圖像分割:就是根據灰度、顏色、紋理和形狀等特徵,把圖像分成若干個特定的、具有獨特性質的區域,這些特徵在同一區域內呈現出相似性,而在不同區域間呈現出明顯的差異性,並提出感興趣目標的技術和過程。 它是由圖像處理到圖像分析的關鍵步驟。從數學角度來看,圖像分割是將數字圖像劃分成互不相交的區域的過程。圖像分割的過程也是一個標記過程,即把屬於同一區域的像索賦予相同的編號。
目標:是將圖像中像素根據一定的規則分為若干(N)個聚(cluster)集合,每個集合包含一類像素。將對象在背景提取出來。
根據演算法分為監督學習方法(根據模型標籤分類)和無監督學習方法(根據規則自動聚類),圖像分割的演算法多數都是無監督學習方法(KMeans)。
圖像分割方法:基於閾值的分割方法、基於邊緣的分割方法、基於區域的分割方法、基於特定理論的分割方法等。圖像分割方法概述如下:
1、基於閾值的分割方法
閾值法的基本思想是基於圖像的灰度特徵來計算一個或多個灰度閾值,並將圖像中每個像素的灰度值與閾值相比較,最後將像素根據比較結果分到合適的類別中。因此,該類方法最為關鍵的一步就是按照某個準則函數來求解最佳灰度閾值。
2、基於邊緣的分割方法
所謂邊緣是指圖像中兩個不同區域的邊界線上連續的像素點的集合,是圖像局部特徵不連續性的反映,體現了灰度、顏色、紋理等圖像特性的突變。通常情況下,基於邊緣的分割方法指的是基於灰度值的邊緣檢測,它是建立在邊緣灰度值會呈現出階躍型或屋頂型變化這一觀測基礎上的方法。
階躍型邊緣兩邊像素點的灰度值存在著明顯的差異,而屋頂型邊緣則位於灰度值上升或下降的轉折處。對於階躍狀邊緣常用微分運算元進行邊緣檢測,其位置對應一階導數的極值點,對應二階導數的過零點(零交叉點)。常用的一階微分運算元有Roberts運算元、Prewitt運算元和Sobel運算元,二階微分運算元有Laplace運算元和Kirsh運算元等。在實際中各種微分運算元常用小區域模板來表示,微分運算是利用模板和圖像卷積來實現。這些運算元對雜訊敏感,只適合於雜訊較小不太複雜的圖像。
3、基於區域的分割方法
此類方法是將圖像按照相似性準則分成不同的區域,主要包括種子區域生長法、區域分裂合併法和分水嶺法等幾種類型。
種子區域生長法是從一組代表不同生長區域的種子像素開始,接下來將種子像素鄰域里符合條件的像素合併到種子像素所代表的生長區域中,並將新添加的像素作為新的種子像素繼續合併過程,直到找不到符合條件的新像素為止。該方法的關鍵是選擇合適的初始種子像素以及合理的生長準則。
區域分裂合併法(Gonzalez,2002)的基本思想是首先將圖像任意分成若干互不相交的區域,然後再按照相關準則對這些區域進行分裂或者合併從而完成分割任務,該方法既適用於灰度圖像分割也適用於紋理圖像分割。
分水嶺法(Meyer,1990)是一種基於拓撲理論的數學形態學的分割方法,其基本思想是把圖像看作是測地學上的拓撲地貌,圖像中每一點像素的灰度值表示該點的海拔高度,每一個局部極小值及其影響區域稱為集水盆,而集水盆的邊界則形成分水嶺。該演算法的實現可以模擬成洪水淹沒的過程,圖像的最低點首先被淹沒,然後水逐漸淹沒整個山谷。當水位到達一定高度的時候將會溢出,這時在水溢出的地方修建堤壩,重複這個過程直到整個圖像上的點全部被淹沒,這時所建立的一系列堤壩就成為分開各個盆地的分水嶺。分水嶺演算法對微弱的邊緣有著良好的響應,但圖像中的雜訊會使分水嶺演算法產生過分割的現象。
4、基於圖論的分割方法
此類方法把圖像分割問題與圖的最小割(min cut)問題相關聯。首先將圖像映射為帶權無向圖G=<V,E>,圖中每個節點N∈V對應於圖像中的每個像素,每條邊∈E連接著一對相鄰的像素,邊的權值表示了相鄰像素之間在灰度、顏色或紋理方面的非負相似度。而對圖像的一個分割s就是對圖的一個剪切,被分割的每個區域C∈S對應著圖中的一個子圖。而分割的最優原則就是使劃分後的子圖在內部保持相似度最大,而子圖之間的相似度保持最小。基於圖論的分割方法的本質就是移除特定的邊,將圖劃分為若干子圖從而實現分割。目前所了解到的基於圖論的方法有GraphCut、GrabCut和Random Walk等。
5、基於能量泛函的分割方法
該類方法主要指的是活動輪廓模型(active contour model)以及在其基礎上發展出來的演算法,其基本思想是使用連續曲線來表達目標邊緣,並定義一個能量泛函使得其自變數包括邊緣曲線,因此分割過程就轉變為求解能量泛函的最小值的過程,一般可通過求解函數對應的歐拉(Euler.Lagrange)方程來實現,能量達到最小時的曲線位置就是目標的輪廓所在。按照模型中曲線表達形式的不同,活動輪廓模型可以分為兩大類:參數活動輪廓模型(parametric active contour model)和幾何活動輪廓模型(geometric active contour model)。
參數活動輪廓模型是基於Lagrange框架,直接以曲線的參數化形式來表達曲線,最具代表性的是由Kasset a1(1987)所提出的Snake模型。該類模型在早期的生物圖像分割領域得到了成功的應用,但其存在著分割結果受初始輪廓的設置影響較大以及難以處理曲線拓撲結構變化等缺點,此外其能量泛函只依賴於曲線參數的選擇,與物體的幾何形狀無關,這也限制了其進一步的應用。
幾何活動輪廓模型的曲線運動過程是基於曲線的幾何度量參數而非曲線的表達參數,因此可以較好地處理拓撲結構的變化,並可以解決參數活動輪廓模型難以解決的問題。而水平集(Level Set)方法(Osher,1988)的引入,則極大地推動了幾何活動輪廓模型的發展,因此幾何活動輪廓模型一般也可被稱為水平集方法。
(以上概念轉自博客http://blog.csdn.net/zouxy09/article/details/8532106 )
1.32.2 距離變換與分水嶺介紹
(1) 距離變換
還記得上節課的內容,測試點多邊形得到結果跟距離變換相似
距離變換常見演算法有兩種:
1)基於倒角距離(如右1)
2)不斷膨脹/ 腐蝕得到(如右2)
(2) 分水嶺
分水嶺分割基於拓撲理論,每一點像素的灰度值表示該點的海拔高度,像素值低的是盆地,像素值高的是山脊,模擬洪水淹沒過程,最低點首先被淹沒,兩個山頭之間被水分割,圖中紅線處為山脊。
1.32.3 相關API
(1)距離變換:計算源圖像的每個像素到最近的零像素的距離.
distanceTransform(
InputArray src, //8位單通道(二進位)源圖像
OutputArray dst, //輸出具有計算距離的圖像,8位或32位浮點的單通道圖像,大小與src相同.
OutputArray labels, //輸出二維數組標籤labels(離散維諾Voronoi圖)
int distanceType, //距離類型 = DIST_L1/DIST_L2
int maskSize, //距離變換的掩膜大小,DIST_MASK_3(maskSize = 3x3),最新的支持DIST_MASK_5(mask = 5x5),推薦3x3
int labelType=DIST_LABEL_CCOMP //要生成的標籤數組的類型
)
distanceTransform( //這是一個重載的成員函數,它與上述函數的區別僅在於它接受的參數。
InputArray src,
OutputArray dst,
int distanceType,
int maskSize,
int dstType = CV_32F //輸出圖像類型,CV_8U or CV_32F。默認5或CV_32F,CV_8U類型只能用於函數的第一個變數和距離類型distanceType == DIST_L1。
)
distanceTransform(binaryImg, distImg, DIST_L1, 3, 5); //距離變換:輸入二值圖,輸出距離圖像,距離類型,掩膜大小,輸出圖像的類型5或CV_32Fn
(2)分水嶺:用分水嶺演算法執行基於標記的圖像分割
watershed(
InputArray image, //輸入8位3通道圖像(輸入銳化原圖8位3通道)
InputOutputArray markers // 輸入或輸出32位單通道的標記,和圖像一樣大小。(輸入高峰輪廓標記)
)
/* 10.執行分水嶺,分割對象 perform watershed */nwatershed(src, markers); //分水嶺變換,分割對象:輸入銳化原圖8位3通道,輸入高峰輪廓標記nMat mark = Mat::zeros(markers.size(), CV_8UC1);nmarkers.convertTo(mark, CV_8UC1);nbitwise_not(mark, mark, Mat()); //位取反,變黑背景nimshow("watershed image", mark); //各對象灰度等級不一樣n
處理流程:
1)去背景,將白色背景變成黑色(為後面的變換做準備)
2)銳化sharp,提高對比度,突出邊緣(使用filter2D與拉普拉斯運算元) - filter2D()
3)轉灰度再自動閾值二值化 - threshold()
4)距離變換,輸出距離圖像(高峰較亮) - distanceTransform()
5)歸一化距離變換結果到[0~1]之間,梯度效果 - normalize()
6)全局閾值再次二值化,各對象分開- threshold() 0.4~1
7)二值腐蝕,僅得到各個高峰Peak - erode()
8)發現高峰輪廓 – findContours()
9)繪製高峰輪廓標記(最高山頭)(填充了)- drawContours()
10)基於標記的分水嶺分割銳化原圖(一個對象一個灰度等級) - watershed()
11)著色每個分割對象區域,輸出結果。
完整程序:
/*1.32 基於距離變換與分水嶺的圖像分割*/n#include <opencv2/opencv.hpp>n#include <iostream>n#include <math.h>nnusing namespace std;nusing namespace cv;nnint main(int argc, char** argv) {nMat src = imread("E:/OpenCV/testimage/牌.jpg");nif (src.empty()) {n printf("could not load image...n");nreturn -1;n }nchar input_win[] = "input image";nchar watershed_win[] = "watershed segmentation demo";n namedWindow(input_win, CV_WINDOW_AUTOSIZE);n imshow(input_win, src);nn/* 1.轉為黑背景 change background */nfor (int row = 0; row < src.rows; row++) {nfor (int col = 0; col < src.cols; col++) {nif (src.at<Vec3b>(row, col) == Vec3b(255, 255, 255)) {n src.at<Vec3b>(row, col)[0] = 0;n src.at<Vec3b>(row, col)[1] = 0;n src.at<Vec3b>(row, col)[2] = 0;n }n }n }n namedWindow("black background", CV_WINDOW_AUTOSIZE);n imshow("black background", src);nn /* 2.銳化 sharpen,提高對比度,突出邊緣 */nMat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1); //核nMat imgLaplance;nMat sharpenImg = src;nfilter2D(src, imgLaplance, CV_32F, kernel, Point(-1, -1), 0, BORDER_DEFAULT); //有卷積核的要用32位深度,浮點數有正負值有可能超過255,避免精度丟失(均值模糊不用)n src.convertTo(sharpenImg, CV_32F);nMat resultImg = sharpenImg - imgLaplance;n resultImg.convertTo(resultImg, CV_8UC3); //數據類型轉變n// imgLaplance.convertTo(imgLaplance, CV_8UC3);n imshow("sharpen image", resultImg);n src = resultImg; //用銳化的結果nn/* 3.轉為灰度再二值化 convert to binary */nMat binaryImg;n cvtColor(src, resultImg, CV_BGR2GRAY);n threshold(resultImg, binaryImg, 40, 255, THRESH_BINARY|THRESH_OTSU); //自動閾值n imshow("binary image", binaryImg);nn /* 4.距離變化 distance transform*/nMat distImg;n distanceTransform(binaryImg, distImg, DIST_L1, 3, 5); //距離變換:輸入二值圖,輸出距離圖像,距離類型,掩膜大小,輸出圖像的類型5或CV_32Fnn/* 5.對距離變換結果進行歸一化到[0~1]之間,梯度效果 */nnormalize(distImg, distImg, 0, 1, NORM_MINMAX); //標準化歸一化到[0~1]n imshow("distanceTransform image", distImg);nn/* 6.距離變化的二值化 binary again,各對象孤立*/n threshold(distImg, distImg, 0.4, 1, THRESH_BINARY); //全局閾值0.4-1n imshow("distance binary image", distImg);nn /* 7.腐蝕,得到各個高峰 */nMat k1 = Mat::ones(3, 3, CV_8UC1); //結構元素全是1,結構的大小影響分割效果nerode(distImg, distImg, k1, Point(-1, -1)); //腐蝕n imshow("erode binary image", distImg);nn/* 8.尋找輪廓 find contours*/nMat dist_8u;n distImg.convertTo(dist_8u, CV_8U);nvector<vector<Point>> contours;n findContours(dist_8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0)); //尋找輪廓,省去層次結構,檢索外部輪廓,輪廓拐點近似nn/* 9.畫輪廓標記並填充 create marker image */nMat markers = Mat::zeros(src.size(), CV_32SC1); //在黑背景上畫輪廓標記nfor (size_t i = 0; i < contours.size(); i++) {n drawContours(markers, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i) + 1),-1); //繪製輪廓並填充:輸入,輪廓點,輪廓索引號,顏色(轉為int,背景色就是從0開始的,+1避免從0開始),thickness(正值代表線寬,負值代表填充)n }n circle(markers, Point(9, 9), 5, Scalar(255, 255, 255), -1); //左上角畫圓:原圖,圓心,半徑,線顏色,thickness(正值代表圓線寬,負值代表填充圓)n imshow("marker image", markers*1000);nn/* 10.執行分水嶺,分割對象 perform watershed */nwatershed(src, markers); //分水嶺變換,分割對象:輸入銳化原圖8位3通道,輸入高峰輪廓標記nMat mark = Mat::zeros(markers.size(), CV_8UC1);n markers.convertTo(mark, CV_8UC1);n bitwise_not(mark, mark, Mat()); //位取反,變黑背景n imshow("watershed image", mark); //各對象灰度等級不一樣nn/* 11.填充隨機顏色到每個分割區域並顯示最終結果 fill with color and display final result*/nvector<Vec3b> colors;nfor (size_t i = 0; i < contours.size(); i++) { //生成隨機顏色nint r = theRNG().uniform(0, 255);nint g = theRNG().uniform(0, 255);nint b = theRNG().uniform(0, 255);n colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));n } nMat dst = Mat::zeros(markers.size(), CV_8UC3);nfor (int row = 0; row < markers.rows; row++) {nfor (int col = 0; col < markers.cols; col++) {nint index = markers.at<int>(row, col); //取得分水嶺分割對象的不同灰度等級的值nnif (index > 0 && index <= static_cast<int>(contours.size())) {n dst.at<Vec3b>(row, col) = colors[index - 1]; //每一個非0灰度等級著一種色n }nelse {n dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0); //非對象著黑色n }n }n }n imshow("Final Result", dst);nn waitKey(0);nreturn 0;n}n
運行結果:
推薦閱讀: