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

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方法。封装的方法便于使用,但是缺少一些功能,灵活性不足。

001using System;
002using System.Collections.Generic;
003using System.ComponentModel;
004using System.Data;
005using System.Drawing;
006using System.Linq;
007using System.Text;
008using System.Windows.Forms;
009using System.Diagnostics;
010using System.Runtime.InteropServices;
011using Emgu.CV;
012using Emgu.CV.CvEnum;
013using Emgu.CV.Structure;
014 
015namespace NewFeaturesOfOpenCV2._1
016{
017    public partial class FormGrabCut : Form
018    {
019        //常量
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;                            //可能的前景标志
028        //成员变量
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;                                 //原始图像与PictureBox在x轴方向上的缩放
036        private double yScale = 1d;                                 //原始图像与PictureBox在y轴方向上的缩放
037        private Point previousMouseLocation = new Point(-1, -1);    //上次绘制线条时,鼠标所处的位置
038        private Rectangle rect;                                     //初始化矩形窗口
039        private bool initialized = false;                           //是否已经初始化过GrabCut
040 
041        public FormGrabCut()
042        {
043            InitializeComponent();
044        }
045 
046        //加载窗体时
047        private void FormGrabCut_Load(object sender, EventArgs e)
048        {
049            //设置提示
050            toolTip.SetToolTip(rbRect, "使用鼠标在源图像绘制矩形窗口,在图像分割之前使用矩形窗口所在的区域进行初始化。");
051            toolTip.SetToolTip(rbMask, "使用鼠标在源图像绘制掩码,左键绘制前景掩码,邮件绘制背景掩码,在图像分割之前使用掩码图像进行初始化。");
052            //初始化前景模型和背景模型
053            foregroundModel = new Matrix<float>(1, 13 * 5);
054            backgroundModel = new Matrix<float>(1, 13 * 5);
055            //加载默认图像
056            LoadImage();
057        }
058 
059        //关闭窗体前,释放资源
060        private void FormGrabCut_FormClosing(object sender, FormClosingEventArgs e)
061        {
062            if (imageSource != null)
063                imageSource.Dispose();
064            if (imageSourceClone != null)
065                imageSourceClone.Dispose();
066            if (imageMask != null)
067                imageMask.Dispose();
068            if (foregroundModel != null)
069                foregroundModel.Dispose();
070            if (backgroundModel != null)
071                backgroundModel.Dispose();
072        }
073 
074        //加载源图像
075        private void btnLoadImage_Click(object sender, EventArgs e)
076        {
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)
082            {
083                if (ofd.FileName != "")
084                {
085                    sourceImageFileName = ofd.FileName;
086                    LoadImage();
087                }
088            }
089            ofd.Dispose();
090        }
091 
092        //重新加载图像
093        private void btnReload_Click(object sender, EventArgs e)
094        {
095            LoadImage();
096        }
097 
098        //加载源图像
099        private void LoadImage()
100        {
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)
109                imageMask.Dispose();
110            imageMask = new Image<Gray, byte>(imageSource.Size);
111            imageMask.SetZero();
112            xScale = 1d * imageSource.Width / pbSource.Width;
113            yScale = 1d * imageSource.Height / pbSource.Height;
114            rect = new Rectangle(-1, -1, 1, 1);
115            initialized = false;
116        }
117 
118        //鼠标在源图像上按下时
119        private void pbSource_MouseDown(object sender, MouseEventArgs e)
120        {
121            if (rbRect.Checked)
122                rect = new Rectangle((int)(e.X * xScale), (int)(e.Y * yScale), 1, 1);
123            else
124                previousMouseLocation = new Point((int)(e.X * xScale), (int)(e.Y * yScale));
125        }
126 
127        //鼠标在源图像上移动时
128        private void pbSource_MouseMove(object sender, MouseEventArgs e)
129        {
130            //绘制矩形
131            if (rbRect.Checked && e.Button != MouseButtons.None)
132            {
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;
138                return;
139            }
140            //绘制线条,用于手工标记前景或者背景
141            if (rbMask.Checked && (e.Button == MouseButtons.Left || e.Button == MouseButtons.Right))
142            {
143                if (previousMouseLocation.X == -1 && previousMouseLocation.Y == -1)
144                {
145                    previousMouseLocation.X = (int)(e.X * xScale);
146                    previousMouseLocation.Y = (int)(e.Y * yScale);
147                }
148                else
149                {
150                    LineSegment2D line = new LineSegment2D(previousMouseLocation, new Point((int)(e.X * xScale), (int)(e.Y * yScale)));
151                    if (e.Button == MouseButtons.Left)
152                    {
153                        imageMask.Draw(line, new Gray((double)GC_FGD), LineWidth);
154                        imageSourceClone.Draw(line, Green, LineWidth);
155                    }
156                    else
157                    {
158                        imageMask.Draw(line, new Gray((double)GC_BGD), LineWidth);
159                        imageSourceClone.Draw(line, Red, LineWidth);
160                    }
161                    pbSource.Image = imageSourceClone.Bitmap;
162                    previousMouseLocation = line.P2;
163                }
164 
165            }
166        }
167 
168        //鼠标在源图像上松开时
169        private void pbSource_MouseUp(object sender, MouseEventArgs e)
170        {
171            if (rbRect.Checked && e.Button != MouseButtons.None)
172            {
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;
178                //绘制矩形结束之后,初始化掩码图像
179                imageMask.SetZero();
180                imageMask.Draw(rect, new Gray((double)GC_PR_FGD), 0);
181                return;
182            }
183            if (rbMask.Checked)
184                previousMouseLocation = new Point(-1, -1);
185        }
186 
187        //开始图像分割
188        private void btnStartSegment_Click(object sender, EventArgs e)
189        {
190            if (rect != new Rectangle(-1, -1, 1, 1))  //必须指定矩形窗
191            {
192                Stopwatch sw = new Stopwatch();
193                Image<Gray, Byte> mask = null;
194                if (rbRect.Checked)
195                {
196                    //用矩形窗初始化
197                    sw.Reset();
198                    sw.Start();
199                    mask = imageSource.GrabCut(rect, (int)nudIterCount.Value);  //注:Image.GrabCut等价于先用矩形初始化CvGrabCut(....,GRABCUT_INIT_TYPE.INIT_WITH_RECT),然后再计算CvGrabCut(....,GRABCUT_INIT_TYPE.INIT_WITH_EVAL)
200                    sw.Stop();
201                    imageMask = mask.Clone();
202                    initialized = true;
203                    ShowResult("用矩形窗初始化GrabCut并计算", sw.ElapsedMilliseconds);
204                }
205                else
206                {
207                    //用掩码初始化
208                    mask = imageMask.Clone();
209                    if (!initialized)
210                    {
211                        sw.Reset();
212                        sw.Start();
213                        CvInvoke.CvGrabCut(imageSource.Ptr, mask.Ptr, ref rect, backgroundModel.Ptr, foregroundModel.Ptr, 1, GRABCUT_INIT_TYPE.INIT_WITH_MASK);
214                        sw.Stop();
215                        initialized = true;
216                        ShowResult("用掩码初始化GrabCut", sw.ElapsedMilliseconds);
217                    }
218                    sw.Reset();
219                    sw.Start();
220                    CvInvoke.CvGrabCut(imageSource.Ptr, mask.Ptr, ref rect, backgroundModel.Ptr, foregroundModel.Ptr, (int)nudIterCount.Value, GRABCUT_INIT_TYPE.EVAL);
221                    sw.Stop();
222                    ShowResult("计算GrabCut", sw.ElapsedMilliseconds);
223                }
224                CvInvoke.cvAndS(mask.Ptr, new MCvScalar(1d), mask.Ptr, IntPtr.Zero);    //将掩码图像和1进行按位“与”操作,这样背景及可能的背景将变为0;而前景及可能的前景将变成1
225                Image<Bgr, Byte> result = imageSource.Copy(mask);
226                pbResult.Image = result.Bitmap;
227                mask.Dispose();
228                //result.Dispose();
229            }
230            else
231                MessageBox.Show(this, "在开始分割之前,请在源图像上绘制一个矩形窗口。", "缺少矩形窗", MessageBoxButtons.OK, MessageBoxIcon.Information);
232        }
233 
234        /// <summary>
235        /// 显示结果
236        /// </summary>
237        /// <param name="prompt">提示</param>
238        /// <param name="elapsedMilliseconds">耗时</param>
239        private void ShowResult(string prompt, double elapsedMilliseconds)
240        {
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);
243        }
244    }
245}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值