OpenCV(EmguCV)2.1新特性介绍之图像分割GrabCut(GrabCut Of OpenCV 2.1)

作者:王先荣
    前不久OpenCV和EmguCV相继发布了2.1版,增加了一些新的特性,本文关注的是其中的图像分割部分——GrabCut。GrabCut主要用于图像编辑中的抠图,作用跟Photoshop中的魔法棒、套索类似,但是更加强大。由于没有GrabCut的文档,探索具体的用法花费了不少时间和精力,仔细看了论文,大致看了源代码。

 

GrabCut简介
    OpenCV中的GrabCut算法是依据《"GrabCut" - Interactive Foreground Extraction using Iterated Graph Cuts》这篇文章来实现的。该算法利用了图像中的纹理(颜色)信息和边界(反差)信息,只要少量的用户交互操作即可得到比较好的分割结果。如果前景和背景之间的颜色反差不大,分割的效果不好;不过,这种情况下允许手工标记一些前景或背景区域,这样能得到较好的结果。经我测试,GrabCut算法的效率不高,初始化341x326大小的矩形窗大约需要20秒,处理需要9秒;而论文中宣称初始化450x300大小的矩形窗仅0.9秒,处理只要0.12秒;虽然矩形大小和测试环境稍有区别,但是结果却相差太多。

GrabCut函数说明
函数原型:
    void cv::grabCut( const Mat& img, Mat& mask, Rect rect,
             Mat& bgdModel, Mat& fgdModel,
             int iterCount, int mode )
其中:
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),执行分割。

GrabCut的用法
    您可以按以下方式来使用GrabCut函数:
(1)用矩形窗或掩码图像初始化grabCut;
(2)执行分割;
(3)如果对结果不满意,在掩码图像中设定前景和(或)背景,再次执行分割;
(4)使用掩码图像中的前景或背景信息。

从上述图片中可以看出,用更多的迭代次数,或者更多的用户交互都能得到更好的结果。

示例
    下面是一个使用GrabCut进行图像分割的例子,其中用了P/INVOKE形式的CvGrabCut函数,以及封装在Image<TColor,TDepth>类中的GrabCut方法。封装的方法便于使用,但是缺少一些功能,灵活性不足。

 

使用GrabCut
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;

