分水嶺方法是基於拓撲理論的一種分割方法, 簡單講是將圖像每一點像素視為海拔高度(elevation), 每一個局部極小值及其鄰近區域為集水盆地(catchment basin), 當水面逐漸上升時, 下圖中的兩個集水盆地會變成同一個新的大集水盆地, 如果在兩個集水盆地匯合處建構一個水壩, 即得到所謂分水嶺(watershed)
1-D Watershed示意圖[1]
3-D Watershed示意圖[2]
- Minimum(最小值)[3]:這裡最小值定義為connected component analysis中像素值較鄰近像素值低的聯通區塊
- Markers(物件標籤)[4]: 如同connected component analysis 的作法, 會將相連通區域的像素標記相同的號碼, 而每一塊標籤可以表示為前景物件或背景物件
- Gradient image(梯度影像): 輸入和原始影像一樣大小的梯度影像, 資料型態為灰階(8-bit, 0~255)。爲得到圖像的邊緣資訊,通常會進行下列運算,g(x,y)= grad( f(x,y) ),其中f(x,y)表示原始圖像,grad{.}表示梯度運算。
- Marker image(標籤影像): 使用者可以在一張灰階影像中任意繪製一段曲線, 用來標記(marker)這是某一個前景物或背景物內的一條曲線,一般會設定該曲線所在位置的值為一個負數常數
- 載入一張測試圖sample.jpg
在兩個想要切割的物件(object 1 和 object 2)內分別用滑鼠拖曳出兩條曲線當marker
圖為原始彩色影像+兩條markers
下圖為兩個物件的markers
下面一張動畫也可以幫助理解分水嶺分割的精神[5], 紅色區域為三個物件的markers(第1張圖),圖2為flooding迭代過程 迭代完成後可以找到watershed(第3張圖)
=======================================================================
以下參考C:\OpenCV-2.4.7\opencv\sources\samples\cpp\watershed.cpp及部分修改, 附上註解筆記
操作說明help()
1: // Win32ConsoleVS2010OpenCV247.cpp : 定義主控台應用程式的進入點。
2: //
3: #include "stdafx.h"
4: #include <opencv2/core/core.hpp>
5: #include "opencv2/imgproc/imgproc.hpp"
6: #include <opencv2/highgui/highgui.hpp>
7: #include <cstdio>
8: #include <iostream>
9: //#define DEBUG
10: using namespace cv;
11: using namespace std;
12:
13: static void help()
14: {
15: cout << "\nThis program demonstrates the famous watershed segmentation algorithm in OpenCV: watershed()\n"
16: "Usage:\n"
17: "./watershed [image_name -- default is fruits.jpg]\n" << endl;
18:
19:
20: cout << "Hot keys: \n"
21: "\tESC - quit the program\n"
22: "\tr - restore the original image\n"
23: "\tw or SPACE - run watershed segmentation algorithm\n"
24: "\t\t(before running it, *roughly* mark the areas to segment on the image)\n"
25: "\t (before that, roughly outline several markers on the image)\n";
26: }
27: Mat markerMask, img;
28: Point prevPt(-1, -1);
- 滑鼠事件偵測onMouse()
1: static void onMouse( int event, int x, int y, int flags, void* )
2: {
3: if( x < 0 || x >= img.cols || y < 0 || y >= img.rows ) // 若x<0或x>=影寬或y<0或y>=影高, 離開
4: return;
5: if( event == CV_EVENT_LBUTTONUP || !(flags & CV_EVENT_FLAG_LBUTTON) ) // 左鍵up 或 非左鍵, prevPt重置(-1,-1)
6: prevPt = Point(-1,-1);
7: else if( event == CV_EVENT_LBUTTONDOWN ) // 按下左鍵(down), 取得目前滑鼠座標(x,y)
8: prevPt = Point(x,y);
9: else if( event == CV_EVENT_MOUSEMOVE && (flags & CV_EVENT_FLAG_LBUTTON) ) // 按下左鍵同時滑鼠移動
10: {
11: Point pt(x, y);
12: if( prevPt.x < 0 ) // 若prevPt.x座標<0, 則更新prevPt成目前座標
13: prevPt = pt;
14: line( markerMask, prevPt, pt, Scalar::all(255), 5, 8, 0 ); // 在markerMask上畫線(從prevPt到pt)
15: line( img, prevPt, pt, Scalar::all(255), 5, 8, 0 ); // 在img上畫線(從prevPt到pt)
16: prevPt = pt;
17:
18: imshow("markerMask", markerMask); // 顯示markerMask影像
19: imshow("image", img); // 顯示img影像
20: }
21: }
主程式main()
1: int _tmain(int argc, _TCHAR* argv[])
2: {
3: char* filename = "sample.jpg";
4: Mat img0 = imread(filename, 1), imgGray;
5:
6: if( img0.empty() )
7: {
8: cout << "無法開啟影像 " << filename << "\n";
9: return 0;
10: }
11: help(); // 操作說明
12: namedWindow( "image", 1 );
13:
14: img0.copyTo(img); // 複製一份給img, img0為原始圖(不更動)
15: cvtColor(img, markerMask, CV_BGR2GRAY); // img彩色轉灰階markerMask
16: #ifdef DEBUG
17: imshow("markerMask",markerMask);
18: #endif
19: cvtColor(markerMask, imgGray, CV_GRAY2BGR); // 灰階轉彩色
20: #ifdef DEBUG
21: imshow("Gray2BGR",imgGray);
22: #endif
23: markerMask = Scalar::all(0);
24: imshow( "image", img );
25: setMouseCallback( "image", onMouse, 0 ); // 設定callback function :onMouse( int event, int x, int y, int flags, void* )
26: // 事件種類, x,y 座標, flags種類, void*
27: for(;;)
28: {
29: int c = waitKey(0);
30:
31: if( (char)c == 27 ) // Escape鍵
32: break;
33:
34: if( (char)c == 'r' )
35: {
36: markerMask = Scalar::all(0); // 初始化為零值
37: img0.copyTo(img);
38: imshow( "image", img );
39: }
40:
41: if( (char)c == 'w' || (char)c == ' ' ) // 按下w鍵或空白鍵
42: {
43: int i, j, compCount = 0;
44: vector<vector<Point> > contours;
45: vector<Vec4i> hierarchy;
46: // 對mmarkerMask進行輪廓搜尋
47: findContours(markerMask, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
48:
49: if( contours.empty() )
50: continue;
51: Mat markers(markerMask.size(), CV_32S); //初始化markers( 32 bit single )
52: markers = Scalar::all(0);
53: int idx = 0;
54: for( ; idx >= 0; idx = hierarchy[idx][0], compCount++ )
55: drawContours(markers, contours, idx, Scalar::all(compCount+1), -1, 8, hierarchy, INT_MAX);
56:
57:
58: if( compCount == 0 )
59: continue;
60:
61: vector<Vec3b> colorTab; // compCount x 3 隨機色彩表
62: for( i = 0; i < compCount; i++ )
63: {
64: int b = theRNG().uniform(0, 255);
65: int g = theRNG().uniform(0, 255);
66: int r = theRNG().uniform(0, 255);
67:
68: colorTab.push_back( Vec3b((uchar)b, (uchar)g, (uchar)r) );// Vec3b(b, g, r)
69: }
70:
71: double t = (double)getTickCount(); // tic
72: watershed( img0, markers ); // 參數1:原始彩色影像, 參數2: markers(黑白影像), 白色為marker
73: t = (double)getTickCount() - t; // toc
74: printf( "execution time = %gms\n", t*1000./getTickFrequency() ); // 顯示花費時間
75:
76: Mat wshed(markers.size(), CV_8UC3); // wshed(彩色影像)
77:
78: // paint the watershed image
79: for( i = 0; i < markers.rows; i++ )
80: for( j = 0; j < markers.cols; j++ )
81: {
82: int index = markers.at<int>(i,j);
83: if( index == -1 ) // (1) index == -1
84: wshed.at<Vec3b>(i,j) = Vec3b(255,255,255);
85: else if( index <= 0 || index > compCount ) // (2) 超過標記範圍塗黑 index <=0 或 index > compCount
86: wshed.at<Vec3b>(i,j) = Vec3b(0,0,0);
87: else
88: wshed.at<Vec3b>(i,j) = colorTab[index - 1]; // (3) 根據隨機色彩表 第 (index-1) 筆
89: }
90:
91: wshed = wshed*0.5 + imgGray*0.5; // 原始彩色影像(imgGray) + marker隨機塗色
92: imshow( "watershed transform", wshed );
93: }
94: }
95:
96: return 0;
97: }
根據marker進行隨機塗色,下圖為隨機色彩表
===========================================================================================================
debug環境設定檔案
http://www.mediafire.com/download/l2kf42zva55u23q/VS2010OpenCV249X86Debug.props
release環境設定檔案
http://www.mediafire.com/download/3y9uer5ni6e0a0t/VS2010OpenCV249X86Release.props
[1]鄭 育 昕, ”分水嶺法的重疊腕骨分割以擷取骨齡特徵”, 國立中央學生物醫學工程研究所碩士論文
[2]Hai Gaoa, Weisi Linb,, Ping Xuea, Wan-Chi Siu, Marker-based image segmentation relying on disjoint set union, Signal Processing: Image Communication 21 (2006) 100–112
[3]Vincent, L. Watersheds in Digital Spaces: An Efficient Algorithm Based on Immersion Simulations, IEEE TRANSACTIONS ON PATTERN ANALYSIS AND MACHINE INTELLIGENCE, VOL. 13, NO. 6, JUNE 1991
[4]J. Serra, L. Vincent, An overview of morphological filtering, Circuits Systems Signal Process. 11 (1) (1992) 47–108