由于最近都有很多课,所以就好久没有写博客,今天抽空写一篇,不能落下太多了,要坚持有时间就写写博客,为自己留下一些回忆,也可以到后期忘记的时候及时复习,不积跬步,无以至千里。不积小流,无以成江海。脚踏实地,打好基础,那才是王道,感觉说了一大堆废话,哈哈,现在正式进入主题,今天要讲的内容就是GrabCut图像分割啦,关于图像分割的方法五花八门,非常多,大家都可以从网上找到,包括GrabCut也是,本人也是在网上学习后在这里做一下记录,顺便分析一下OpenCV自带grabCut函数的使用,和例子的分析,没有什么创新点,方便以后查阅。
原理在这几篇博客里已经讲得很仔细了,涉及到的内容也比较多,大家可以查阅一下,它是一个系列来的
①、图像分割之(一)概述
②、图像分割之(二)Graph Cut(图割)
③、图像分割之(三)从Graph Cut到Grab Cut
④、图像分割之(四)OpenCV的GrabCut函数使用和源码解读
总的来说,GrabCut算法时Graph Cut算法的改进,主要有以下几点的改进
①、Graph Cut的目标和背景的模型是灰度直方图,Grab Cut取代为RGB三通道的混合高斯模型GMM;
②、Graph Cut的能量最小化(分割)是一次达到的,而Grab Cut取代为一个不断进行分割估计和模型参数学习的交互迭代过程;
③、Grab Cut允许不完全的标注,Graph Cut需要用户指定目标和背景的一些种子点,但是Grab Cut只需要提供背景区域的像素集,最后如果需要得到更精确的分割,可以在初次分割的结果上加上一些确定的种子点,再运行算法。
GCApplication.h
#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace std; using namespace cv; 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被按下时,flags返回的值 const int FGD_KEY = CV_EVENT_FLAG_SHIFTKEY;//当SHIFT被按下时,flags返回的值 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; } 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; //rectState, lblsState, prLblsState三个变量分别表示矩形标记的状态, //鼠标左键标记的状态,鼠标右键标记的状态,分别有三个状态:NOT_SET(未处理) //IN_PROCESS(处理)、SET(已处理) uchar rectState, lblsState, prLblsState; bool isInitialized; Rect rect; //在第一次矩形分割后,第二次标记mask值时,四种值出现的点都分别保存在 //fgdPxls, bgdPxls, prFgdPxls, prBgdPxls四个变量中 vector<Point> fgdPxls, bgdPxls, prFgdPxls, prBgdPxls; //迭代的次数 int iterCount; };
#include "GCApplication.h" //初始化掩码图和各变量 //mask图GrabCut函数中对应第二个参数,标记图片中哪些属于前景,哪些属于背景 //mask图只可存入四种数值,分别为:GC_BGD、GC_FGD、GC_PR_BGD、GC_PR_FGD //mask初始化为背景,即赋值为GC_BGD 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; lblsState = NOT_SET; prLblsState = NOT_SET; iterCount = 0; } //初始化窗口和图片 //将读取的图片和窗口名存入类中的私有变量image和winName中,有利于存储 //初始化掩码图以及各变量 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(); } //显示图片 //如果 fgdPxls, bgdPxls, prFgdPxls, prBgdPxls变量非空,则在图片中显示标记的点 //如果 rectState 已经表示被标记,则也在图片中显示标记的矩形 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 ); } vector<Point>::const_iterator it; 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 void GCApplication::setRectInMask() { assert( !mask.empty() ); mask.setTo( GC_BGD ); 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) ); } 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; fvalue = GC_FGD; } //否则,运行以下代码 else { bpxls = &prBgdPxls; fpxls = &prFgdPxls; bvalue = GC_PR_BGD; fvalue = GC_PR_FGD; } //判断是shift键被按下或者ctrl键被按下,分别执行操作 if( flags & BGD_KEY ) { bpxls->push_back(p); circle( mask, p, radius, bvalue, thickness ); } if( flags & FGD_KEY ) { fpxls->push_back(p); circle( mask, p, radius, fvalue, thickness ); } } //鼠标响应 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; //如果rectState为NOT_SET并且ctrl或者shift没被按下,则运行以下代码,设置矩形框 if( rectState == NOT_SET && !isb && !isf ) { rectState = IN_PROCESS; rect = Rect( x, y, 1, 1 ); } //如果rectState为SET,并且ctrl或者shift被按下,则运行以下代码,标记GC_BGD(GC_FGD) if ( (isb || isf) && rectState == SET ) lblsState = IN_PROCESS; } break; case CV_EVENT_RBUTTONDOWN: // set GC_PR_BGD(GC_PR_FGD) labels { //如果rectState为SET,并且ctrl或者shift被按下时,标记GC_PR_BGD(GC_PR_FGD) bool isb = (flags & BGD_KEY) != 0, isf = (flags & FGD_KEY) != 0; if ( (isb || isf) && rectState == SET ) prLblsState = IN_PROCESS; } break; case CV_EVENT_LBUTTONUP: //如果rectState为IN_PROCESS,则确定鼠标走过的整个矩形,并且通过矩形设置Mask 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(); } //如果lblsState为IN_PROCESS,则通过圆圈标记Mask if( lblsState == IN_PROCESS ) { setLblsInMask(flags, Point(x,y), false); lblsState = SET; showImage(); } break; case CV_EVENT_RBUTTONUP: //如果prLblsState为IN_PROCESS,则通过圆圈标记Mask if( prLblsState == IN_PROCESS ) { setLblsInMask(flags, Point(x,y), true); prLblsState = SET; showImage(); } break; case CV_EVENT_MOUSEMOVE: //如果rectState为IN_PROCESS,则鼠标移动时生成矩形 if( rectState == IN_PROCESS ) { rect = Rect( Point(rect.x, rect.y), Point(x,y) ); assert( bgdPxls.empty() && fgdPxls.empty() && prBgdPxls.empty() && prFgdPxls.empty() ); showImage(); }//如果lblsState为IN_PROCESS,则鼠标移动时用圆圈标记Mask else if( lblsState == IN_PROCESS ) { setLblsInMask(flags, Point(x,y), false); showImage(); }//如果prLblsState为IN_PROCESS,则鼠标移动时用圆圈标记Mask else if( prLblsState == IN_PROCESS ) { setLblsInMask(flags, Point(x,y), true); showImage(); } break; } } //如果lblsState或者prLblsState被设置为SET,则说明图片已经被鼠标标记处前景和背景, //而且已经经过矩形处理过一次了,则执行grabCut的GC_INIT_WITH_MASK形式,否则,执行 //GC_INIT_WITH_RECT形式,清除bgdPxls等变量标记,方便下次标记 int GCApplication::nextIter() { if( isInitialized ) grabCut( *image, mask, rect, bgdModel, fgdModel, 3 ); else { if( rectState != SET ) return iterCount; if( lblsState == SET || prLblsState == SET ) grabCut( *image, mask, rect, bgdModel, fgdModel, 3, GC_INIT_WITH_MASK ); else grabCut( *image, mask, rect, bgdModel, fgdModel, 3, GC_INIT_WITH_RECT ); isInitialized = true; } iterCount++; bgdPxls.clear(); fgdPxls.clear(); prBgdPxls.clear(); prFgdPxls.clear(); return iterCount; }
#include "GCApplication.h" 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; } 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 ="shark.jpg"; if( filename.empty() ) { cout << "\nDurn, couldn't read any file."<< endl; return 1; } 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"; namedWindow( winName, WINDOW_AUTOSIZE ); //设置鼠标响应函数 setMouseCallback( winName, on_mouse, 0 ); //初始化窗口和图片 gcapp.setImageAndWinName( image, winName ); gcapp.showImage(); for(;;) { int c = waitKey(0); switch( (char) c ) { //ESC按键退出 case '\x1b': cout << "Exiting ..." << endl; goto exit_main; //r按键重置图像 case 'r': cout << endl; gcapp.reset(); gcapp.showImage(); break; //n按键进行一次处理 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: destroyWindow( winName ); return 0; }
图1、原始图像
图2、第一次运行后图像
图3、第二次标记后图像
图4、第二次标记后运行结果(基本已完成)
void grabCut(InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode=GC_EVAL )
rect:用于限定需要进行分割的图像范围,只有该矩形窗口内的图像才被认为可能是前景GCD_PR_FGD,矩形外的被认为是背景GCD_BGD。
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),执行分割。