【问题标题】:Monotone polygon triangulation with collinear, vertical edge segments具有共线垂直边段的单调多边形三角剖分
【发布时间】:2014-11-01 01:17:10
【问题描述】:

我正在尝试使用广为人知的 2 遍扫描线算法来实现多边形三角剖分,该算法在第一个扫描线遍中将多边形细分为单调子组件,然后在第二遍中对这些单调组件进行三角剖分。我当前的实现适用于一般情况,但我一生无法弄清楚如何调整它以处理包含多个重合边缘段的输入(从左到右扫描时具有相等 x 坐标的段,或从右到左扫过时,y 坐标相等)。

编辑: 我刚刚意识到我提出这个问题的方式让它变得非常冗长和冗长,所以这里有一个快速的 TL;DR;对于任何了解多边形三角剖分但不想阅读整个内容的人:以下形状是三角剖分算法第二遍的有效输入吗?如果是:如何调整第二遍来处理它,如果不是:我如何调整第一遍,使其产生单调的子组件,可以进入第二遍:

http://www.wouterbijlsma.nl/~wouter/tmp/RTdr6rET9.png

下面这个问题的长版本;-)

算法的快速概述:

  1. 第一遍将输入多边形细分为“单调子组件”。单调子组件是一个多边形,可以分成 2 个连接的链,其坐标从左到右排序(当使用垂直扫描线实现算法时)或从上到下排序(当使用水平扫描时)线)。假设我们使用一条垂直扫描线:然后每个单调子分量可以分成一个上链和一个下链,在最小和最大 x 坐标处连接,当扫描任一链的顶点时,x 坐标为增加。如果子组件严格单调,则上下链不能有x坐标相同的边段(即:垂直边段)。

  2. 第二遍扫描单调子组件并通过添加内部边缘将它们细分为三角形。这个想法是每个子组件从左到右扫描,并且在扫描线撞击的每个顶点处,可能发生以下两种情况之一:a)扫描线左侧的未三角区域可以通过添加对角线进行三角剖分,或 b) 当前顶点不能“看到”任何先前扫过的,但在扫掠线左侧的未三角化区域中未处理的顶点。在情况 b) 中,顶点被推入堆栈(“反射链”),并且通过构造,在某些情况下,情况 a) 将发生,并且反射链的顶点将一个接一个地弹出并与与最后一个扫描线顶点的对角线。

上面的描述缺少一些细节,但我假设任何知道如何回答我的问题的人都已经了解算法,所以我不会在这里详细说明。

我遇到的问题如下:假设我有一个多边形代表一个指向左侧的箭头,例如这样:

http://www.wouterbijlsma.nl/~wouter/tmp/RTdr6rET9.png

当我将这个形状输入到我的算法中时,单调细分通道保持形状不变:其中有垂直边缘,所以它不是严格单调的,但它是单调的,并且只要我了解该算法,它不必在您可以对其进行三角剖分之前进行细分(也许这是我出错的地方,因为我的假设是错误的)。

现在假设我将(未修改的)箭头多边形输入第二遍以对其进行三角测量。如何处理箭头底部的 2 个垂直边缘段?扫描线算法要求多边形的顶点从左到右排序,因此您会假设答案将归结为如何对具有相同 x 坐标的顶点进行排序(例如,按链顺序或 y 坐标,或在多边形边界索引上),但无论我使用什么排序,三角剖分总是会失败。

我们将最左边的顶点称为顶点 0,并按逆时针顺序排列顶点。这意味着箭头底部的 4 个顶点是顶点 1、2、5 和 6。我们有三个排序选项:

  1. 我用来实现算法的一些源材料说“在增加 y 坐标时对具有相等 x 坐标的顶点进行排序”,即:1、2、5、6。如果我这样做并扫描它们,第一个三角形出来没问题 (0, 1, 2),但之后算法会添加一条边 (5, 2),这会创建一个 4 顶点分量 (0, 2, 5, 6)。没有添加边 (0, 5),因为三角剖分算法规定将边添加到反射链上所有先前未三角剖分的顶点除了第一个(更改这将打破一般情况)。虽然由 4 个顶点界定的多边形区域是三角形的,但它显然不是三角形,因为它有 4 个点,而且根据大多数定义,它也不是有效的多边形,因为它有共线的边。

  2. 我读到的另一篇论文说“打破联系,以保持链顺序”。这意味着我的示例中的 4 个顶点将按 1、2、6、5 排序,因为下链和上链都是从左到右运行的。如果我按这个顺序扫描它们,我会再次得到一个三角形 (0, 1, 2),但是下一个扫描的顶点 (6) 将创建一个多边形 (0, 1, 6),这比第一种情况更糟糕,因为它创建了一条边 (1, 6),它在顶点 5 上运行但不包含它。扫描顶点 5 会完全弄乱算法的状态,因为它会创建一个退化三角形 (1, 5, 6),一条线,并且扫描尾部顶点将无法对多边形的其余部分进行三角剖分

    李>
  3. 按原始多边形顶点顺序(沿边界,逆时针)排序:在这种情况下,这将产生与情况 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 讲”章节)

