基於邊緣的影象分割——分水嶺演算法(watershed)演算法分析(附opencv原始碼分析)
最近需要做一個影象分割的程式,查了opencv的原始碼,發現opencv裡實現的影象分割一共有兩個方法,watershed和mean-shift演算法。這兩個演算法的具體實現都在segmentation.cpp檔案內。
watershed(分水嶺演算法)方法是一種基於邊界點的分割演算法。我想好好的研究一下, 網上找了一些部落格和教程,感覺也就泛泛的解釋了一下實驗的流程,具體演算法的執行過程並不清楚,又把原始論文拿出來看了看,看完了以後也不太清晰,索性把opencv的原始碼挑出來分析一下。
首先,寫一個影象分割的小程式。程式碼如下:
#include "stdafx.h" #include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using namespace std; Vec3b RandomColor(int value); //生成隨機顏色函式 int main( int argc, char* argv[] ) { //src = imread( "E://ylab//DY//ETHZShapeClasses-V1.2//Bottles//green.jpg" ); //src = imread( "E://ylab//DY//ETHZShapeClasses-V1.2//Mugs//jazzburger.jpg" ); //src = imread( "E://1.png" ); //Mat image=imread( "E://ylab//DY//ETHZShapeClasses-V1.2//Bottles//green.jpg" ); Mat image=imread( "E://ylab//DY//ETHZShapeClasses-V1.2//bottles//green.jpg" );//載入RGB彩色影象 imshow("Source Image",image); //灰度化,濾波,Canny邊緣檢測 Mat imageGray,imageCanny; cvtColor(image,imageGray,CV_RGB2GRAY);//灰度轉換 GaussianBlur(imageGray,imageGray,Size(5,5),2); //高斯濾波 imshow("Gray Image",imageGray); Canny(imageGray,imageCanny,40,100); imshow("Canny Image",imageCanny); //查詢輪廓 vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(imageCanny,contours,hierarchy,RETR_LIST,CHAIN_APPROX_SIMPLE,Point()); Mat imageContours=Mat::zeros(image.size(),CV_8UC1); //輪廓 Mat marks(image.size(),CV_32S); //Opencv分水嶺第二個矩陣引數 marks=Scalar::all(0); int index = 0; int compCount = 0; for( ; index >= 0; index = hierarchy[index][0], compCount++ ) { //對marks進行標記,對不同區域的輪廓進行編號,相當於設定注水點,有多少輪廓,就有多少注水點 drawContours(marks, contours, index, Scalar::all(compCount+1), 1, 8, hierarchy); drawContours(imageContours,contours,index,Scalar(255),1,8,hierarchy); } //我們來看一下傳入的矩陣marks裡是什麼東西 Mat marksShows; convertScaleAbs(marks,marksShows); imshow("marksShow",marksShows); imshow("輪廓",imageContours); watershed(image,marks); //我們再來看一下分水嶺演算法之後的矩陣marks裡是什麼東西 Mat afterWatershed; convertScaleAbs(marks,afterWatershed); imshow("After Watershed",afterWatershed); //對每一個區域進行顏色填充 Mat PerspectiveImage=Mat::zeros(image.size(),CV_8UC3); for(int i=0;i<marks.rows;i++) { for(int j=0;j<marks.cols;j++) { int index=marks.at<int>(i,j); if(marks.at<int>(i,j)==-1) { PerspectiveImage.at<Vec3b>(i,j)=Vec3b(255,255,255); } else { PerspectiveImage.at<Vec3b>(i,j) =RandomColor(index); } } } imshow("After ColorFill",PerspectiveImage); //分割並填充顏色的結果跟原始影象融合 Mat wshed; addWeighted(image,0.4,PerspectiveImage,0.6,0,wshed); imshow("AddWeighted Image",wshed); waitKey(); } Vec3b RandomColor(int value) //生成隨機顏色函式 { value=value%255; //生成0~255的隨機數 RNG rng; int aa=rng.uniform(0,value); int bb=rng.uniform(0,value); int cc=rng.uniform(0,value); return Vec3b(aa,bb,cc); }
裡面基本上都是呼叫opencv現成的函式,實現watershed演算法,演算法的執行結果可以看一下:
從程式碼和最終結果裡可以大致的看出演算法的流程:
1.進行灰度化
2.高斯濾波以消除噪聲的干擾
3.用canny運算元檢測邊緣
4.用findcontours查詢輪廓
5.利用輪廓特徵,實現影象分割
然後,把watershed的原始碼單獨拿出來分析一下,我做了一些必要的註釋:
watershed的具體實現中,用佇列來實現對畫素點的儲存。(我本來還以為會用什麼很高階的資料結構或者很複雜的演算法,等真正的看完程式碼才發現,用的也就是很基本的資料結構和想法,只是實現起來有些複雜,需要考慮的細節比較多)這是定義的兩個結構體,CvWSNode用來儲存佇列中的點,其中的next元素用來指向佇列中的下一個點,CvWSQueuev是佇列的頭指標。typedef struct CvWSNode { struct CvWSNode* next; int mask_ofs; int img_ofs; } CvWSNode; typedef struct CvWSQueuev //這個僅僅是一個頭指標 { CvWSNode* first; CvWSNode* last; } CvWSQueue;
static CvWSNode* icvAllocWSNodes( CvMemStorage* storage ) //申請一段連續的記憶體空間,並將記憶體空間連線起來 { CvWSNode* n = 0; int i, count = (storage->block_size - sizeof(CvMemBlock))/sizeof(*n) - 1; n = (CvWSNode*)cvMemStorageAlloc( storage, count*sizeof(*n) ); for( i = 0; i < count-1; i++ ) n[i].next = n + i + 1; n[count-1].next = 0; return n; }
這一個函式的作用就是申請一段連續的記憶體空間,用來儲存畫素點
CV_IMPL void
cvWatershed( const CvArr* srcarr, CvArr* dstarr )
{
const int IN_QUEUE = -2;
const int WSHED = -1;
const int NQ = 256;
cv::Ptr<CvMemStorage> storage;
CvMat sstub, *src;
CvMat dstub, *dst;
CvSize size;
CvWSNode* free_node = 0, *node;
CvWSQueue q[NQ];
int active_queue;
int i, j;
int db, dg, dr;
int* mask;
uchar* img;
int mstep, istep;
int subs_tab[513];
// MAX(a,b) = b + MAX(a-b,0)
#define ws_max(a,b) ((b) + subs_tab[(a)-(b)+NQ])
// MIN(a,b) = a - MAX(a-b,0)
#define ws_min(a,b) ((a) - subs_tab[(a)-(b)+NQ])
#define ws_push(idx,mofs,iofs) \
{ \
if( !free_node ) \
free_node = icvAllocWSNodes( storage );\
node = free_node; \
free_node = free_node->next;\
node->next = 0; \
node->mask_ofs = mofs; \
node->img_ofs = iofs; \
if( q[idx].last ) \
q[idx].last->next=node; \
else \
q[idx].first = node; \
q[idx].last = node; \
}
#define ws_pop(idx,mofs,iofs) \
{ \
node = q[idx].first; \
q[idx].first = node->next; \
if( !node->next ) \
q[idx].last = 0; \
node->next = free_node; \
free_node = node; \
mofs = node->mask_ofs; \
iofs = node->img_ofs; \
}
#define c_diff(ptr1,ptr2,diff) \
{ \
db = abs((ptr1)[0] - (ptr2)[0]);\
dg = abs((ptr1)[1] - (ptr2)[1]);\
dr = abs((ptr1)[2] - (ptr2)[2]);\
diff = ws_max(db,dg); \
diff = ws_max(diff,dr); \
assert( 0 <= diff && diff <= 255 ); \
}
src = cvGetMat( srcarr, &sstub );
dst = cvGetMat( dstarr, &dstub );
if( CV_MAT_TYPE(src->type) != CV_8UC3 )
CV_Error( CV_StsUnsupportedFormat, "Only 8-bit, 3-channel input images are supported" );
if( CV_MAT_TYPE(dst->type) != CV_32SC1 )
CV_Error( CV_StsUnsupportedFormat,
"Only 32-bit, 1-channel output images are supported" );
if( !CV_ARE_SIZES_EQ( src, dst ))
CV_Error( CV_StsUnmatchedSizes, "The input and output images must have the same size" );
size = cvGetMatSize(src); //圖片的大小size 有height和width分量
storage = cvCreateMemStorage();
istep = src->step;
img = src->data.ptr;
mstep = dst->step / sizeof(mask[0]); //step為每行元素的位元組數,
mask = dst->data.i;
memset( q, 0, NQ*sizeof(q[0]) ); //void *memset(void *s,int c,size_t n) 總的作用:將已開闢記憶體空間 s 的首 n 個位元組的值設為值 c。
for( i = 0; i < 256; i++ )
subs_tab[i] = 0;
for( i = 256; i <= 512; i++ )
subs_tab[i] = i - 256;
// draw a pixel-wide border of dummy "watershed" (i.e. boundary) pixels
for( j = 0; j < size.width; j++ )
mask[j] = mask[j + mstep*(size.height-1)] = WSHED; //把mask的上邊界和下邊界畫素賦值為-1
// initial phase: put all the neighbor pixels of each marker to the ordered queue -
// determine the initial boundaries of the basins
for( i = 1; i < size.height-1; i++ )
{
img += istep; mask += mstep;
mask[0] = mask[size.width-1] = WSHED; //把mask的左邊界和右邊界畫素賦值為-1
for( j = 1; j < size.width-1; j++ ) //把和邊界點相鄰的點連成連結串列,進行儲存
{
int* m = mask + j;
if( m[0] < 0 ) m[0] = 0;
if( m[0] == 0 && (m[-1] > 0 || m[1] > 0 || m[-mstep] > 0 || m[mstep] > 0) )
{
uchar* ptr = img + j*3;
int idx = 256, t;
if( m[-1] > 0 )
c_diff( ptr, ptr - 3, idx );
if( m[1] > 0 )
{
c_diff( ptr, ptr + 3, t );
idx = ws_min( idx, t );
}
if( m[-mstep] > 0 )
{
c_diff( ptr, ptr - istep, t );
idx = ws_min( idx, t );
}
if( m[mstep] > 0 )
{
c_diff( ptr, ptr + istep, t );
idx = ws_min( idx, t );
}
assert( 0 <= idx && idx <= 255 );
ws_push( idx, i*mstep + j, i*istep + j*3 );
m[0] = IN_QUEUE; // IN_QUEUE=-2 在序列裡,就把Mark矩陣邊緣點相鄰點的位置賦值為-2,
}
}
}
// find the first non-empty queue
for( i = 0; i < NQ; i++ )
if( q[i].first )
break;
// if there is no markers, exit immediately
if( i == NQ )
return;
active_queue = i;
img = src->data.ptr;
mask = dst->data.i;
// recursively fill the basins 遞迴的填充盆地
for(;;)
{
int mofs, iofs;
int lab = 0, t;
int* m;
uchar* ptr;
if( q[active_queue].first == 0 )
{
for( i = active_queue+1; i < NQ; i++ )
if( q[i].first )
break;
if( i == NQ )
break;
active_queue = i;
}
ws_pop( active_queue, mofs, iofs );
m = mask + mofs;
ptr = img + iofs;
t = m[-1];
if( t > 0 ) lab = t;
t = m[1];
if( t > 0 )
{
if( lab == 0 ) lab = t;
else if( t != lab ) lab = WSHED;
}
t = m[-mstep];
if( t > 0 )
{
if( lab == 0 ) lab = t;
else if( t != lab ) lab = WSHED;
}
t = m[mstep];
if( t > 0 )
{
if( lab == 0 ) lab = t;
else if( t != lab ) lab = WSHED;
}
assert( lab != 0 );
m[0] = lab;
if( lab == WSHED )
continue;
if( m[-1] == 0 )
{
c_diff( ptr, ptr - 3, t );
ws_push( t, mofs - 1, iofs - 3 );
active_queue = ws_min( active_queue, t );
m[-1] = IN_QUEUE;
}
if( m[1] == 0 )
{
c_diff( ptr, ptr + 3, t );
ws_push( t, mofs + 1, iofs + 3 );
active_queue = ws_min( active_queue, t );
m[1] = IN_QUEUE;
}
if( m[-mstep] == 0 )
{
c_diff( ptr, ptr - istep, t );
ws_push( t, mofs - mstep, iofs - istep );
active_queue = ws_min( active_queue, t );
m[-mstep] = IN_QUEUE;
}
if( m[mstep] == 0 )
{
c_diff( ptr, ptr + istep, t );
ws_push( t, mofs + mstep, iofs + istep );
active_queue = ws_min( active_queue, t );
m[mstep] = IN_QUEUE;
}
}
}
ws_max和我說ws_min這兩個函式說白了就是求最大值和最小值,但是這裡用來求最大最小值的方法還真是奇特,我還是第一次見到,其中使用了subs_tab,然後利用這個陣列的值就可以求極大值和極小值。
ws_push和ws_pop這兩個函式,從名字上就可以看出來,一個是往佇列的末端加入元素,另一個是從佇列的頂端取出元素。
c_diff是為了計算圖片RGB三個分量的梯度,並選出一個梯度最大的。
傳入watershed函式共有兩個引數,第一個引數是原始的3通道彩色圖片,第二個引數是Mark矩陣,這個矩陣裡,每一條輪廓都被表示了出來,而且,每一條輪廓的灰度值是不同的,這些輪廓,就被該演算法成為種子,演算法就是從這些輪廓上的點開始計算並分割成不同的區域的。除了邊界點外的其他區域,畫素值都賦值為0。
後面緊接著一個兩層迴圈的函式,這個兩層迴圈是為了把與邊界點相鄰的元素都找出來,然後以這些點作為出發點。
後面的演算法,緊接著就是一個不斷進入佇列和離開佇列的過程,如果是靠近邊界點,就賦值成和邊界點一樣的畫素值,周圍的點有非零的點,就賦值成同樣的灰度值,如果是邊界點。這樣,從不同的邊界點延伸出的區域,就具有了不同的顏色,也就完成了影象的分割。
相關推薦
基於邊緣的影象分割——分水嶺演算法(watershed)演算法分析(附opencv原始碼分析)
最近需要做一個影象分割的程式,查了opencv的原始碼,發現opencv裡實現的影象分割一共有兩個方法,watershed和mean-shift演算法。這兩個演算法的具體實現都在segmentation.cpp檔案內。 watershed(分水嶺演算法)方法是一種基於邊界點
影象分割 | FCN資料集製作的全流程(影象標註)
一 全卷積神經網路 文章所有程式碼已上傳至github,覺得好用就給個star吧,謝謝 深度學習影象分割(FCN)訓練自己的模型大致可以以下三步: 1.為自己的資料製作label; 2.將自己的資料分為train,val和test集; 3.仿照voc_lyaers.py編寫自己的輸入資料層。
影象分割--分水嶺分割法
#include <iostream> #include<opencv2\highgui\highgui.hpp> #include<opencv2\opencv.hpp> using namespace std; using namespace cv;
基於Android簡單備忘錄的設計與實現(附git原始碼連結)
前言 課程作業需要,於是忙活兩天寫了一個簡單的備忘錄,使用了ListView,SQLite。 開發環境:Android Studio 原始碼連結:https://gitee.com/zg0212/Memoire 功能截圖 主頁面 新建頁面
基於struts2的學生報道管理系統(附github原始碼地址)
本專案參考了《java web輕量級開發全體驗》,加入了對mysql的支援。 一、基本業務功能 通過struts2框架,結合mysql資料庫構建一個學生報到管理系統,來模擬學生報到登記的過程。基本功能
Windows虛擬地址轉物理地址(原理+源碼實現,附簡單小工具)
size_t \n 動手 機器碼 status 情況 direct nload amp
記錄小文件上傳的幾個例子(含進度條效果,附源碼下載)
clas 運行 open ntb httppost 會有 text recent mappath 1、簡單原生上傳 無javascript腳本、無進度條; 借助iframe實現post提交後的無刷新效果; jquery插件ajaxFileUpload.js的實現原
兄弟連區塊鏈入門教程eth原始碼分析p2p-udp.go原始碼分析(二)
ping方法與pending的處理,之前談到了pending是等待一個reply。 這裡通過程式碼來分析是如何實現等待reply的。pending方法把pending結構體傳送給addpending. 然後等待訊息的處理和接收。 // ping sends a ping message to the giv
struts2+spring+hibernate框架總結(框架分析+環境搭建+例項原始碼下載)
首先,SSH不是一個框架,而是多個框架(struts+spring+hibernate)的整合,是目前較流行的一種Web應用程式開源整合框架,用於構建靈活、易於擴充套件的多層Web應用程式。 整合SSH框架的系統從職責上分為四層:表示層、業務邏輯層、資料持久層和域模組層(實體層)。 Struts
SSH框架總結(框架分析+環境搭建+例項原始碼下載)
首先,SSH不是一個框架,而是多個框架(struts+spring+hibernate)的整合,是目前較流行的一種Web應用程式開源整合框架,用於構建靈活、易於擴充套件的多層Web應用程式。 整合SSH框架的系統從職責上分為四層:表示層、業務邏輯層、資料持久層和域模組層
springBoot 搭建web專案(前後端分離,附專案原始碼地址)
springBoot 搭建web專案(前後端分離,附專案原始碼地址) 概述 該專案包含springBoot-example-ui 和 springBoot-example,分別為前端與後端,前後端分離,利用ajax互動。 springBoot-exam
近200篇機器學習&深度學習資料分享(含各種文件,視訊,原始碼等)
編者按:本文收集了百來篇關於機器學習和深度學習的資料,含各種文件,視訊,原始碼等。而且原文也會不定期的更新,望看到文章的朋友能夠學到更多。 介紹:這是一篇介紹機器學習歷史的文章,介紹很全面,從感知機、神經網路、決策樹、SVM、Adaboost 到隨機森林、Deep Learning. 介紹:這是瑞
RxJava(一) create操作符的用法和原始碼分析
RxJava系列文章目錄導讀: 1 create操作符的基本使用 顧名思義,Create操作符是用來建立一個Observable的。下面來看一個簡單的程式碼段: Observable.create(new Observable.OnSubscrib
Vue2.0結合iView快速搭建後臺管理網站模板(附github原始碼地址)
一、專案背景: 嘗試使用vue結合其UI框架iView快速搭建網站後臺模板(在前後端分離的大背景下,傳統的js、jquery已經不在是搭建前端的首選,尤其是mvvm模式下衍生出來的react.js、angular.js和vue.js等框架是的前端開發更加高效簡潔,效能提高的
Rxjava2.x 原始碼分析,Map操作符原始碼分析(二)
首先看一個小例子: Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(Observa
Linux下好用的MarkDown編輯器——Remarkable(官網掛了,附Github下載地址)
轉戰Ubuntu,想找一款合手的MarkDown本地編輯器,好多人都推薦Remarkable,看了Youtube上的介紹也確實很強大,功能比我在Windows下用的MarkdownPad2還多了個TOC,而且MarkDownPad還得收費的Pro版本才有的匯出HTML等功能
Android 之 三級快取(記憶體!!!、本地、網路)及記憶體LruCache擴充套件 及原始碼分析--- 學習和程式碼講解
一. 三級快取簡介 如上圖所示,目前App中UI介面經常會涉及到圖片,特別是像“今日關注”新聞這類app中,圖片運用的機率十分頻繁。當手機上需要顯示大量圖片類似listView、gridView控制元件並且使用者會上下滑動,即將瀏覽過的圖片又載入一遍,
我的android多執行緒程式設計之路(1)之經驗詳解,原始碼分析
寫在伊始 android開發這麼久了,對於多執行緒這塊一直處於似懂非懂的神奇狀態,今天總結出來,分享一下,希望大家多多指正。共同交流,懇望得到您的建議。 本文簡介 本文會基於自己在開發中對於執行緒這塊的實際使用,大概從執行緒程序的概念,執行緒的建立(T
深入理解Spark 2.1 Core (一):RDD的原理與原始碼分析
本文連結:http://blog.csdn.net/u011239443/article/details/53894611 該論文來自Berkeley實驗室,英文標題為:Resilient Distributed Datasets: A Fault-Toler
微信開發獲取地理位置例項(java,非常詳細,附工程原始碼)
在本篇部落格之前,博主已經寫了4篇關於微信相關文章,其中三篇是本文基礎: 1、微信開發之入門教程,該文章詳細講解了企業號體驗號免費申請與一些必要的配置,以及如何呼叫微信介面。 2、微信開發之通過代理除錯本地專案,該文章詳細講解了如何除錯本地專案,使用工具的詳細安裝與配置。