最近一段时间在开发一个画图软件,其中需要实现魔棒功能。网上浏览了一圈,没有找到。苦思之后,终于开窍了:。思路是:先用漫水填充算法, 获得一张连通区域的二值图。然后对这幅图进行边缘检测,获取边缘。如果使用emgucv或者opencv,可以直接使用函数floodFill()获得区域,再函数Canny()与FindContours()函数获得边界。
1.漫水填充
这里我不适用emgucv的方法,使用的是一个网友算法,改了一点点。
public Bitmap FloodFill(Bitmap src, Point location, Color fillColor, int threshould)
{
try
{
Bitmap srcbmp = src;
Bitmap dstbmp = new Bitmap(src.Width,src.Height);
int w = srcbmp.Width;
int h = srcbmp.Height;
Stack<Point> fillPoints = new Stack<Point>(w * h);
System.Drawing.Imaging.BitmapData bmpData = srcbmp.LockBits(new Rectangle(0, 0, srcbmp.Width, srcbmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
System.Drawing.Imaging.BitmapData dstbmpData = dstbmp.LockBits(new Rectangle(0, 0, dstbmp.Width, dstbmp.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
IntPtr ptr = bmpData.Scan0;
int stride = bmpData.Stride;
int bytes = bmpData.Stride * srcbmp.Height;
byte[] grayValues = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(ptr, grayValues, 0, bytes);
Color backColor = Color.FromArgb(grayValues[location.X * 3 + 2 + location.Y * stride], grayValues[location.X * 3 + 1 + location.Y * stride], grayValues[location.X * 3 + location.Y * stride]);
IntPtr dstptr = dstbmpData.Scan0;
byte[] temp = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(dstptr, temp, 0, bytes);
int gray = (int)((backColor.R + backColor.G + backColor.B) / 3);
if (location.X < 0 || location.X >= w || location.Y < 0 || location.Y >= h) return null;
fillPoints.Push(new Point(location.X, location.Y));
int[,] mask = new int[w, h];
while (fillPoints.Count > 0)
{
Point p = fillPoints.Pop();
mask[p.X, p.Y] = 1;
temp[3 * p.X + p.Y * stride] = (byte)fillColor.B;
temp[3 * p.X + 1 + p.Y * stride] = (byte)fillColor.G;
temp[3 * p.X + 2 + p.Y * stride] = (byte)fillColor.R;
if (p.X > 0 && (Math.Abs(gray - (int)((grayValues[3 * (p.X - 1) + p.Y * stride] + grayValues[3 * (p.X - 1) + 1 + p.Y * stride] + grayValues[3 * (p.X - 1) + 2 + p.Y * stride]) / 3)) < threshould) && (mask[p.X - 1, p.Y] != 1))
{
temp[3 * (p.X - 1) + p.Y * stride] = (byte)fillColor.B;
temp[3 * (p.X - 1) + 1 + p.Y * stride] = (byte)fillColor.G;
temp[3 * (p.X - 1) + 2 + p.Y * stride] = (byte)fillColor.R;
fillPoints.Push(new Point(p.X - 1, p.Y));
mask[p.X - 1, p.Y] = 1;
}
if (p.X < w - 1 && (Math.Abs(gray - (int)((grayValues[3 * (p.X + 1) + p.Y * stride] + grayValues[3 * (p.X + 1) + 1 + p.Y * stride] + grayValues[3 * (p.X + 1) + 2 + p.Y * stride]) / 3)) < threshould) && (mask[p.X + 1, p.Y] != 1))
{
temp[3 * (p.X + 1) + p.Y * stride] = (byte)fillColor.B;
temp[3 * (p.X + 1) + 1 + p.Y * stride] = (byte)fillColor.G;
temp[3 * (p.X + 1) + 2 + p.Y * stride] = (byte)fillColor.R;
fillPoints.Push(new Point(p.X + 1, p.Y));
mask[p.X + 1, p.Y] = 1;
}
if (p.Y > 0 && (Math.Abs(gray - (int)((grayValues[3 * p.X + (p.Y - 1) * stride] + grayValues[3 * p.X + 1 + (p.Y - 1) * stride] + grayValues[3 * p.X + 2 + (p.Y - 1) * stride]) / 3)) < threshould) && (mask[p.X, p.Y - 1] != 1))
{
temp[3 * p.X + (p.Y - 1) * stride] = (byte)fillColor.B;
temp[3 * p.X + 1 + (p.Y - 1) * stride] = (byte)fillColor.G;
temp[3 * p.X + 2 + (p.Y - 1) * stride] = (byte)fillColor.R;
fillPoints.Push(new Point(p.X, p.Y - 1));
mask[p.X, p.Y - 1] = 1;
}
if (p.Y < h - 1 && (Math.Abs(gray - (int)((grayValues[3 * p.X + (p.Y + 1) * stride] + grayValues[3 * p.X + 1 + (p.Y + 1) * stride] + grayValues[3 * p.X + 2 + (p.Y + 1) * stride]) / 3)) < threshould) && (mask[p.X, p.Y + 1] != 1))
{
temp[3 * p.X + (p.Y + 1) * stride] = (byte)fillColor.B;
temp[3 * p.X + 1 + (p.Y + 1) * stride] = (byte)fillColor.G;
temp[3 * p.X + 2 + (p.Y + 1) * stride] = (byte)fillColor.R;
fillPoints.Push(new Point(p.X, p.Y + 1));
mask[p.X, p.Y + 1] = 1;
}
}
fillPoints.Clear();
System.Runtime.InteropServices.Marshal.Copy(temp, 0, dstptr, bytes);
srcbmp.UnlockBits(bmpData);
dstbmp.UnlockBits(dstbmpData);
return dstbmp;
}
catch (Exception exp)
{
MessageBox.Show(exp.Message);
return null;
}
}
实际上这个函数时有缺陷的,转换位图数据时用了System.Drawing.Imaging.PixelFormat.Format24bppRgb
这是不可取的。应该是32位argb。否则无法处理透明与黑色。这里灰度值使用平均值,我觉得应该使用PS开源程序的加权方法。
这里获得了一个连通的区域,实际上相当于一张掩码图。利用这张图,一是方便追踪边界,而是对原图进行掩码操作,进行分离等。
2.边缘追踪
使用边缘追踪算法,可以真正将边缘寻找出来,这样可以得到有序的点集合,可惜我不太理解EmguCV的CvInvoke.FindContours()获得的Emgu.CV.Util.VectorOfVectorOfPoint
是怎么个有序排列发,只好粗暴地显示出来算数。
最可靠的还那张掩码图,而不是这些莫名其妙的边界点。
private bool FloodFillTOcanny()
{
Image<Bgr, Byte> srcimg = new Image<Bgr, Byte>((Bitmap)pictureBox2.Image);
//转成灰度图
Image<Gray, Byte> grayimg = srcimg.Convert<Gray, Byte>() ;
// CvInvoke.BitwiseNot(grayimg, grayimg);
//Canny 边缘检测
Image<Gray, Byte> cannyGrayimg = grayimg.Canny((int)numericUD_FloodFill.Value, (int)numericUD_FloodFill.Value);
Gray bkGrayWhite = new Gray(255);
Emgu.CV.IOutputArray hierarchy = new Image<Gray, Byte>(srcimg.Width, srcimg.Height, bkGrayWhite);
Image<Rgb, Byte> imgresult = new Image<Rgb, Byte>(srcimg.Width, srcimg.Height, new Rgb(255, 255, 255));
CvInvoke.FindContours( cannyGrayimg,
contours,
(Emgu.CV.IOutputArray)hierarchy,
Emgu.CV.CvEnum.RetrType.Ccomp,
Emgu.CV.CvEnum.ChainApproxMethod.ChainApproxNone//保存为
);
GraphicsPath myGraphicsPath = new GraphicsPath();
Region myRegion = new Region();
myGraphicsPath.Reset();
int areaMax = 0, idx = 0 ;
for (int ii = 0; ii < contours.Size ; ii++)
{
int area = (int)CvInvoke.ContourArea(contours[ii]);
if (area > areaMax) { areaMax = area; idx = ii;}
if (area < 1) continue;
CvInvoke.DrawContours(imgresult, contours, ii, new MCvScalar(0, 0, 0), 1, Emgu.CV.CvEnum.LineType.EightConnected, (Emgu.CV.IInputArray)null, 2147483647);
imageBox1.Image = imgresult;
try
{
myGraphicsPath.AddPolygon( contours[ii].ToArray() );
}
catch
{
//MessageBox.Show(e.Message);
}
}
myRegion.MakeEmpty();
myRegion.Union(myGraphicsPath);
pictureBox1.Refresh();
Pen pen = new Pen(Color.Red, 1);
pen.DashStyle = DashStyle.Dot;
Graphics gs = pictureBox1.CreateGraphics();
gs.DrawPath(pen, myGraphicsPath);
if (myRegion.IsVisible(lastPoint) )
{
//gs.DrawPolygon(pen, respts);
}
else
{
//gs.DrawRectangle(pen, new Rectangle(0,0,pictureBox1.Image.Width, pictureBox1.Image.Height));
}
gs.Dispose();
return true;
}
这个函数也是不可取的,我只做一个演示,其中CvInvoke.FindContours( cannyGrayimg,
contours,
(Emgu.CV.IOutputArray)hierarchy,
Emgu.CV.CvEnum.RetrType.Ccomp,
Emgu.CV.CvEnum.ChainApproxMethod.ChainApproxNone//保存方式
);
函数最后的一个参数影响还是挺大的,使用是可以试试不同枚举参数;
3.测试
拉了几个控件,测试一番
我的鼠标猥琐地点了大腿那里,第二图显示漫水算法得到掩码图,最后一张图是边缘获取的结果,并且加到了原图上面。
源码:http://download.youkuaiyun.com/download/wangzibigan/10172940
(不知道怎么设成免费;文件28m其实大部分是emgucv库)
我的这篇文章其实没有实现魔棒功能,只是一个边缘的获取显示,所有我又写了第二篇。
我项目中的软件已经实现了魔棒功能,目前正在认真完善”羽化“功能。