namespace NewFeaturesOfOpenCV2._1
{
public partial class FormGrabCut : Form
{
//常量
private static readonly Bgr Blue = new Bgr(255d, 0d, 0d); //蓝色,用于绘制矩形
private static readonly Bgr Green = new Bgr(0d, 255d, 0d); //绿色,用于绘制前景曲线
private static readonly Bgr Red = new Bgr(0d, 0d, 255d); //红色,用于绘制背景曲线
private const int LineWidth = 5; //绘制线条的宽度
private const int GC_BGD = 0; //背景标志
private const int GC_FGD = 1; //前景标志
private const int GC_PR_BGD = 2; //可能的背景标志
private const int GC_PR_FGD = 3; //可能的前景标志
//成员变量
private string sourceImageFileName = "wky_tms_2272x1704.jpg";//源图像文件名
private Image<Bgr, Byte> imageSource = null; //源图像
private Image<Bgr, Byte> imageSourceClone = null; //源图像的克隆
private Image<Gray, Byte> imageMask = null; //掩码图像:保存初始化之后的掩码信息及用户绘制的信息
private Matrix<Single> foregroundModel = null; //前景模型
private Matrix<Single> backgroundModel = null; //背景模型
private double xScale = 1d; //原始图像与PictureBox在x轴方向上的缩放
private double yScale = 1d; //原始图像与PictureBox在y轴方向上的缩放
private Point previousMouseLocation = new Point(-1, -1); //上次绘制线条时,鼠标所处的位置
private Rectangle rect; //初始化矩形窗口
private bool initialized = false; //是否已经初始化过GrabCut

public FormGrabCut()
{
InitializeComponent();
}

//加载窗体时
private void FormGrabCut_Load(object sender, EventArgs e)
{
//设置提示
toolTip.SetToolTip(rbRect, "使用鼠标在源图像绘制矩形窗口,在图像分割之前使用矩形窗口所在的区域进行初始化。");
toolTip.SetToolTip(rbMask,
"使用鼠标在源图像绘制掩码,左键绘制前景掩码,邮件绘制背景掩码,在图像分割之前使用掩码图像进行初始化。");
//初始化前景模型和背景模型
foregroundModel = new Matrix<float>(1, 13 * 5);
backgroundModel
= new Matrix<float>(1, 13 * 5);
//加载默认图像
LoadImage();
}

//关闭窗体前,释放资源
private void FormGrabCut_FormClosing(object sender, FormClosingEventArgs e)
{
if (imageSource != null)
imageSource.Dispose();
if (imageSourceClone != null)
imageSourceClone.Dispose();
if (imageMask != null)
imageMask.Dispose();
if (foregroundModel != null)
foregroundModel.Dispose();
if (backgroundModel != null)
backgroundModel.Dispose();
}

//加载源图像
private void btnLoadImage_Click(object sender, EventArgs e)
{
OpenFileDialog ofd
= new OpenFileDialog();
ofd.CheckFileExists
= true;
ofd.DefaultExt
= "jpg";
ofd.Filter
= "图片文件|*.jpg;*.png;*.bmp|所有文件|*.*";
if (ofd.ShowDialog(this) == DialogResult.OK)
{
if (ofd.FileName != "")
{
sourceImageFileName
= ofd.FileName;
LoadImage();
}
}
ofd.Dispose();
}

//重新加载图像
private void btnReload_Click(object sender, EventArgs e)
{
LoadImage();
}

//加载源图像
private void LoadImage()
{
if (imageSource != null)
imageSource.Dispose();
imageSource
= new Image<Bgr, byte>(sourceImageFileName);
if (imageSourceClone != null)
imageSourceClone.Dispose();
imageSourceClone
= imageSource.Copy();
pbSource.Image
= imageSourceClone.Bitmap;
if (imageMask != null)
imageMask.Dispose();
imageMask
= new Image<Gray, byte>(imageSource.Size);
imageMask.SetZero();
xScale
= 1d * imageSource.Width / pbSource.Width;
yScale
= 1d * imageSource.Height / pbSource.Height;
rect
= new Rectangle(-1, -1, 1, 1);
initialized
= false;
}

//鼠标在源图像上按下时
private void pbSource_MouseDown(object sender, MouseEventArgs e)
{
if (rbRect.Checked)
rect
= new Rectangle((int)(e.X * xScale), (int)(e.Y * yScale), 1, 1);
else
previousMouseLocation
= new Point((int)(e.X * xScale), (int)(e.Y * yScale));
}

//鼠标在源图像上移动时
private void pbSource_MouseMove(object sender, MouseEventArgs e)
{
//绘制矩形
if (rbRect.Checked && e.Button != MouseButtons.None)
{
rect
= new Rectangle(rect.Left, rect.Top, (int)(e.X * xScale - rect.Left), (int)(e.Y * yScale - rect.Top));
imageSourceClone.Dispose();
imageSourceClone
= imageSource.Clone();
imageSourceClone.Draw(rect, Blue, LineWidth);
pbSource.Image
= imageSourceClone.Bitmap;
return;
}
//绘制线条,用于手工标记前景或者背景
if (rbMask.Checked && (e.Button == MouseButtons.Left || e.Button == MouseButtons.Right))
{
if (previousMouseLocation.X == -1 && previousMouseLocation.Y == -1)
{
previousMouseLocation.X
= (int)(e.X * xScale);
previousMouseLocation.Y
= (int)(e.Y * yScale);
}
else
{
LineSegment2D line
= new LineSegment2D(previousMouseLocation, new Point((int)(e.X * xScale), (int)(e.Y * yScale)));
if (e.Button == MouseButtons.Left)
{
imageMask.Draw(line,
new Gray((double)GC_FGD), LineWidth);
imageSourceClone.Draw(line, Green, LineWidth);
}
else
{
imageMask.Draw(line,
new Gray((double)GC_BGD), LineWidth);
imageSourceClone.Draw(line, Red, LineWidth);
}
pbSource.Image
= imageSourceClone.Bitmap;
previousMouseLocation
= line.P2;
}

}
}

//鼠标在源图像上松开时
private void pbSource_MouseUp(object sender, MouseEventArgs e)
{
if (rbRect.Checked && e.Button != MouseButtons.None)
{
rect
= new Rectangle(rect.Left, rect.Top, (int)(e.X * xScale - rect.Left), (int)(e.Y * yScale - rect.Top));
imageSourceClone.Dispose();
imageSourceClone
= imageSource.Clone();
imageSourceClone.Draw(rect, Blue, LineWidth);
pbSource.Image
= imageSourceClone.Bitmap;
//绘制矩形结束之后,初始化掩码图像
imageMask.SetZero();
imageMask.Draw(rect,
new Gray((double)GC_PR_FGD), 0);
return;
}
if (rbMask.Checked)
previousMouseLocation
= new Point(-1, -1);
}

//开始图像分割
private void btnStartSegment_Click(object sender, EventArgs e)
{
if (rect != new Rectangle(-1, -1, 1, 1)) //必须指定矩形窗
{
Stopwatch sw
= new Stopwatch();
Image
<Gray, Byte> mask = null;
if (rbRect.Checked)
{
//用矩形窗初始化
sw.Reset();
sw.Start();
mask
= imageSource.GrabCut(rect, (int)nudIterCount.Value); //注:Image.GrabCut等价于先用矩形初始化CvGrabCut(....,GRABCUT_INIT_TYPE.INIT_WITH_RECT),然后再计算CvGrabCut(....,GRABCUT_INIT_TYPE.INIT_WITH_EVAL)
sw.Stop();
imageMask
= mask.Clone();
initialized
= true;
ShowResult(
"用矩形窗初始化GrabCut并计算", sw.ElapsedMilliseconds);
}
else
{
//用掩码初始化
mask = imageMask.Clone();
if (!initialized)
{
sw.Reset();
sw.Start();
CvInvoke.CvGrabCut(imageSource.Ptr, mask.Ptr,
ref rect, backgroundModel.Ptr, foregroundModel.Ptr, 1, GRABCUT_INIT_TYPE.INIT_WITH_MASK);
sw.Stop();
initialized
= true;
ShowResult(
"用掩码初始化GrabCut", sw.ElapsedMilliseconds);
}
sw.Reset();
sw.Start();
CvInvoke.CvGrabCut(imageSource.Ptr, mask.Ptr,
ref rect, backgroundModel.Ptr, foregroundModel.Ptr, (int)nudIterCount.Value, GRABCUT_INIT_TYPE.EVAL);
sw.Stop();
ShowResult(
"计算GrabCut", sw.ElapsedMilliseconds);
}
CvInvoke.cvAndS(mask.Ptr,
new MCvScalar(1d), mask.Ptr, IntPtr.Zero); //将掩码图像和1进行按位“与”操作,这样背景及可能的背景将变为0;而前景及可能的前景将变成1
Image<Bgr, Byte> result = imageSource.Copy(mask);
pbResult.Image
= result.Bitmap;
mask.Dispose();
//result.Dispose();
}
else
MessageBox.Show(
this, "在开始分割之前,请在源图像上绘制一个矩形窗口。", "缺少矩形窗", MessageBoxButtons.OK, MessageBoxIcon.Information);
}

/// <summary>
/// 显示结果
/// </summary>
/// <param name="prompt">提示</param>
/// <param name="elapsedMilliseconds">耗时</param>
private void ShowResult(string prompt, double elapsedMilliseconds)
{
txtResult.Text
+= string.Format("{0},耗时:{1:F04}毫秒,参数(矩形窗起点:{2},大小:{3}X{4},迭代次数:{5})。\r\n",
prompt, elapsedMilliseconds, rect.Location, rect.Width, rect.Height, nudIterCount.Value);
}
}
}

