前言
grabcut是在graph cut基礎上改進的一種圖像分割算法,它同樣是基於圖割理論的,關於圖割的簡單介紹可以參考本人前面的博文:一些知識點的初步理解_8(Graph Cuts,ing...) 。稍微看了下grabcut方面的論文,論文中一般都是在graph cut上作改進,比如說引入了GMM模型等。同graph cut一樣,在使用grabcut是也是需要人機交互的,即人工先給定一定區域的目標或者背景,然后送給grabcut算法來分割。通過實驗發現,其分割效果一般般,且分割速度比較慢,一張普通大小的圖片差不多需要1s左右的時間,
實驗說明
因為在使用該算法的過程中需要人機交互,所以需要用到很多鼠標鍵盤的操作,因此這些GUI操作的邏輯關系一定要處理好。其中最主要的GUI設計就在鼠標鍵盤響應函數中,在設置關於圖片的鼠標鍵盤響應函數時,其函數的參數必須滿足一定的類型,如下所示:
mouseClick (int event, int x, int y, int flags, void* param);
其中event是 CV_EVENT_*變量之一; x和y是鼠標指針在圖像坐標系的坐標,並不是整個窗口的坐標; flags是CV_EVENT_FLAG的組合, 即表示所有的按鍵,一般情況下是固定的; param是用戶定義的傳遞到cvSetMouseCallback函數調用的參數,這通常在回調函數中都有類似這種功能的的參數。
Grabcut在opencv中核心算法函數為:
void cv::grabCut( const Mat& img, Mat& mask, Rect rect, Mat& bgdModel, Mat& fgdModel, int iterCount, int mode )
這里引用網友的博文學習OpenCV——grabcut來解釋該函數的用法:
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),執行分割。
實驗結果
分割前的原始圖如下所示:

由於本次實驗是一個人機交互的過程,需要人的手工輸入前景點和背景點,或者可能的前景點和背景點,本次實驗中,本人只用鼠標畫出了一些前景點(Shift+鼠標左鍵+鼠標移動)和一些背景點(Ctrl+鼠標左鍵+鼠標移動),如下圖所示(其中紅色的為前景點,藍色的為背景點,外面的綠色框為圖像分割的區域):

一次grabcut迭代后分割的結果:

若干次迭代后分割的結果:

