【图像处理】OpenCV系列二十六 --- 图像分割(grabC
上一节我们学习了距离变换算法,相信大家学习之后,对距离变换算法已经有了基本的认识,本节呢,我们对学习图像分割,对grabCut函数详解!
一、函数详解
1、函数原型
void grabCut(InputArray img,
InputOutputArray mask,
Rect rect,
InputOutputArray bgdModel,
InputOutputArray fgdModel,
int iterCount,
int mode = GC_EVAL)
2、函数功能
主要实现对图像背景的分割!
3、参数详解
-
第一个参数,InputArray img,输入图像,一个8位3通道的图像;
-
第二个参数,InputOutputArray mask,输入/输出一个8位单通道的掩码图像;当函数的参数mode设置为GC_INIT_WITH_RECT,掩码由函数进行初始化;它的元素可能是GrabCutClasses中的一个;
掩码的类型
(1) GC_BGD,表示是背景;
(2) GC_FGD,表示是前景;
(3) GC_PR_BGD, 表示可能是背景;
(4) GC_PR_FGD, 表示可能是前景;
-
第三个参数,Rect rect,要分割对象的ROI,ROI外部的像素被标记为“明显的背景”;只有当函数的参数mode设置为GC_INIT_WITH_RECT时,才使用该参数;
-
第四个参数,InputOutputArray bgdModel,背景模型的临时数组;在处理同一图像时,不要修改它;
-
第五个参数,InputOutputArray fgdModel,前景模型的临时数组;在处理同一图像时,不要修改它;
-
第六个参数,int iterCount,在返回结果之前,算法应该进行的迭代次数;
Note:
可以将函数中的参数mode设置为GC_INIT_WITH_MASK或者GC_EVAL进一步调用来细化结果;
- 第七个参数,int mode = GC_EVAL,操作的模式;
操作的模式有以下几种
(1) GC_INIT_WITH_RECT,函数使用提供的矩形来初始化状态和掩码;之后,它运行算法iterCount次迭代。
(2) GC_INIT_WITH_MASK,函数使用提供的掩码初始化状态;
Note:
可以组合使用GC_INIT_WITH_RECT和GC_INIT_WITH_MASK;
(3) GC_EVAL,该值意味着该算法只需恢复;
(4) GC_EVAL_FREEZE_MODEL,该值意味着该算法应该只运行具有固定模型的GrabCut算法(一次迭代);
二、综合实例
1、实验案例
实现对人物的图像进行抠图
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <iostream>
using namespace std;
using namespace cv;
static void help()
{
cout << "\n在图像中选择一块区域,然后用grabCut尝试将其分割出来.\n"
"\n在图像中选择一块你要分割的矩形区域\n" <<
"\nHot keys: \n"
"\tESC - 退出程序\n"
"\tr - 回复原始图像\n"
"\tn - 下一次迭代\n"
"\n"
"\t左键移动,设置要分割的矩形\n"
"\n"
"\tCTRL+left 鼠标按键 - 设置 GC_BGD 背景\n"
"\tSHIFT+left 鼠标按键 - 设置 GC_FGD 前景\n"
"\n"
"\tCTRL+right 鼠标按键 - 设置 GC_PR_BGD 可能是背景\n"
"\tSHIFT+right 鼠标按键 - 设置 GC_PR_FGD 可能是前景\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);
// 背景模型按键 Ctrl键
const int BGD_KEY = EVENT_FLAG_CTRLKEY;
// 前景模型按键 shift键
const int FGD_KEY = EVENT_FLAG_SHIFTKEY;
// 获取二值化的掩码
static void getBinMask( const Mat& comMask, Mat& binMask )
{
if( comMask.empty() || comMask.type()!=CV_8UC1 )
CV_Error( Error::StsBadArg,
"comMask is empty or has incorrect type (not CV_8UC1)" );
// 进行判断,如果为false,则重新创建binMask掩码
if( binMask.empty() ||
binMask.rows!=comMask.rows
|| binMask.cols!=comMask.cols )
binMask.create( comMask.size(), CV_8UC1 );
binMask = comMask & 1; // 所有的像素&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:
// 在掩码中设置Rect
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();
// 初始化设置为false
isInitialized = false;
// 矩形,左键,右键三个状态设置为NOT_SET
rectState = NOT_SET;
lblsState = NOT_SET;
prLblsState = NOT_SET;
// 迭代次数设置为0
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();
}
// 显示图像
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 );
}
// 设置要分割的Rect
void GCApplication::setRectInMask()
{
CV_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;
}
// 背景+Ctrl键
if( flags & BGD_KEY )
{
// 点集压栈
bpxls->push_back(p);
// 绘制圆
circle( mask, p, radius, bvalue, thickness );
}
// 前景+shift键
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* )
{
switch( event )
{
// set rect or GC_BGD(GC_FGD) labels 左键按下
case EVENT_LBUTTONDOWN:
{
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 )
// 标记左键正在占用
lblsState = IN_PROCESS;
}
break;
// set GC_PR_BGD(GC_PR_FGD) labels 右键按下
case EVENT_RBUTTONDOWN:
{
bool isb = (flags & BGD_KEY) != 0,
isf = (flags & FGD_KEY) != 0;
if ( (isb || isf) && rectState == SET )
// 标记右键正在被占用
prLblsState = IN_PROCESS;
}
break;
// 左键松起
case EVENT_LBUTTONUP:
// 如果绘制矩形状态被占用
if( rectState == IN_PROCESS )
{
// 绘制的矩形
rect = Rect( Point(rect.x, rect.y), Point(x,y) );
// 绘制矩形的状态标注为设置
rectState = SET;
// 设置掩码
setRectInMask();
// 对一些参数进行判断
CV_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 EVENT_RBUTTONUP:
// 如果右键正在被占用
if( prLblsState == IN_PROCESS )
{
// 键盘加鼠标按下,画圆
setLblsInMask(flags, Point(x,y), true);
// 右键状态改为设置
prLblsState = SET;
// 显示图像
showImage();
}
break;
// 鼠标移动
case EVENT_MOUSEMOVE:
// 如果绘制矩形的状态正在被占用
if( rectState == IN_PROCESS )
{
// 实时刷新矩形的大小
rect = Rect( Point(rect.x, rect.y), Point(x,y) );
// 对点集的参数进行判断
CV_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;
}
}
// 下一次迭代
int GCApplication::nextIter()
{
// 如果已经初始化,直接进行分割图像
if( isInitialized )
// 分割图像
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;
}
// 迭代次数加1
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 );
}
// mian函数
int main( int argc, char** argv )
{
// 辅助信息
help();
// 载入图像
Mat image = imread("lena.png", IMREAD_COLOR);
// 判断图像是否为空
if( image.empty() )
{
cout << "\n image error!" << endl;
return -1;
}
const string winName = "image";
namedWindow( winName, WINDOW_AUTOSIZE );
// 设置鼠标调用事件
setMouseCallback( winName, on_mouse, 0 );
// 设置窗口名称
gcapp.setImageAndWinName( image, winName );
// 显示图像
gcapp.showImage();
for(;;)
{
// 从键盘接收输入
char c = (char)waitKey(0);
switch( 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:
destroyWindow( winName );
return 0;
}
2、实验结果
原图 抠图的结果我是奕双,现在已经毕业将近两年了,从大学开始学编程,期间学习了C语言编程,C++语言编程,Win32编程,MFC编程,毕业之后进入一家图像处理相关领域的公司,掌握了用OpenCV对图像进行处理,如果大家对相关领域感兴趣的话,可以关注我,我这边会为大家进行解答哦!如果大家需要相关学习资料的话,可以私聊我哦!