点击这里下载本文源代码

 

 

感谢您耐心看完本文,希望对您有所帮助。
欢迎转载,但是请注明出处,保留作者。

posted @ 2010-04-27 15:00 Wuya 阅读(9559) 评论(36) 编辑 收藏

 回复 引用 查看   
#1楼 2010-04-27 15:04 young40      
不错。。。。。。。。。。
 回复 引用 查看   
#2楼[楼主] 2010-04-27 15:30 Wuya      
@young40
感谢支持~~

 回复 引用 查看   
#3楼 2010-04-27 16:20 紫色永恒      
国内研究opencv的太少了
 回复 引用 查看   
#4楼[楼主] 2010-04-27 16:30 Wuya      
@紫色永恒
国内其实有很多人在搞OpenCV,大本营在www.opencv.org.cn。
不过我基本上用C#来搞,所以没有入C/C++的队伍。
很多牛人都不屑于写博客而已。

 回复 引用 查看   
#5楼 2010-04-27 16:31 紫色永恒      
@Wuya
是啊,我前一阵经常去www.opencv.org.cn看看(感觉论坛不是很火),c#这块的介绍少之又少。博主搞了多久了?

 回复 引用 查看   
#6楼[楼主] 2010-04-27 16:48 Wuya      
@紫色永恒
断断续续学了快半年了,搞这个的原因有二:
(1)以前经常接触的基本都是数据库增删改查及相应的界面,想换个东西搞搞;
(2)完成导师安排的论文,结果一进来发现这里的水太深,非朝夕之功。

