作者:王先荣
前不久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方法。封装的方法便于使用,但是缺少一些功能,灵活性不足。
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);
}
}
}