另外,關於本次實驗的操作方法可以參考代碼中函數的help內容,里面有詳細的介紹。
實驗代碼及注釋:
本次實驗代碼來源為opencv自帶的c++ sample。
crabcut.cpp:
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace std; using namespace cv; static void help() { cout << "\nThis program demonstrates GrabCut segmentation -- select an object in a region\n" "and then grabcut will attempt to segment it out.\n" "Call:\n" "./grabcut <image_name>\n" "\nSelect a rectangular area around the object you want to segment\n" << "\nHot keys: \n" "\tESC - quit the program\n" "\tr - restore the original image\n" "\tn - next iteration\n" "\n" "\tleft mouse button - set rectangle\n" "\n" "\tCTRL+left mouse button - set GC_BGD pixels\n" "\tSHIFT+left mouse button - set CG_FGD pixels\n" "\n" "\tCTRL+right mouse button - set GC_PR_BGD pixels\n" "\tSHIFT+right mouse button - set CG_PR_FGD pixels\n" << endl; } const Scalar RED = Scalar(0,0,255); const Scalar PINK = Scalar(230,130,255); const Scalar BLUE = Scalar(255,0,0); const Scalar LIGHTBLUE = Scalar(255,255,160); const Scalar GREEN = Scalar(0,255,0); const int BGD_KEY = CV_EVENT_FLAG_CTRLKEY; //Ctrl鍵 const int FGD_KEY = CV_EVENT_FLAG_SHIFTKEY; //Shift鍵 static void getBinMask( const Mat& comMask, Mat& binMask ) { if( comMask.empty() || comMask.type()!=CV_8UC1 ) CV_Error( CV_StsBadArg, "comMask is empty or has incorrect type (not CV_8UC1)" ); if( binMask.empty() || binMask.rows!=comMask.rows || binMask.cols!=comMask.cols ) binMask.create( comMask.size(), CV_8UC1 ); binMask = comMask & 1; //得到mask的最低位,實際上是只保留確定的或者有可能的前景點當做mask } class GCApplication { public: enum{ NOT_SET = 0, IN_PROCESS = 1, SET = 2 }; static const int radius = 2; static const int thickness = -1; void reset(); void setImageAndWinName( const Mat& _image, const string& _winName ); void showImage() const; void mouseClick( int event, int x, int y, int flags, void* param ); int nextIter(); int getIterCount() const { return iterCount; } private: void setRectInMask(); void setLblsInMask( int flags, Point p, bool isPr ); const string* winName; const Mat* image; Mat mask; Mat bgdModel, fgdModel; uchar rectState, lblsState, prLblsState; bool isInitialized; Rect rect; vector<Point> fgdPxls, bgdPxls, prFgdPxls, prBgdPxls; int iterCount; }; /*給類的變量賦值*/ void GCApplication::reset() { if( !mask.empty() ) mask.setTo(Scalar::all(GC_BGD)); bgdPxls.clear(); fgdPxls.clear(); prBgdPxls.clear(); prFgdPxls.clear(); isInitialized = false; rectState = NOT_SET; //NOT_SET == 0 lblsState = NOT_SET; prLblsState = NOT_SET; iterCount = 0; } /*給類的成員變量賦值而已*/ void GCApplication::setImageAndWinName( const Mat& _image, const string& _winName ) { if( _image.empty() || _winName.empty() ) return; image = &_image; winName = &_winName; mask.create( image->size(), CV_8UC1); reset(); } /*顯示4個點,一個矩形和圖像內容,因為后面的步驟很多地方都要用到這個函數,所以單獨拿出來*/ void GCApplication::showImage() const { if( image->empty() || winName->empty() ) return; Mat res; Mat binMask; if( !isInitialized ) image->copyTo( res ); else { getBinMask( mask, binMask ); image->copyTo( res, binMask ); //按照最低位是0還是1來復制,只保留跟前景有關的圖像,比如說可能的前景,可能的背景 } vector<Point>::const_iterator it; /*下面4句代碼是將選中的4個點用不同的顏色顯示出來*/ for( it = bgdPxls.begin(); it != bgdPxls.end(); ++it ) //迭代器可以看成是一個指針 circle( res, *it, radius, BLUE, thickness ); for( it = fgdPxls.begin(); it != fgdPxls.end(); ++it ) //確定的前景用紅色表示 circle( res, *it, radius, RED, thickness ); for( it = prBgdPxls.begin(); it != prBgdPxls.end(); ++it ) circle( res, *it, radius, LIGHTBLUE, thickness ); for( it = prFgdPxls.begin(); it != prFgdPxls.end(); ++it ) circle( res, *it, radius, PINK, thickness ); /*畫矩形*/ if( rectState == IN_PROCESS || rectState == SET ) rectangle( res, Point( rect.x, rect.y ), Point(rect.x + rect.width, rect.y + rect.height ), GREEN, 2); imshow( *winName, res ); } /*該步驟完成后,mask圖像中rect內部是3,外面全是0*/ void GCApplication::setRectInMask() { assert( !mask.empty() ); mask.setTo( GC_BGD ); //GC_BGD == 0 rect.x = max(0, rect.x); rect.y = max(0, rect.y); rect.width = min(rect.width, image->cols-rect.x); rect.height = min(rect.height, image->rows-rect.y); (mask(rect)).setTo( Scalar(GC_PR_FGD) ); //GC_PR_FGD == 3,矩形內部,為可能的前景點 } void GCApplication::setLblsInMask( int flags, Point p, bool isPr ) { vector<Point> *bpxls, *fpxls; uchar bvalue, fvalue; if( !isPr ) //確定的點 { bpxls = &bgdPxls; fpxls = &fgdPxls; bvalue = GC_BGD; //0 fvalue = GC_FGD; //1 } else //概率點 { bpxls = &prBgdPxls; fpxls = &prFgdPxls; bvalue = GC_PR_BGD; //2 fvalue = GC_PR_FGD; //3 } if( flags & BGD_KEY ) { bpxls->push_back(p); circle( mask, p, radius, bvalue, thickness ); //該點處為2 } if( flags & FGD_KEY ) { fpxls->push_back(p); circle( mask, p, radius, fvalue, thickness ); //該點處為3 } } /*鼠標響應函數,參數flags為CV_EVENT_FLAG的組合*/ void GCApplication::mouseClick( int event, int x, int y, int flags, void* ) { // TODO add bad args check switch( event ) { case CV_EVENT_LBUTTONDOWN: // set rect or GC_BGD(GC_FGD) labels { bool isb = (flags & BGD_KEY) != 0, isf = (flags & FGD_KEY) != 0; if( rectState == NOT_SET && !isb && !isf )//只有左鍵按下時 { rectState = IN_PROCESS; //表示正在畫矩形 rect = Rect( x, y, 1, 1 ); } if ( (isb || isf) && rectState == SET ) //按下了alt鍵或者shift鍵,且畫好了矩形,表示正在畫前景背景點 lblsState = IN_PROCESS; } break; case CV_EVENT_RBUTTONDOWN: // set GC_PR_BGD(GC_PR_FGD) labels { bool isb = (flags & BGD_KEY) != 0, isf = (flags & FGD_KEY) != 0; if ( (isb || isf) && rectState == SET ) //正在畫可能的前景背景點 prLblsState = IN_PROCESS; } break; case CV_EVENT_LBUTTONUP: if( rectState == IN_PROCESS ) { rect = Rect( Point(rect.x, rect.y), Point(x,y) ); //矩形結束 rectState = SET; setRectInMask(); assert( bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty() ); showImage(); } if( lblsState == IN_PROCESS ) //已畫了前后景點 { setLblsInMask(flags, Point(x,y), false); //畫出前景點 lblsState = SET; showImage(); } break; case CV_EVENT_RBUTTONUP: if( prLblsState == IN_PROCESS ) { setLblsInMask(flags, Point(x,y), true); //畫出背景點 prLblsState = SET; showImage(); } break; case CV_EVENT_MOUSEMOVE: if( rectState == IN_PROCESS ) { rect = Rect( Point(rect.x, rect.y), Point(x,y) ); assert( bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty() ); showImage(); //不斷的顯示圖片 } else if( lblsState == IN_PROCESS ) { setLblsInMask(flags, Point(x,y), false); showImage(); } else if( prLblsState == IN_PROCESS ) { setLblsInMask(flags, Point(x,y), true); showImage(); } break; } } /*該函數進行grabcut算法,並且返回算法運行迭代的次數*/ int GCApplication::nextIter() { if( isInitialized ) //使用grab算法進行一次迭代,參數2為mask,里面存的mask位是:矩形內部除掉那些可能是背景或者已經確定是背景后的所有的點,且mask同時也為輸出 //保存的是分割后的前景圖像 grabCut( *image, mask, rect, bgdModel, fgdModel, 1 ); else { if( rectState != SET ) return iterCount; if( lblsState == SET || prLblsState == SET ) grabCut( *image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_MASK ); else grabCut( *image, mask, rect, bgdModel, fgdModel, 1, GC_INIT_WITH_RECT ); isInitialized = true; } iterCount++; bgdPxls.clear(); fgdPxls.clear(); prBgdPxls.clear(); prFgdPxls.clear(); return iterCount; } GCApplication gcapp; static void on_mouse( int event, int x, int y, int flags, void* param ) { gcapp.mouseClick( event, x, y, flags, param ); } int main( int argc, char** argv ) { string filename = "test.jpg"; Mat image = imread( filename, 1 ); if( image.empty() ) { cout << "\n Durn, couldn't read image filename " << filename << endl; return 1; } help(); const string winName = "image"; cvNamedWindow( winName.c_str(), CV_WINDOW_AUTOSIZE ); cvSetMouseCallback( winName.c_str(), on_mouse, 0 ); gcapp.setImageAndWinName( image, winName ); gcapp.showImage(); for(;;) { int c = cvWaitKey(0); switch( (char) c ) { case '\x1b': cout << "Exiting ..." << endl; goto exit_main; case 'r': cout << endl; gcapp.reset(); gcapp.showImage(); break; case 'n': int iterCount = gcapp.getIterCount(); cout << "<" << iterCount << "... "; int newIterCount = gcapp.nextIter(); if( newIterCount > iterCount ) { gcapp.showImage(); cout << iterCount << ">" << endl; } else cout << "rect must be determined>" << endl; break; } } exit_main: cvDestroyWindow( winName.c_str() ); return 0; }
實驗總結:本次實驗測試了下opencv自帶的grabcut算法,其效果還算可以,不過需要人工交互提供分割信息,比較難以接受的是其分割速度太慢,不能實時處理。
參考文獻:
一些知識點的初步理解_8(Graph Cuts,ing...)