我进度慢的主要原因是英文不好,而图像处理这块好点的资料基本是英文的;中文资料要么难以找到,即使找到了很多也是语焉不详,更别说提供源代码之类的了。还有就是数学没学好,看到公式就头晕。

 回复 引用 查看   
#7楼 2010-04-27 21:22 长空无忌      
终于更新了,每次上emgu都老样子
 回复 引用 查看   
#8楼[楼主] 2010-04-27 23:21 Wuya      
@长空无忌
emgu的更新速度也还蛮快的,
作者对bug的反应速度及修复都还好,
只是用的人好像不多。

 回复 引用 查看   
#9楼 2010-05-06 00:01 zlalex      
一直都在用aforge,看了您的博客后也准备学习下emguCV,肯定有很多不懂的地方,到时候再向您请教。
另外,关于评价各种图像处理类库的那篇文章,我大致看了下,感觉Apply方法最好还是直接用unmanegedImage那个参数的重载适合一些,直接用bitmap不是太好,这个也是作者的建议。不过我没有测试,也不知道是不是这样。

 回复 引用 查看   
#10楼[楼主] 2010-05-09 19:58 Wuya      
@zlalex
哈,没注意到还有个重载的方法。
下次有空试试看。

 回复 引用 查看   
#11楼[楼主] 2010-05-10 08:53 Wuya      
@zlalex
已经使用AForge中的UnmanagedImage类进行了测试,测试结果见《各种图像处理类库的比较及选择(The Comparison of Image Processing Libraries)》的评论。

 回复 引用 查看   
#12楼 2010-07-17 22:28 computervision      
请问博主用cvMat* 给Mat& 赋值为什么不可以? mask用单通道矩阵可以吗? 谢谢!
 回复 引用 查看   
#13楼[楼主] 2010-07-19 16:07 Wuya      
@computervision
(1)刚才在MSDN中的看了&运算符的用法,建议您也去看看主题为《Lvalue Reference Declarator: &》的章节。
(2)Mat是OpenCv中的C++类,而CvMat是C风格的结构,您可以去看看其中的转换方式。
(3)mask需要用单通道的矩阵。

 回复 引用 查看   
#14楼 2010-07-19 17:17 computervision      
谢谢
 回复 引用 查看   
#15楼 2010-07-19 17:17 computervision      
@Wuya
谢谢

 回复 引用 查看   
#16楼 2010-07-23 14:56 borrows      
你也是图像视频方向啊?
 回复 引用 查看   
#17楼 2010-07-23 14:56 borrows      
你说的EmguCV是什么东西啊?
 回复 引用 查看   
#18楼[楼主] 2010-07-23 16:49 Wuya      
@borrows
(1)在这个系列文章的第一篇中介绍了EmguCV;简而言之,EmguCV是将OpenCV封装成方便在.net环境使用的图像处理类库。
(2)我在空闲时间看看这方面的东西。

 回复 引用 查看   
#19楼 2010-10-25 19:51 掌印      
楼主,我也在做这方面的东东,能否交流一下? QQ:1134703754
 回复 引用 查看   
#20楼[楼主] 2010-10-26 08:06 Wuya      
现在几乎不用QQ了,我们可以就在这里聊。
 回复 引用 查看   
#21楼 2010-12-07 11:00 neroliu      
mask——掩码图像是指的什么,具体是有什么作用,可以介绍下啊,详细点最好了,谢谢
 回复 引用 查看   
#22楼[楼主] 2010-12-17 10:17 Wuya      
@neroliu
引用neroliu:mask——掩码图像是指的什么,具体是有什么作用,可以介绍下啊,详细点最好了,谢谢

掩码图像一般用于标明某个像素。
例如:
像素是否为前景;
某像素是否参与运算;
等等。。。。。

 回复 引用 查看   
#23楼 2011-01-28 19:09 niit234      
今天看了楼主的文章,感觉受益匪浅,同时对楼主的这种深入的研究的精神感到佩服,最近正在研究C#与Opencv结合进行图像处理,楼主这个例子是个很好的值得深入学习的例子,楼主能否不吝把完整代码(下载的代码中只有cs文件,无form及工程文件,不好进行实践深入学习)发给我,本人邮箱429106990@qq.com ,感激不尽,还望楼主以后多多赐教...
 回复 引用 查看   
#24楼[楼主] 2011-01-28 21:14 Wuya      
@niit234
有一篇专门的文章,放了全部代码,您可以去下载。

 回复 引用 查看   