我已经阅读了很多这些。几乎每一组幻灯片、论文、博客或算法的任何其他描述具体都提到了具有相等 x 坐标(或 y 坐标,如果使用水平扫描线)的顶点,但它们所有人 只是说“我们假设没有相等的 x 坐标”,并且这个限制“很容易固定,仅用于简化表示”或“不是算法的基础”或其他任何内容。奇怪的是,没有他们关心详细说明支持这种情况所需的更改或解决方法,或者它们包含一些关于以某种方式对相等的 x 顶点进行排序的模糊陈述,但实际上并不能解决问题。

也许我只是有点愚蠢或遗漏了一些非常明显的东西,但我已经搞砸了好几天试图修复这个角落案例但没有结果,它开始变得非常令人沮丧。我假设为基本案例实现算法(包括编写 DCEL 数据结构、扫描线算法、排序边缘图、确定内角和可见性所需的三角函数、有效存储和查找顶点分类的数据结构等) 几乎是所有工作,之后修复垂直边缘问题将是微不足道的。现在我花了更多时间尝试修复垂直边缘,而不是我需要让算法适用于一般情况的所有其他东西(只要它不适合我扔给它的任何多边形,它都可以完美工作' t 有垂直边缘)。

谢谢!沃特

【问题讨论】:

  • 经过轻微剪切变换的箭头是否正确(即 (x, y) |-> (x + epsilon y, y))?
  • 我问是因为处理退化 w.r.t 的常用技巧之一。扫描线是进行无限小的符号旋转,这相当于“按 y 排序”,如果 y 几乎平行的扫描线与点 (x, y) 相交,那么“左边”的点是那些按字典顺序小于 (x, y) 的点,并且“右边”的点更大。但是,如果剪切箭头导致退化的三角剖分,那么共线段就会出现更根本的问题。
  • @David Eisenstat:对具有相同 x 坐标的每个点应用轻微的剪切变换确实会导致“有效”三角剖分并防止算法进入无效状态。实际上,我至少曾经将其视为一种解决方案。我看到的问题是,这会添加额外的面积非常小的三角形。在“箭头”的情况下,这将是一个三角形 (2, 5, 6)。当然可以从结果中过滤掉这样的三角形,但我不确定是否有更好的解决方案可以直接产生适当的三角剖分。
  • 受约束的 Delaunay 三角剖分是一种可能,但也许比我更了解计算几何的人可能会提出一种替代方案,以重用您现有的更多代码库(平面图算法是我的专长)。

标签: algorithm graphics polygon computational-geometry triangulation


【解决方案1】:

我终于自己弄清楚了,所以我会回答我自己的问题,以供后代使用;-)

事实证明,使三角剖分算法适用于具有垂直边缘的多边形的更改很少,并且不需要特殊情况来处理它们。我必须更改以下内容:

  1. 在增加 y 坐标时对具有相等 x 坐标的顶点进行排序(注意:我使用垂直扫描线,对于水平扫描线,先在 y 上排序,然后在 x 上)
  2. 将具有入射垂直边的顶点分类为“合并”或“拆分”,而不是“常规上/下”(又名“上链/下链”)
  3. 当反射链的最后 2 个点与当前扫描线顶点共线时,切勿添加对角线

大多数关于算法的论文都提到了第一个要求。从下到上排序具有符号顺时针旋转的相同效果,就像 David Eisenstat 提到的那样。

我必须做出的第二个更改是因为我误解了各种顶点分类。我的假设是合并顶点应该总是有两个入射边完全在它的左边,一个分裂顶点完全在它的右边,这是不正确的。如果两条入射边中的一条是垂直的,另一条在顶点的左侧,则应归类为“合并”,如果另一条边在右侧,则应归类为“分裂”。特别是,我不得不改变以下几行:

// 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 个小时来解决这个问题,我有点沮丧,但至少我终于让这个愚蠢的事情起作用了。只希望我读过的关于这个特定算法的许多论文中至少有一篇能更明确地说明我在实现中遗漏的三件事:-/

【讨论】:

    【解决方案2】:

    在我们的扫描线引擎中,我们使用所有点之间的总字典顺序,其中我们认为y 坐标是主要 坐标,x 坐标是我次要 em> 坐标(在我们的例子中,我们从下到上的方向扫描)。在我们的实现中,为了概括对具有相同y 坐标的点的处理,我们假设位于输入队列后面的点(具有更大的x 坐标)同时位于稍微“更高”的位置。飞机的数量是无穷小的。显然,这是通过一些无穷小的剪切量进行剪切变换的概念变体,在 cmets 中已经提到过。

    而且,正如您在上面已经提到的,这种方法的副作用是,在一般情况下,它会导致在单调化阶段形成“不必要的”切割,如下所示

    尽管形状在垂直方向上已经是单调的,但算法发现有必要添加红色显示的切削刃(同样,在这种情况下,我们从下到上方向扫掠)。但是由于我们的最终目标是三角测量,所以这不是一个大问题。当然,如果您对最终三角剖分的质量有一些额外的限制,那么这些“早期”三角形可能会成为问题。

    在我的例子中,生成三角剖分是为了作为算法的输入,该算法执行初始三角剖分的动力学变换。为了稳健,动力学三角剖分算法无论如何都必须能够处理“坏”的针状三角形,因此我不对三角剖分的质量施加任何限制。

    【讨论】:

      猜你喜欢
      • 2017-04-09
      • 1970-01-01
      • 2012-09-28
      • 2015-03-19
      • 2012-05-06
      • 2011-01-14
      • 2011-10-05
      • 1970-01-01
      • 2011-07-12
      相关资源
      最近更新 更多