我尝试使用众所周知的2遍扫描线算法实现多边形三角测量,该算法将多边形细分为第一扫描线通道中的单调子组件,然后在第二遍中对这些单调组件进行三角测量。我目前的实现适用于一般情况,但我不能为我的生活找出如何调整它来处理包含多个重合边缘段的输入(当从左到右扫描时具有相等x坐标的段,或从右向左扫描时等于y坐标。
编辑: 我只是意识到我构思这个问题的方式使它很长很冗长,所以这里有一个快速的TL; DR;对于任何了解多边形三角剖分但不想读整件事的人来说:以下形状是三角测量算法第二遍的有效输入吗?如果是:如何调整第二遍来处理它,如果没有:我如何调整第一遍以使它产生可以被送入第二遍的单调子组件:
http://www.wouterbijlsma.nl/~wouter/tmp/RTdr6rET9.png
这一点下方问题的长版本; - )
算法的快速概述:
第一遍将输入多边形细分为'单调子组件'。单调子组件是一个多边形,可以分成2个连接的链,这些链的坐标从左到右排序(当使用垂直扫描线实现算法时),或从上到下(当使用水平扫描时)线)。让我们假设我们使用垂直扫描线:然后可以将每个单调子组件拆分为上链和下链,连接在最小和最大x坐标处,并且当扫描任一链的顶点时, x坐标正在增加。如果子组件严格monontone ,则上链和下链不能具有相同x坐标的边缘段(即:垂直边缘段)。
第二遍扫描单调子组件,并通过添加内部边缘将它们细分为三角形。这个想法是每个子组件从左到右扫描,并且在扫描线击中的每个顶点处,可能发生以下两种情况之一:a)扫描线左侧的非三角形区域可以通过添加对角线进行三角测量,或b)当前顶点不能看到'在扫描线左侧的非三角形区域中的任何先前扫描但未处理的顶点。在b)的情况下,顶点被推到堆栈('反射链'),并且通过构造,在某些情况下,a)将发生,并且反射链的顶点将被一个弹出-one并与对角线连接到最后一条扫描线顶点。
上面的描述缺少一些细节,但我认为任何知道如何回答我的问题的人都已经理解了这个算法,所以我不会在这里详细介绍。
我遇到的问题如下:假设我有一个多边形表示一个指向左侧的箭头,例如:
http://www.wouterbijlsma.nl/~wouter/tmp/RTdr6rET9.png
当我将这个形状输入到我的算法中时,单调细分传递使形状保持不变:其中有垂直边缘,因此它不是严格单调,但它是单调的,并且就我理解算法而言,它不必在你进行三角测量之前进行细分(也许这就是我出错的原因,因为我的假设很糟糕)。
现在假设我将(未修改的)箭头多边形输入第二遍以对其进行三角测量。如何处理箭头底部的2个垂直边缘线段?扫描线算法要求多边形的顶点从左到右排序,因此您可以假设答案将归结为如何使用相同的x坐标对顶点进行排序(例如,按照链的顺序,或在y坐标上,或者在多边形边界索引上),但无论我使用什么排序,三角测量总是会失败。
让我们调用最左边的顶点顶点0,并按逆时针顺序排列顶点。这意味着箭头底部的4个顶点是顶点1,2,5和6.我们有三个排序选项:
我用来实现算法的一些源材料说“在增加的y坐标上排序具有相等x坐标的顶点”,即:1,2,5,6。如果我这样做并扫描它们,第一个三角形出来正常(0,1,2),但之后算法将添加一个边(5,2),这将创建一个4顶点组件(0,2,5,6)。没有边缘(0,5)被添加,因为三角测量算法规定向反射链上除了第一个之外的所有先前的非三角形顶点添加边缘(改变这将破坏一般情况)。虽然由4个顶点限定的多边形区域是三角形的,但它显然不是三角形,因为它有4个点,并且由于它具有共线边缘,所以它也不是大多数定义的有效多边形。 / p>
我读到的另一篇论文说“打破关系,以便保持连锁订单”。这意味着我的示例中的4个顶点将被分类为1,2,6,5,因为下链和上链都是从左到右运行。如果我按此顺序扫描它们,我再次获得一个三角形(0,1,2),但是下一个扫描的顶点(6)将创建一个多边形(0,1,6),这比第一种情况更糟糕,因为它会创建一个边缘(1,6),它在顶点5上运行,但不包含它。扫描顶点5将完全弄乱算法的状态,因为它会创建一个简并三角形(1,5,6),一条线,并且扫描尾部顶点将无法对多边形的其余部分进行三角测量
按原始多边形顶点顺序排序(沿着边界,逆时针方向):在这种情况下,这将产生与情况1)相同的结果,即:1,2,5,6,已经显示为失败。
我开始认为像这样的箭头形状应该被认为是非单调的,或者(即使我从未在算法的任何描述中看到过这种情况),单调三角剖分算法需要输入到严格单调。这可能是我想念的吗?如果是,我将如何调整单调细分传递以处理(多个,重合)垂直边缘段?我使用的源素材将所有顶点分类为'开始','结束','合并''分裂'或者'定期'细分期间(下/上),但是在垂直段的情况下,这些分类是不明确的:根据这些顶点类的定义,垂直段的顶点部分可以被认为是开始/结束顶点,但也是拆分或合并顶点。或者我做必须在它们的y坐标上对4个顶点进行排序,然后创建一个带有2个共线边缘的无效4顶点三角形分量,然后将其修复为“#”。之后通过去除共线边缘上的顶点?
我用来实现算法的主要来源是引入三角测量算法的原始GJPT-78论文,它不是公开的(付费专区),所以我不能在这里链接它,但很多CS课程资料在线提供,描述了算法,我也使用了这些例子:
http://www.cs.ucsb.edu/~suri/cs235/Triangulation.pdf https://www.cs.umd.edu/class/spring2012/cmsc754/Lects/cmsc754-lects.pdf(参见'第6讲&第39章)
我已经阅读了其中的更多内容。几乎所有幻灯片,纸张,博客或其他任何描述的算法具体都提到了具有相等x坐标的顶点(如果使用水平扫描线,则提到y坐标),但它们所有只是说'我们假设没有相等的x坐标'并且这种限制很容易修复,仅用于简化表示。或者'不是算法的基础'管他呢。奇怪的是, none 他们关心详细说明支持这种情况所需的更改或变通方法,或者它们包含一些关于以某种方式排序相等x-vertices的模糊语句,这在实践中并不能解决问题。 / p>
也许我只是有点愚蠢或者缺少一些非常明显的东西,但是我现在一直在努力修复这个角落案例几天没有结果,而且它开始变得非常明显令人沮丧的。我假设实现了基本情况的算法(包括编写DCEL数据结构,扫描线算法,有序边缘图,确定内角和可见性所需的三角法,有效存储和查找顶点分类的数据结构等)几乎所有的工作都是如此,之后解决垂直边缘问题将是微不足道的。现在我花了更多的时间来修复垂直边缘,而不是所有其他的东西,我需要使算法适用于一般情况组合(它适用于我抛出的任何多边形,只要它没有&# 39; t有垂直边缘。)
谢谢!沃特
答案 0 :(得分:3)
我终于自己想出来了,所以我会回答我自己的问题,为了子孙后代; - )
事实证明,使三角测量算法适用于具有垂直边缘的多边形的变化很小,并且不需要特殊情况来处理它们。我不得不改变以下事项:
大多数关于算法的论文都提到了第一个要求。如David Eisenstat所述,从下到上排序具有象征性顺时针旋转的相同效果。
我必须做的第二个改变是因为我误解了各种顶点分类。我的假设是合并顶点应该始终将两个入射边完全放在其左边,一个完全向右分割的顶点,这是不正确的。如果2个入射边中的一个是垂直的,另一个是顶点的左边,则应该将其归类为“合并”,如果另一个边缘位于右侧,则应将其归类为“分割”。特别是,我不得不改变以下几行:
// Classify vertex based on the interior angle and which side of the sweep line the two edges are
BOOL reflex_vertex = (interiorAngle < 0);
BOOL both_left = (e_in.origin.coordinates.x < vertex.coordinates.x) && (e_out.destination.coordinates.x < vertex.coordinates.x);
BOOL both_right = (e_in.origin.coordinates.x > vertex.coordinates.x) && (e_out.destination.coordinates.x > vertex.coordinates.x);
if (!reflex_vertex && both_right)
type = K14SweepLineVertexTypeStart;
else if (!reflex_vertex && both_left)
type = K14SweepLineVertexTypeEnd;
else if (reflex_vertex && both_right)
type = K14SweepLineVertexTypeSplit;
else if (reflex_vertex && both_left)
type = K14SweepLineVertexTypeMerge;
else if ([_lowerChainVertices containsObject:@(vertex.id)])
type = K14SweepLineVertexTypeLowerChain;
else
type = K14SweepLineVertexTypeUpperChain;
对此:
// Classify vertex based on the interior angle and which side of the sweep line the two edges are
BOOL reflex_vertex = (interiorAngle < 0);
BOOL both_left = (e_in.origin.coordinates.x <= vertex.coordinates.x) && (e_out.destination.coordinates.x <= vertex.coordinates.x);
BOOL both_right = (e_in.origin.coordinates.x >= vertex.coordinates.x) && (e_out.destination.coordinates.x >= vertex.coordinates.x);
...
最后一项更改对于防止输出中具有3个共线点的退化三角形是必要的。在对单调子组件进行三角测量时,只要在与堆栈上的顶点相同的多边形链上找到顶点(“反射链”),就会将对角线从当前扫描线顶点添加到所有可见的反射链顶点。在我的实现中,通过查看堆栈顶部顶点的(带符号)内角来确定可见性。此检查仅查看角度的符号,其中正角度表示可见(内部小于或等于到pi弧度,或180度)。问题在于或相等部分,如果堆栈顶部的2个点加上当前扫描线顶点是共线的,则内角恰好是pi弧度,并且不应添加对角线。我不得不改变支票:
BOOL visible = (vi_x_interior_angle > 0.0f);
对此:
BOOL visible = (vi_x_interior_angle > 0.0f) && ((vi_x_interior_angle + COMPARE_EPSILON) < M_PI);
我使用一个小epsilon,如果您的顶点是静态/硬编码并且垂直边的x坐标完全相等,则不是必需的,但在我的情况下,可能会计算顶点并且可能有小的舍入误差。如果3个点几乎完全共线,则不添加对角线通常会产生比添加几乎为零的三角形更好的结果。
除了这三件事之外,不需要任何特殊的处理来使算法适用于任何简单的多边形(没有自交,没有孔)。至少浪费了20个小时来解决这个问题我感到有点沮丧,但至少我终于得到了愚蠢的工作。只是希望我读到的关于这个特定算法的许多论文中至少有一篇对我在实现中遗漏的3件事情更加明确: - /
答案 1 :(得分:0)
在我们的扫描线引擎中,我们在所有点中使用总字典排序,我们认为y
坐标为主坐标,x
坐标为 >次要坐标(在我们的例子中,我们从底部到顶部方向扫描)。在我们的实现中,为了概括具有相同y
坐标的点的处理,我们假设位于输入队列中较晚的点(具有较大的x
坐标)同时位于稍微位置#34;更高&#34;在飞机上一些无穷小的量。显然,这是通过一些无限小的剪切力进行剪切变换的概念变体,这已在评论中提到过。
而且,正如您在上面已经提到的,这种方法的副作用是,在一般情况下,它会导致形成不必要的&#34;在单调化阶段削减,如下所示
尽管形状沿垂直方向已经是单调的,但算法发现有必要添加红色显示的切割边缘(同样,在这种情况下,我们从底部到顶部方向扫描)。但是,由于我们的最终目标是三角测量,所以这不是一个大问题。当然,如果你对最终三角测量的质量有一些额外的限制,那么这些&#34;早期&#34;三角形可能成为一个问题。
在我的情况下,生成三角测量以便用作执行初始三角测量的动力学变换的算法的输入。为了稳健,动力学三角测量算法必须能够处理“坏”的问题。无论如何都是针状三角形,因此我不会对三角测量的质量施加任何限制。