#25楼 2011-06-09 20:38 喷漆HOT      
楼主 你好 !我对代码重生成成功 但在debug时,image.cs中的一条语句(Bitmap bmp = new Bitmap(file.FullName)) 提示参数无效 ,未处理ArgumentException, 只是怎么回事啊 ! 如何能够解决呢? 期待你的答复 多谢了?
 回复 引用 查看   
#26楼[楼主] 2011-06-10 08:34 Wuya      
@喷漆HOT
请检查file.FullName是否为空,或者指向了不存在的文件。
这些代码不实际使用,很多需要判断的细节都未添加,抱歉。

 回复 引用 查看   
#27楼 2011-08-12 08:25 苹果苹果大苹果      
只有opencv2.1才有grabcut是吗
opencv2.1在windows上是只有在vs2008下安装吗

 回复 引用 查看   
#28楼[楼主] 2011-08-15 07:54 Wuya      
@苹果苹果大苹果
1. 尽管我没用过OpenCV2.2和2.3,但是按照程序库的一贯风格,新功能在添加之后,一般是不会删除掉的,因此我估计在OpenCV里比2.1更新的版本中都有grabcut。
2. OpenCV的安装跟有没有Visual Studio没关系。不过在Visual Studio里编程是件很方便的事情。

 回复 引用 查看   
#29楼 2011-08-16 19:10 苹果苹果大苹果      
但是opencv china那个网站上写的是 :VC 2008 Express下安装OpenCV2.0/2.1,是不是意味着opencv2.0/2.1只与vc2008 express适用?比如我现在用的vc++ 6.0就没法用了?我现在用的是opencv1.0的,但是之后的学习需要grabcut,貌似非得装2.1版本的了。其实也搜索到了grabcut的之前的graphcut的yuri大牛写的程序,但是不知道怎么应用到图像处理中,只好去找个直接可以调用的。
新手,问的问题很傻。。。不好意思、、、、

 回复 引用 查看   
#30楼[楼主] 2011-08-17 08:14 Wuya      
我很长时间没用VC6了,不知道是否能在VC6里面使用OPENCV2.0和2.1,您可以到www.opencv.org.cn看看是否有直接使用的方法。
不过变通的方法我倒是有的,您用LoadLibrary加载包含grabcut的dll,然后找到grabcut的地址,使用该API,最后释放dll。

 回复 引用 查看   
#31楼 2011-09-29 16:55 ckxdw      
@Wuya
博主您好,请问您知道在哪里把GrabCut分割的背景色换成别的颜色?默认是黑色的,我现在需要背景色是其他颜色的。

 回复 引用 查看   
#32楼[楼主] 2011-09-30 08:09 Wuya      
@ckxdw
您将btnStartSegment_Click方法中的下列两行语句稍做修改即可:
CvInvoke.cvAndS(mask.Ptr, new MCvScalar(1d), mask.Ptr, IntPtr.Zero); //将掩码图像和1进行按位“与”操作,这样背景及可能的背景将变为0;而前景及可能的前景将变成1
Image<Bgr, Byte> result = imageSource.Copy(mask);

方法如下:
//创建一个保存结果的图像,尺寸跟掩码图像相同
//分别遍历掩码图像、源图像和结果图像的每个像素
//如果掩码图像的像素值是0,则为背景,结果图像填充您想要的颜色;
//如果掩码图像的像素值是1,则为前景,结果图像填充源图像的像素值。

 回复 引用 查看   
#33楼 2011-09-30 08:48 ckxdw      
@Wuya
非常感谢!!

 回复 引用 查看   
#34楼[楼主] 2011-10-01 08:05 Wuya      
@ckxdw
^_^

 回复 引用 查看   
#35楼 2012-02-10 00:17 zkj3e      
王老师,我要grabcut函数时,出现如下错误:
断言:dtrm > std::numeric_limits<double>::epsilon()
如何处理啊?

 回复 引用 查看   
#36楼 2012-02-13 19:43 zkj3e      
博主,grabcut的这个错误怎么处理:

OpenCV Error: Assertion failed (dtrm > std::numeric_limits<double>::epsilon()) in calcInverseCovAndDeterm, file /home/zengkejie/OpenCV-2.3.0/modules/imgproc/src/grabcut.cpp, line 216
terminate called after throwing an instance of 'cv::Exception'
what(): /home/zengkejie/OpenCV-2.3.0/modules/imgproc/src/grabcut.cpp:216: error: (-215) dtrm > std::numeric_limits<double>::epsilon() in function calcInverseCovAndDeterm