http://www.oschina.net/question/54100_36518
作者:王先荣
前不久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方法。封装的方法便于使用,但是缺少一些功能,灵活性不足。
002 | using System.Collections.Generic; |
003 | using System.ComponentModel; |
008 | using System.Windows.Forms; |
009 | using System.Diagnostics; |
010 | using System.Runtime.InteropServices; |
013 | using Emgu.CV.Structure; |
015 | namespace NewFeaturesOfOpenCV2._1 |
017 | public partial class FormGrabCut : Form |
020 | private static readonly Bgr Blue = new Bgr(255d, 0d, 0d); |
021 | private static readonly Bgr Green = new Bgr(0d, 255d, 0d); |
022 | private static readonly Bgr Red = new Bgr(0d, 0d, 255d); |
023 | private const int LineWidth = 5; |
024 | private const int GC_BGD = 0; |
025 | private const int GC_FGD = 1; |
026 | private const int GC_PR_BGD = 2; |
027 | private const int GC_PR_FGD = 3; |
029 | private string sourceImageFileName = "wky_tms_2272x1704.jpg" ; |
030 | private Image<Bgr, Byte> imageSource = null; |
031 | private Image<Bgr, Byte> imageSourceClone = null; |
032 | private Image<Gray, Byte> imageMask = null; |
033 | private Matrix<Single> foregroundModel = null; |
034 | private Matrix<Single> backgroundModel = null; |
035 | private double xScale = 1d; |
036 | private double yScale = 1d; |
037 | private Point previousMouseLocation = new Point(-1, -1); |
038 | private Rectangle rect; |
039 | private bool initialized = false ; |
043 | InitializeComponent(); |
047 | private void FormGrabCut_Load(object sender, EventArgs e) |
050 | toolTip.SetToolTip(rbRect, "使用鼠标在源图像绘制矩形窗口,在图像分割之前使用矩形窗口所在的区域进行初始化。" ); |
051 | toolTip.SetToolTip(rbMask, "使用鼠标在源图像绘制掩码,左键绘制前景掩码,邮件绘制背景掩码,在图像分割之前使用掩码图像进行初始化。" ); |
053 | foregroundModel = new Matrix< float >(1, 13 * 5); |
054 | backgroundModel = new Matrix< float >(1, 13 * 5); |
060 | private void FormGrabCut_FormClosing(object sender, FormClosingEventArgs e) |
062 | if (imageSource != null) |
063 | imageSource.Dispose(); |
064 | if (imageSourceClone != null) |
065 | imageSourceClone.Dispose(); |
066 | if (imageMask != null) |
068 | if (foregroundModel != null) |
069 | foregroundModel.Dispose(); |
070 | if (backgroundModel != null) |
071 | backgroundModel.Dispose(); |
075 | private void btnLoadImage_Click(object sender, EventArgs e) |
077 | OpenFileDialog ofd = new OpenFileDialog(); |
078 | ofd.CheckFileExists = true ; |
079 | ofd.DefaultExt = "jpg" ; |
080 | ofd.Filter = "图片文件|*.jpg;*.png;*.bmp|所有文件|*.*" ; |
081 | if (ofd.ShowDialog( this ) == DialogResult.OK) |
083 | if (ofd.FileName != "" ) |
085 | sourceImageFileName = ofd.FileName; |
093 | private void btnReload_Click(object sender, EventArgs e) |
099 | private void LoadImage() |
101 | if (imageSource != null) |
102 | imageSource.Dispose(); |
103 | imageSource = new Image<Bgr, byte>(sourceImageFileName); |
104 | if (imageSourceClone != null) |
105 | imageSourceClone.Dispose(); |
106 | imageSourceClone = imageSource.Copy(); |
107 | pbSource.Image = imageSourceClone.Bitmap; |
108 | if (imageMask != null) |
110 | imageMask = new Image<Gray, byte>(imageSource.Size); |
112 | xScale = 1d * imageSource.Width / pbSource.Width; |
113 | yScale = 1d * imageSource.Height / pbSource.Height; |
114 | rect = new Rectangle(-1, -1, 1, 1); |
119 | private void pbSource_MouseDown(object sender, MouseEventArgs e) |
122 | rect = new Rectangle(( int )(e.X * xScale), ( int )(e.Y * yScale), 1, 1); |
124 | previousMouseLocation = new Point(( int )(e.X * xScale), ( int )(e.Y * yScale)); |
128 | private void pbSource_MouseMove(object sender, MouseEventArgs e) |
131 | if (rbRect.Checked && e.Button != MouseButtons.None) |
133 | rect = new Rectangle(rect.Left, rect.Top, ( int )(e.X * xScale - rect.Left), ( int )(e.Y * yScale - rect.Top)); |
134 | imageSourceClone.Dispose(); |
135 | imageSourceClone = imageSource.Clone(); |
136 | imageSourceClone.Draw(rect, Blue, LineWidth); |
137 | pbSource.Image = imageSourceClone.Bitmap; |
141 | if (rbMask.Checked && (e.Button == MouseButtons.Left || e.Button == MouseButtons.Right)) |
143 | if (previousMouseLocation.X == -1 && previousMouseLocation.Y == -1) |
145 | previousMouseLocation.X = ( int )(e.X * xScale); |
146 | previousMouseLocation.Y = ( int )(e.Y * yScale); |
150 | LineSegment2D line = new LineSegment2D(previousMouseLocation, new Point(( int )(e.X * xScale), ( int )(e.Y * yScale))); |
151 | if (e.Button == MouseButtons.Left) |
153 | imageMask.Draw(line, new Gray(( double )GC_FGD), LineWidth); |
154 | imageSourceClone.Draw(line, Green, LineWidth); |
158 | imageMask.Draw(line, new Gray(( double )GC_BGD), LineWidth); |
159 | imageSourceClone.Draw(line, Red, LineWidth); |
161 | pbSource.Image = imageSourceClone.Bitmap; |
162 | previousMouseLocation = line.P2; |
169 | private void pbSource_MouseUp(object sender, MouseEventArgs e) |
171 | if (rbRect.Checked && e.Button != MouseButtons.None) |
173 | rect = new Rectangle(rect.Left, rect.Top, ( int )(e.X * xScale - rect.Left), ( int )(e.Y * yScale - rect.Top)); |
174 | imageSourceClone.Dispose(); |
175 | imageSourceClone = imageSource.Clone(); |
176 | imageSourceClone.Draw(rect, Blue, LineWidth); |
177 | pbSource.Image = imageSourceClone.Bitmap; |
180 | imageMask.Draw(rect, new Gray(( double )GC_PR_FGD), 0); |
184 | previousMouseLocation = new Point(-1, -1); |
188 | private void btnStartSegment_Click(object sender, EventArgs e) |
190 | if (rect != new Rectangle(-1, -1, 1, 1)) |
192 | Stopwatch sw = new Stopwatch(); |
193 | Image<Gray, Byte> mask = null; |
199 | mask = imageSource.GrabCut(rect, ( int )nudIterCount.Value); |
201 | imageMask = mask.Clone(); |
203 | ShowResult( "用矩形窗初始化GrabCut并计算" , sw.ElapsedMilliseconds); |
208 | mask = imageMask.Clone(); |
213 | CvInvoke.CvGrabCut(imageSource.Ptr, mask.Ptr, ref rect, backgroundModel.Ptr, foregroundModel.Ptr, 1, GRABCUT_INIT_TYPE.INIT_WITH_MASK); |
216 | ShowResult( "用掩码初始化GrabCut" , sw.ElapsedMilliseconds); |
220 | CvInvoke.CvGrabCut(imageSource.Ptr, mask.Ptr, ref rect, backgroundModel.Ptr, foregroundModel.Ptr, ( int )nudIterCount.Value, GRABCUT_INIT_TYPE.EVAL); |
222 | ShowResult( "计算GrabCut" , sw.ElapsedMilliseconds); |
224 | CvInvoke.cvAndS(mask.Ptr, new MCvScalar(1d), mask.Ptr, IntPtr.Zero); |
225 | Image<Bgr, Byte> result = imageSource.Copy(mask); |
226 | pbResult.Image = result.Bitmap; |
231 | MessageBox.Show( this , "在开始分割之前,请在源图像上绘制一个矩形窗口。" , "缺少矩形窗" , MessageBoxButtons.OK, MessageBoxIcon.Information); |
239 | private void ShowResult(string prompt, double elapsedMilliseconds) |
241 | txtResult.Text += string.Format( "{0},耗时:{1:F04}毫秒,参数(矩形窗起点:{2},大小:{3}X{4},迭代次数:{5})。\r\n" , |
242 | prompt, elapsedMilliseconds, rect.Location, rect.Width, rect.Height, nudIterCount.Value); |