玩转轮廓
描绘图形轮廓的技巧
- 下载 TraceOutline2d.zip 仅含可执行文件 - 569.6 KB
- 下载 FunWithOutLines2d-noexe.zip - 1.1 MB
- 下载 FunWithOutLines2d.zip - 2.2 MB
引言
看一张数字图像。它只是大量的像素,但您或许能看到线条、色度、形状、结构……然后是物体和实体。您是否曾想过,您是如何理解这堆杂乱的像素的?
在本文中,我们将探讨一种使用轮廓追踪技术将图像分割成子图像的方法。
背景
将图像分割成子图像通常称为图像分割。主要有两种方法:颜色法和轮廓法。
在颜色法中,我们将颜色相近且位置邻近的像素组合成斑点。
在轮廓法中,我们寻找与背景像素相邻的前景像素,然后将它们连接起来形成一个闭合路径。闭合路径内的像素将构成子图像。
图 1
一些初步概念
相邻像素
在图1中,标记为 X 的像素有8个相邻像素(每个相距1个像素),方向从左上角(1)开始顺时针到左侧(8)。其他方向按顺时针顺序依次为:上方(2)、右上(3)、右侧(4)、右下(5)、下方(6)和左下(7)。
前景像素和背景像素
前景像素是属于我们想要分离的对象(子图像)的像素。背景像素是围绕对象的其余像素。在图1中,左侧和上方的较暗像素是前景像素,而右侧和下方的像素是背景像素。
Outline
轮廓像素是至少有一个相邻像素是背景像素的前景像素。在图1中,标记为1、2、3...10的较暗像素就是一些轮廓像素。
算法
1. 从靠近对象轮廓的一个像素开始。
2. 找到距离步骤1中像素最近的轮廓像素。这是第一个轮廓像素。
3. 从步骤2中的轮廓像素开始,顺时针探测其相邻像素,找到下一个轮廓像素。这是下一个轮廓像素。
4. 重复步骤3,直到我们回到步骤2中的像素。
举例说明,在图1中,如果我们从标记为X的像素开始,第一个轮廓像素将是标记为2的深色像素。顺时针前进,在下方(6)方向找到下一个轮廓像素,即标记为3的像素。从这里开始,以像素3为参考,我们的起始方向将是像素2之后的下一个顺时针方向,即右上(3),我们在左下(7)方向找到下一个轮廓像素,即像素4。
下面的代码实现了该算法。
public string TraceOutlineN(Bitmap bm, int x0, int y0, int probe_width, Color fg, Color bg, bool bauto_threshold, int n)
{
...
while (!hitstart)
{
count++;
//fallback to prevent infinite loop
if (count > countlimit)
{
return "";
}
//getting all the neighbours' pixel color
try
{
//processing top neighbours left to right
for (int i = 0; i <= 2 * n; i++)
{
diffx = i - n;
index1 = CoordsToIndex(x + diffx, y - n, bmpData.Stride);
cn[i] = ((x + diffx) >= 0 && (x + diffx) < max_width && (y - n) >= 0 && (y - n) < max_height) ?
Color.FromArgb(rgbValues[index1 + 2], rgbValues[index1 + 1], rgbValues[index1])
: Color.Empty;
}
//processing right neighbours top to bottom
for (int i = 2 * n + 1; i < 4 * n; i++)
{
diffy = i - 3 * n;
index1 = CoordsToIndex(x + n, y + diffy, bmpData.Stride);
cn[i] = ((x + n) >= 0 && (x + n) < max_width && (y + diffy) >= 0 && (y + diffy) < max_height) ?
Color.FromArgb(rgbValues[index1 + 2], rgbValues[index1 + 1], rgbValues[index1])
: Color.Empty;
}
//processing bottom neighbours right to left
for (int i = 4 * n; i <= 6 * n; i++)
{
diffx = i - 5 * n;
index1 = CoordsToIndex(x - diffx, y + n, bmpData.Stride);
cn[i] = ((x - diffx) >= 0 && (x - diffx) < max_width && (y + n) >= 0 && (y + n) < max_height) ?
Color.FromArgb(rgbValues[index1 + 2], rgbValues[index1 + 1], rgbValues[index1])
: Color.Empty;
}
//processing left neighbours bottom to top
for (int i = 6 * n + 1; i < 8 * n; i++)
{
diffy = i - 7 * n;
index1 = CoordsToIndex(x - n, y - diffy, bmpData.Stride);
cn[i] = ((x - n) >= 0 && (x - n) < max_width && (y - diffy) >= 0 && (y - diffy) < max_height) ?
Color.FromArgb(rgbValues[index1 + 2], rgbValues[index1 + 1], rgbValues[index1])
: Color.Empty;
}
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
return "";
}
int index = 0;
string tests = "";
bool dir_found = false;
//find the first valid foreground pixel
for (int i = start_direction; i < start_direction + (8 * n); i++)
{
index = i % (8 * n);
if (!cn[index].Equals(Color.Empty))
if (GetMonoColor(cn[index]) == gfg)
{
current_direction = index;
dir_found = true;
break;
}
}
//if no foreground pixel found, just find the next valid pixel
if (!dir_found)
for (int i = start_direction; i < start_direction + (8 * n); i++)
{
index = i % (8 * n);
if (!cn[index].Equals(Color.Empty))
{
current_direction = index;
dir_found = true;
break;
}
}
// find the next direction to look for foreground pixels
if ((index >= 0) && (index <= 2 * n))
{
diffx = index - n;
x += diffx;
y -= n;
}
if ((index > 2 * n) && (index < 4 * n))
{
diffy = index - 3 * n;
x += n;
y += diffy;
}
if ((index >= 4 * n) && (index <= 6 * n))
{
diffx = index - 5 * n;
x -= diffx;
y += n;
}
if ((index > 6 * n) && (index < 8 * n))
{
diffy = index - 7 * n;
x -= n;
y -= diffy;
}
//store the found outline
tests = x + "," + y + ";";
s = s + tests;
start_direction = (current_direction + (4 * n + 1)) % (8 * n);
//adaptive stop condition
bool bMinCountOK = (n > 1) ? (count > (max_height / 5)) : (count > 10);
if (bMinCountOK && (Math.Abs(x - x1) < (n + 1) && (Math.Abs(y - y1) < (n + 1))))
hitstart = true;
}
return s;
}
使用代码
public string TraceOutlineN(Bitmap bm, int x0, int y0, int probe_width, Color fg, Color bg, bool bauto_threshold, int n)
参数
System.Drawing.Bitmap bm - 这可以是窗体或控件的Image属性。
int x0, int y0 - 图像中开始探测轮廓的像素的x和y坐标。
int probe_width - (水平)探测以找到第一个轮廓像素的像素数。
Color fg - 前景颜色(通常设置为黑色)。
Color bg - 背景颜色(通常设置为白色)。
bool bauto_threshold - 是否自动计算阈值以确定前景和背景的选项。
int n - 连接相距n个像素的轮廓像素。对于追踪清晰连接的轮廓,此值设置为1。
返回:格式为 "x0,y0;x1,y1;x2,y2;..." 的字符串,包含连接的轮廓像素的坐标。
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
CTraceOuline trace = new CTraceOuline();
string s=trace.TraceOutlineN((Bitmap)pictureBox1.Image, e.X, e.Y,20, Color.Black, Color.White,true, 1);
if (s != "")
{
Graphics g = Graphics.FromImage(pictureBox1.Image);
Point[] p = trace.StringOutline2Polygon(s);
Form2 f2 = new Form2();
f2.BackgroundImage = (Bitmap)pictureBox1.Image.Clone();
GraphicsPath gp=new GraphicsPath();
gp.AddPolygon(p);
f2.Region = new Region(gp);
f2.Show();
f2.Left = e.X;
f2.Refresh();
g.DrawPolygon(new Pen(Color.Red, 1), p);
pictureBox1.Refresh();
}
else
{
MessageBox.Show("Failed tracing outline!");
}
}
}
上述代码在释放鼠标右键时触发。鼠标的x,y坐标被用作探测起点,探测20个像素以找到起始轮廓像素。设置了自动阈值,前景为黑色,背景为白色,连接像素间距设置为1个像素。
TraceOulineN() 函数返回的字符串值由 StringOutline2Polgon() 处理,以获得一个点数组。
这个点数组用于在 GraphicsPath 对象中创建一个多边形路径,该路径又可用于创建一个 Region 对象。
该 Region 对象被分配给一个新窗体,这样窗体的形状就呈现为所追踪的轮廓形状。图像也已被复制到新窗体中,因此它看起来就像是从追踪对象上剥离出来的。
演示
对于本文开头的截图,我预加载了一张自行车的图片文件。在后轮边缘上或附近右键单击,自行车就会被剥离到桌面上!
您也可以在图片框上(左键拖动)进行绘制来创建一幅画,然后在您画的图形边缘附近右键单击。双击图片框会清除其内容。单击“重新加载”按钮可以重新加载之前加载的图像。
对于弹出的图形(新窗体),您可以拖动它,或者在某个子区域上右键单击以移除该子区域。请注意,在截图中,“hello”这个词中的“O”是透明的,您可以通过它看到桌面。这是通过在“O”的内部右键单击以移除该子区域来实现的。双击弹出的图形会将其从桌面上卸载。
对于弹出的图形,Alt+左键单击(按住Alt键同时进行鼠标左键单击)将使其顺时针旋转10度。同样,Alt+右键单击将使图形逆时针旋转10度。您还可以通过Ctrl+右键单击来缩小弹出图形,通过Ctrl+左键单击来放大。
有关此类图像变换的更多详细信息,请参阅我的文章:
我增加了一个有趣的功能,即创建标注。在文本框中输入一些文本,然后点击“创建标注”按钮。一个带有您文本信息的标注将弹出到桌面上。
更多演示
我在演示中添加了一些功能,以展示 CTraceOutline 类的更高级和有趣的用法。
图2和图3展示了手动设置阈值和颜色过滤。
图 2
在图2中,首先禁用自动阈值并勾选绿色的复选框,然后滑动阈值滚动条。当滚动条滑动时,主图片框中的图片将被转换为单色图片,显示在滚动条下方的小图片框中。请注意,当您滑动时,图像中不包含被过滤颜色的部分将消失。当单色图片只显示对应绿色圆圈的部分时,停止滑动,并在主图片框中右上角绿色圆圈的边缘附近右键单击。
图 3
在图3中,我们尝试移除花瓶后面的嘈杂背景。禁用自动阈值,然后选择一个颜色过滤器并滑动阈值滚动条,直到您在单色图片框中看到花朵和背景之间有合理的间隙。然后在主图片中花朵的边缘附近右键单击。
图 4
图4展示了N的使用,这是TraceOutlineN()函数的最后一个参数。N是我们想要连接的轮廓像素之间的像素数。这对于轮廓连接不清晰的低质量图像很有用。请看图4中像素的放大区域,边缘像素没有连接。单词“Connect”是使用影线画刷(hatch-brush)书写的。当我们取消勾选“实心画刷”复选框时,会选择影线画刷。对于这个图像片段,任何轮廓像素最近的相邻轮廓像素至少相距2个像素。我们可以通过将N设置为 > 1来尝试连接边缘像素。在这种情况下,我们将其设置为4,然后在图像片段的边缘附近右键单击。
关注点
演示窗体可以最小化,并且可以用来创建弹出图像,甚至可以通过易于使用的标注信息创建功能在桌面上留下消息和提醒。祝您玩得开心!
历史
版本1:2014年4月17日
2014年5月3日:在演示中添加了更多功能,以展示CTraceOutline类的更高级用法
2014年5月5日:添加了旋转图像和区域的功能
2014年5月7日:添加了图像和区域的缩放功能。添加了标注创建功能
2014年5月9日:修复了变换的错误,并添加了嵌入资源的选择和加载。同时简化了标注创建步骤