Delaunay Triangulation Study Notes

Delaunay Triangulation Study Notes

1 Voronoi \text{Voronoi} Voronoi picture

Before understanding Delaunay triangulation, introduce Voronoi \text{Voronoi}Voronoi picture.

1.1 Definition and properties

definition

Let any nnPoint set P = { pi ∣ i = 1 , . . . , n } P=\{p_i|i=1,...,n\} composed of n mutually different plane points (also called base points)P={ pii=1,...,n} P P The Voronoi \text{Voronoi}corresponding to PThe Voronoi diagram can be understood as a region (unit) division of the plane. For each region obtained by division (also calledVoronoi \text{Voronoi}Voronoi polygons), should satisfy: at the base pointpi p_ipiTake any point in the corresponding unit, and the point reaches pi p_ipiThe (Euclidean) distance must be less than to pj, pj ∈ P , j ≠ i p_j, p_j\in{P},j\ne{i}pjpjP,j=The (Euclidean) distance of i .

image-20230522154118839

Voronoi \text{Voronoi} Voronoi also known as Thiessen polygon orDirichlet \text{Dirichlet}Dirichlet diagram.

The V-graph on the plane can be regarded as the graph formed on the plane by each point in the data point set P as a growth point, which spreads outward at the same rate until they meet each other. Except for the outermost point which forms an open area, each other point forms a convex polygon.

some properties

  • Each Voronoi \text{Voronoi}Only one base point is included in the Voronoi polygon;
  • Voronoi \text{Voronoi}The (European) distance from any point in the Voronoi polygon to the corresponding base point is the shortest;
  • Place in Voronoi \text{Voronoi}The distance from the point on the side of the Voronoi polygon to the discrete points on both sides is equal;
  • n n Set PP of n pointsP Voronoi \text{Voronoi} Voronoi diagrams have at most2 n − 5 2n-52 n5 vertices and3 n − 6 3n-63 n6 sides;
  • If any four base points are not concentric, each Voronoi \text{Voronoi}Voronoi vertices are exactly threeVoronoi \text{Voronoi}The intersection of Voronoi edges, that is, byPPThe center of the circumcircle of the triangle formed by the three points in P.

reference:

[1] Gao Li. Research on Improved Delaunay Triangulation Algorithm [D]. Lanzhou Jiaotong University, 2015.

[2] Thiessen polygon_Baidu Encyclopedia (baidu.com)

2 Triangulation

2.1 Definition and properties

Let any nnPoint setP = { pi ∣ i = 1 , . . . , n } composed of n planar points P=\{p_i|i=1,...,n\ }P={ pii=1,...,n } . Triangulation refers toP i P_inon-intersectingstraight line segmentsPiwith P j P_jPj 1 ≤ i , j ≤ n , i ≠ j 1≤i,j≤n,i≠j 1ijni=j , and make each partition in the convex hull a triangle.

Since the triangulation is a planar graph, the following conditions are met:

  • There are no intersecting edges in the graph (except for line segment endpoints, there are no overlapping edges);
  • The edges in the graph do not contain the set PPany other point in P , except endpoints;
  • All the patches in the figure are triangles, and the set of all triangles constitutes PPThe convex hull of P.

image-20230523011544467

2.2 Quality Evaluation Standards

Since the triangulation for a given point set is not unique, we have the following quality evaluation criteria for different triangulations:

Quality Evaluation Standards explain
minimum angle The smallest angle among all the interior angles of a triangle
aspect ratio The ratio of the shortest side to the longest side of a triangle
radius ratio The ratio of twice the radius of the inscribed circle of a triangle to the radius of the circumscribed circle

reference:

[1] Technology Sharing: Introduction to Delaunay Triangulation Algorithm - Zhihu (zhihu.com)

3 Delaunay triangulation

Delaunay partitioning is a standard for triangulation.

3.1 Definition

Delaunay Edge : Assuming EEEEEE is the edge set of the triangulation of the point set) in an edgeeee (two endpoints area, ba, ba,b),eeThe condition for e to be a Delaunay side is: there is a circle passing througha, ba, ba,b Two points, inside the circle (up to three points in a circle) does not contain point setPPAny other point in P , this feature is also called the empty circle feature.

Delaunay triangulation : if the point set PPA triangulationTT of PT contains only Delaunay edges, then the triangulation is called a Delaunay triangulation.

Another definition of Delaunay triangulation : because Delaunay triangulation and Voronoi \text{Voronoi}The Voronoi diagram is a duality relation, and the Delaunay triangulation isVoronoi \text{Voronoi}The accompanying graphics of the Voronoi diagram, the two can be transformed into each other. MakeVoronoi \text{Voronoi}The dual graph of the Voronoi graph, that is, for each Voronoi \text{Voronoi}The Voronoi edge (limited to a finite line segment) is used as the perpendicular line passing through a certain two points in the point set, and the obtained is the Delaunay triangulation.

image-20230524001225163

3.2 Criterion and nature

A triangulation must meet the following two important criteria before it can be called a Delaunay triangulation.

  • Empty Circle Properties

    The Delaunay triangulation is unique (any four points cannot be in the same circle), and there will be no other points in the circumcircle of any triangle in the Delaunay triangulation. That is, it satisfies the definition of a Delaunay edge.

  • Maximize the Minimum Angle Property

    In point set PPOf all the possible triangulations of P , the Delaunay triangulation forms the triangle withminimum angle.

Delaunay triangulation has the following important properties:

(1) Uniqueness : No matter where the network is built from any point set, the resulting Delaunay triangulation is unique.

(2) Closest : The triangle is composed of the nearest three points, and the sides of the formed triangle will not intersect.

(3) The most regular : If the minimum angle of each triangle in the triangulation is arranged in ascending order, then the value obtained by the arrangement of the Delaunay triangulation is the largest.

(4) Optimality : If the diagonals of a convex quadrilateral formed by any two adjacent triangles are exchanged, the smallest angle among the six interior angles of the two triangles will not increase after the exchange.

(5) Regional : When moving, adding, or deleting a vertex in the triangulation, only adjacent triangles will be affected.

(6) Hull with convex polygons : In the constructed triangulation, the outermost boundary constitutes the convex polygonal "hull" of the point set (ie, the convex hull of the point set).

4 Delaunay triangulation algorithm

The algorithm of Delaunay triangulation can be divided into point-by-point insertion method, triangulation growth method, divide and conquer algorithm and so on. The divide-and-conquer algorithm has the highest efficiency, and the point-by-point insertion method is simple and efficient, and takes up less memory, but its time complexity is poor. Due to the relatively low efficiency of the triangular network growth method, it is rarely used at present.

image-20230525011734005

4.1 Bowyer-Watson Algorithm

The Bowyer-Watson algorithm is a kind of point-by-point interpolation method, which is easy to understand and implement.

4.1.1 Algorithm steps:

Step1: Construct a super triangle (super-triangle) containing all points in the point set, and put it into the triangle list;

This triangle list can be understood as a triangulation, which currently contains only one triangle, the super triangle.

Step2: Insert the points in the point set into the existing triangulation one by one, and make the following adjustments:

  • In the triangle list, find all the triangles whose circumcircle contains the insertion point (called the influence triangle of the point), and the collection of all influence triangles constitutes a "star shaped polygon". The meaning of a star polygon is that the line connecting any vertex of the polygon to the insertion point is inside the polygon.

  • For the above star polygon, delete all the triangles inside it to form a "cavity". Connect the vertices of the hole boundary with the newly inserted point to obtain a new triangle, which replaces the deleted triangle in the subdivision, thereby completing the insertion of a point in the Delaunay triangle list, and obtaining a new Delaunay triangulation containing the insertion point.

Another way of saying is: in the triangle list, find out the triangle whose circumcircle contains the insertion point (called the influence triangle of the point), delete the common side of the influence triangle, and connect the insertion point with all the vertices of the influence triangle, Thus completing the insertion of a point in the list of Delaunay triangles. The diagram for this step is as follows:

img

Step3: Perform Step2 in a loop until all points are inserted.

Step4: Finally, delete the triangle associated with the super triangle from the triangle list to obtain the Delaunay triangulation of the point set.

4.1.2 Algorithm Pseudocode

version one

from Triangulate: Pan Pacific Computer Conference, Beijing, China (paulbourke.net)

input : vertex list
output : triangle list
   initialize the triangle list
   determine the supertriangle
   add supertriangle vertices to the end of the vertex list
   add the supertriangle to the triangle list
   for each sample point in the vertex list
      initialize the edge buffer
      for each triangle currently in the triangle list
         calculate the triangle circumcircle center and radius
         if the point lies in the triangle circumcircle then
            add the three triangle edges to the edge buffer
            remove the triangle from the triangle list
         endif
      endfor
      delete all doubly specified edges from the edge buffer
         this leaves the edges of the enclosing polygon only
      add to the triangle list all triangles formed between the point 
         and the edges of the enclosing polygon
   endfor
   remove any triangles from the triangle list that use the supertriangle vertices
   remove the supertriangle vertices from the vertex list
end

version two

from Bowyer–Watson algorithm - Wikipedia.

function BowyerWatson (pointList)
    // pointList is a set of coordinates defining the points to be triangulated
    triangulation := empty triangle mesh data structure
    add super-triangle to triangulation // must be large enough to completely contain all the points in pointList
    for each point in pointList do // add all the points one at a time to the triangulation
        badTriangles := empty set
        for each triangle in triangulation do // first find all the triangles that are no longer valid due to the insertion
            if point is inside circumcircle of triangle
                add triangle to badTriangles
        polygon := empty set
        for each triangle in badTriangles do // find the boundary of the polygonal hole
            for each edge in triangle do
                if edge is not shared by any other triangles in badTriangles
                    add edge to polygon
        for each triangle in badTriangles do // remove them from the data structure
            remove triangle from triangulation
        for each edge in polygon do // re-triangulate the polygonal hole
            newTri := form a triangle from edge to point
            add newTri to triangulation
    for each triangle in triangulation // done inserting points, now clean up
        if triangle contains a vertex from original super-triangle
            remove triangle from triangulation
    return triangulation

4.1.3 Algorithm Explanation Example

Here we take the four points A, B, C, and D as an example to draw a picture to illustrate the flow of the entire Bowyer-Watson algorithm.

  • First create a super triangle (p1, p2, p3), this triangle should include all the points in the point set. Add this super triangle to the list of triangles.

    image-20230525133130002

    See [4.1.5 Implementation Details] (#4.1.5 Implementation Details) for details on how to construct a super triangle.

  • We insert point A first, because the only circumscribed circle of the super triangle in the original triangle list must contain point A, so we can regard the triangle ▲(p1,p2,p3) as a star polygon with no triangle inside, so we Directly connect point A to its vertices, and split the original triangle into three triangles (▲(p1,A,p2), ▲(p2,A,p3), ▲(p3,A,p1)).

    image-20230525135633264

  • Then insert point B, and make the circumcircle of each triangle in the triangle list, we have found the influence triangle of point B, namely ▲(p1,A,p3), ▲(p2,A,p3).

    image-20230525140915264

  • Delete triangles (common side, here side (A,p3)) inside the star polygon that affect the triangle composition. Then connect point B to all vertices of the influence triangle.

    image-20230525141540743

  • Next, insert point C, the same as the process of inserting point B above, find the influence triangle, delete the common edge, and connect the vertices of the influence triangle.

    image-20230525142157546

  • Then insert point D in the same way.

    image-20230525142614560

  • Finally delete the triangle associated with the super triangle vertex (p1,p2,p3).

    image-20230525142914270

  • The final triangulation results of points A, B, C, and D are shown in the figure below.

    image-20230525143023480

4.1.4 Algorithm optimization

The more time-consuming step of the Bowyer-Watson algorithm is the positioning of the new insertion point. In the above, we traverse all the triangles in the triangle list, and then judge whether the insertion point falls within its circumcircle. As the size of the point set increases, the triangle list will gradually increase during the construction of the Delaunay triangulation, and the time consumption of inserting point positioning will also increase accordingly. Therefore, there are many optimized and improved algorithms for the point positioning problem.

(1) Sorting optimization

The optimization idea of ​​the algorithm is as follows: First, sort the points in the original point set according to the x coordinates from small to large. When inserting, it is no longer inserted randomly, but in sorted order. It is guaranteed that the newly inserted point will not appear to the left of the previously inserted point. There is another difference. In addition to the list of point sets, the algorithm also has a list of determined triangles and a list of undetermined triangles. Every time the insertion point is positioned, it is only necessary to perform positioning calculations for the new insertion point in the undetermined triangle list, instead of querying all generated triangles. If the insertion point is on the right side of the circumcircle of the query triangle, it means that the query triangle is a legal Delaunay triangle, and it is moved to the determined triangle list; if it is outside the circumcircle and not on the right side, it means that the query triangle is still An undetermined triangle, no operation is performed; if it is inside the circumcircle, it means that the query triangle is not a Delaunay triangle, and it is removed from the list of undetermined triangles.

pseudocode:

input: 顶点列表(vertices)                                      //vertices为外部生成的随机或乱序顶点列表
output:已确定的三角形列表(triangles)
    初始化顶点列表
    创建索引列表(indices = new Array(vertices.length))    //indices数组中的值为0,1,2,3,......,vertices.length-1
    基于vertices中的顶点x坐标对indices进行sort           //sort后的indices值顺序为顶点坐标x从小到大排序(也可对y坐标,本例中针对x坐标)
    确定超级三角形
    将超级三角形保存至未确定三角形列表(temp triangles)
    将超级三角形push到triangles列表
    遍历基于indices顺序的vertices中每一个点            //基于indices后,则顶点则是由x从小到大出现
      初始化边缓存数组(edge buffer)
      遍历temp triangles中的每一个三角形
        计算该三角形的圆心和半径
        如果该点在外接圆的右侧
          则该三角形为Delaunay三角形,保存到triangles
          并在temp里去除掉
          跳过
        如果该点在外接圆外(即也不是外接圆右侧)
          则该三角形为不确定                      //后面会在问题中讨论
          跳过
        如果该点在外接圆内
          则该三角形不为Delaunay三角形
          将三边保存至edge buffer
          在temp中去除掉该三角形
      对edge buffer进行去重
      将edge buffer中的边与当前的点进行组合成若干三角形并保存至temp triangles中
    将triangles与temp triangles进行合并
    除去与超级三角形有关的三角形
end

detail:

When the insertion point is on the right side of the circumcircle of the query triangle. Since the insertion points are inserted from left to right according to the size of the x coordinate, the remaining points must be on the right side of the circumcircle, that is, the query triangle satisfies the empty circle property and is a Delaunay triangle.

img

In the circumscribed circle of the query triangle, when inserting point 1, it meets the condition of being outside, but it cannot guarantee that all subsequent points will remain outside the circumscribed circle. As shown in the figure, point 2 and the points that may appear later are likely to appear in the circle, so that the triangle is decomposed by sides. So in this optimization algorithm, if the point is on the outside and not on the right side, it will be skipped, and the triangle will be checked in the temp triangles until the next point is inside the circle or on the right side of the circle. Make sure to remove it from the triangle list, and proceed to the following operations.

When the point is on the circle, it is also operated according to the method of being inside the circle. This situation will appear in the actual situation, and this phenomenon is called "degeneration".

img

reference:

[1] Triangulation Algorithm (delaunay) - Paper Strange Beast - Blog Garden (cnblogs.com)

[2] darkskyapp/delaunay-fast: Fast Delaunay Triangulation in JavaScript. (github.com)

(2) Fast point positioning

ALL:

reference:

[1] Computational Geometry Learning - Point Positioning_Mathematic_feng's Blog-CSDN Blog

[2] Liu Qinqin. A Review of the Algorithm for Delaunay Triangulation in the Plane Domain [J]. Electronic Design Engineering, 2017, 25(01): 47-51. DOI: 10.14022/j.cnki.dzsjgc.2017.01.012.

[3] Berger, Deng Junhui. Computational Geometry: Algorithms and Applications (3rd Edition) [M]. Tsinghua University Press, 2009.

4.1.5 Implementation Details

(1) Construct a super triangle

method one:

image-20230525231438652

The green quadrilateral is the bounding box of the point set.

Method Two:

img

According to the similar triangle theorem, the diagonal triangle of the small rectangle that is half of the rectangle is obtained, and after being doubled, the hypotenuse of the enlarged right triangle passes through the point (Xmax, Ymin). In order to include all the points in the super triangle, the vertices of the triangle are extended horizontally and high at the lower right corner, and it is necessary to ensure that the base of the extended triangle is greater than the height.

Reference from: Triangulation Algorithm (delaunay) - Paper Strange Beast - Blog Garden (cnblogs.com)

Other methods:

The initialization figure of the above two methods is a triangle. In addition, some scholars proposed to use an initial rectangle containing all points, and then connect any diagonal of the rectangle to divide it into two triangles and add it to the initial triangulation list.

image-20230525232322324

In the figure above, K \text{K}K is a positive displacement value.

(2) Calculate the center and radius of the circumscribed circle of the triangle

reference:

https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcircle_equations

https://en.wikipedia.org/wiki/Circumscribed_circle#Circumcenter_coordinates

After calculating the coordinates of the center of the circumscribed circle, the distance between the center of the circle and any vertex of the triangle is the radius of the circumscribed circle.

If the three points are collinear, then the center of the circumscribed circle will be at infinity.

4.1.6 Code implementation

First define the data structures of points, edges, and triangles.

from typing import List

class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def dist2(self, other) -> float:
        return (self.x - other.x) * (self.x - other.x) + (self.y - other.y) * (self.y - other.y)

    def isInCircumcircleOf(self, t) -> bool:
        A, B, C = t.verticies[0], t.verticies[1], t.verticies[2]
        a2 = A.x * A.x + A.y * A.y
        b2 = B.x * B.x + B.y * B.y
        c2 = C.x * C.x + C.y * C.y
        D = 2.0 * (A.x * (B.y - C.y) + B.x * (C.y - A.y) + C.x * (A.y - B.y))
        center_x = (a2 * (B.y - C.y) + b2 * (C.y - A.y) + c2 * (A.y - B.y)) / D
        center_y = (a2 * (C.x - B.x) + b2 * (A.x - C.x) + c2 * (B.x - A.x)) / D
        center = Point(center_x, center_y)
        return self.dist2(center) <= A.dist2(center)

class Edge:
    def __init__(self, begin: Point, end: Point):
        self.begin = begin
        self.end = end

    def __eq__(self, other):
        return (self.begin == other.begin and self.end == other.end) or (
                self.begin == other.end and self.end == other.begin)

class Triangle:
    def __init__(self, A: Point, B: Point, C: Point):
        self.verticies = [A, B, C]
        self.edges = [Edge(A, B), Edge(B, C), Edge(C, A)]

Then define a class for delaunay triangulation.

class Delaunay_Triangulation:
    """Bowyer Watson Algorithm"""

    def __init__(self, points: List[Point]):
        self.points: List[Point] = points
        self.super_triangle = self.getSuperTriangle()
        self.triangles: List[Triangle] = [self.super_triangle]  # add super-triangle to triangulation
        for point in points:  # add all the initial points one at a time to the triangulation
            self.addPoint(point)
        self.removeSuperTriangle()

    def getSuperTriangle(self) -> Triangle:
        sorted_x = sorted(self.points, key = lambda p: p.x)
        sorted_y = sorted(self.points, key = lambda p: p.y)
        xmin, xmax = sorted_x[0].x, sorted_x[-1].x
        ymin, ymax = sorted_y[0].y, sorted_y[-1].y
        dx, dy = xmax - xmin, ymax - ymin
        dmax = max(dx, dy)
        xmid = xmin + dx * 0.5
        ymid = ymin + dy * 0.5

        return Triangle(Point(xmid - 20 * dmax, ymid - dmax),
                        Point(xmid, ymid + 20 * dmax),
                        Point(xmid + 20 * dmax, ymid - dmax))

    def addPoint(self, point: Point) -> None:
        bad_triangles: List[Triangle] = []
        # first find all the triangles that are no longer valid due to the insertion
        for triangle in self.triangles:
            if point.isInCircumcircleOf(triangle):
                bad_triangles.append(triangle)

        polygon: List[Edge] = []
        # Find the boundary of the polygonal hole
        for triangle1 in bad_triangles:
            for edge in triangle1.edges:
                edge_shared = False
                for triangle2 in bad_triangles:
                    if triangle1 == triangle2:
                        continue
                    if edge in triangle2.edges:
                        edge_shared = True
                if not edge_shared:
                    polygon.append(edge)

        # Remove broken triangles from the triangulation list
        for triangle in bad_triangles:
            self.triangles.remove(triangle)

        # Create triangles with the newly created edges
        for edge in polygon:
            new_triangle = Triangle(edge.begin, edge.end, point)
            self.triangles.append(new_triangle)

    def removeSuperTriangle(self) -> None:
        # Remove the triangles that has connection to the super-triangle
        super_verticies = self.super_triangle.verticies
        for i in range(len(self.triangles) - 1, -1, -1):
            triangle = self.triangles[i]
            has_common = False
            for vertex1 in triangle.verticies:
                for vertex2 in super_verticies:
                    if vertex1 == vertex2:
                        has_common = True
            if has_common:
                self.triangles.remove(triangle)

    def exportTriangles(self):
        ps = [p for t in self.triangles for p in t.verticies]
        xs = [p.x for p in ps]
        ys = [p.y for p in ps]
        ts = [(ps.index(t.verticies[0]), ps.index(t.verticies[1]), ps.index(t.verticies[2])) for t in self.triangles]
        return xs, ys, ts

The exportTrianglesfunction is used to export the data required by the matplotlib library to draw the subdivision results. The algorithm test code is as follows:

from random import randint, seed
from Delaunay_Triangulation import Delaunay_Triangulation, Point
import time

if __name__ == '__main__':
    seed(5)
    n = 10
    xs = [randint(1, 98) for x in range(n)]
    ys = [randint(1, 98) for x in range(n)]
    seted_points = set(zip(xs, ys))
    print("The actual number of input points: ", len(seted_points))
    points = [Point(x, y) for x, y in seted_points]
    start_time = time.time()
    dt = Delaunay_Triangulation(points)
    print(f"Triangulating {
      
      len(seted_points)} points takes {
      
      time.time() - start_time} s")
    # number of DT triangles
    print(len(dt.triangles), "Delaunay triangles")

    import matplotlib.pyplot as plt
    import matplotlib.tri as tri

    # Plot the triangulation.
    fig, ax = plt.subplots()
    ax.margins(0.1)
    ax.set_aspect('equal')
    xs1, ys1, ts = dt.exportTriangles()
    ax.triplot(tri.Triangulation(xs1, ys1, ts), 'bo-')
    triang = tri.Triangulation(xs, ys)
    ax.triplot(triang, 'ro-')
    ax.set_title('triplot of Delaunay triangulation')
    plt.show()

Among them seted_points = set(zip(xs, ys)), it is used to remove duplicate points in the randomly generated point set to meet the requirements of different coordinates of the input point set. Otherwise, if the coordinates of the two randomly generated points are the same, when calculating the circumcircle of the point, in the following code, D is 0, and ZeroDivisionError: division by zeroan exception will be thrown.

D = 2.0 * (A.x * (B.y - C.y) + B.x * (C.y - A.y) + C.x * (A.y - B.y))
center_x = (a2 * (B.y - C.y) + b2 * (C.y - A.y) + c2 * (A.y - B.y)) / D
center_y = (a2 * (C.x - B.x) + b2 * (A.x - C.x) + c2 * (B.x - A.x)) / D

The function of the following two lines of code is to call matplotlib.tri.Triangulationthe build triangulation. The correctness of the algorithm can be verified by drawing the code we wrote and the result of triangulation by calling the third-party library function in two different colors.

triang = tri.Triangulation(xs, ys)
ax.triplot(triang, 'ro-')

The test results are as follows:

image-20230528205450735

After testing, it is found matplotlib.tri.Triangulationthat the triangulation obtained by the function will be different from the triangulation result of this algorithm when the number of randomly generated point sets is relatively large.

matplotlib.tri.TriangulationFunction analysis result:

image-20230528205916411

The dissection result of the class we wrote Delaunay_Triangulation:

image-20230528205904484

The reason for this kind of problem, I guess, may be because some four points are in the same circle.

In the properties of Delaunay triangulation, if the point set PPAny four points in P are not co-circular, then there is a unique Delaunay triangulationTTT. _ If the point setPPFour pointsA , B , C , DA,B,C,D in PA,B,C,D share circles, and △ABC, △BCD belong to Delaunay triangulation T, then the sideBC BCThe resulting triangulationT'T' after BC flippingT ' (including △ABD, △ACD) is also a Delaunay triangulation.

Technology Sharing: Introduction to Delaunay Triangulation Algorithm - Zhihu (zhihu.com)

By inputting a series of concentric points, the result shown in the figure below is obtained. Both belong to the Delaunay triangulation.

image-20230529004839957

In terms of efficiency, three sets of data were tested. The result is as follows:

Point set size/piece time-consuming/s
1000 4
4000 60
6000 150

The time complexity is about O ( n 2 ) O(n^2)O ( n2) n n n is the point set size.

4.2 Divide and conquer

references:

[1] Guibas L, Stolfi J. Primitives for the manipulation of general subdivisions and the computation of Voronoi[J]. ACM Transactions on Graphics, 1985, 4(2): 74–123.

L. Guibas and J. Stolfi proposed the Quad-Edge data structure and used it to simplify the Delaunay triangulation divide-and-conquer algorithm proposed by Shamos and Hoey in 1975. In the above references, the author devoted a section to the introduction of divide and conquer triangulation, with detailed pseudo-code.

4.2.1 Quad-Edge

Quad-Edge Data Structure and Library (cmu.edu)

img

class Vertex:
    def __init__(self, x, y, _id = None):
        self.id = _id
        self.x = x
        self.y = y

        self.name = f'v_{
      
      self.id}'  # for debugging

class Quad_Edge:
    """A directed edge: org -> dest.
    When traversing edge ring: Next is CCW, Prev is CW."""
    def __init__(self, org, dest):
        self.org = org  # Origin
        self.dest = dest  # Destination
        self.onext = None  # next edge around origin,with same origin
        self.oprev = None  # prev edge around origin,with same origin
        self.sym = None  # edge pointing opposite dest this edge
        self.deleted = False  # Deleted flag

        self.name = f'e_{
      
      self.org.id}_{
      
      self.dest.id}'  # for debugging

In divide-and-conquer triangulation we use Quad-Edge as the edge data structure. Below we describe some topological operations on edges.

Reference: https://github.com/alexbaryzhikov/triangulation

def create_edge(org, dest, edges) -> Quad_Edge:
    """
    Creates an edge, add it dest edges, and return it.
    """
    e = Quad_Edge(org, dest)
    es = Quad_Edge(dest, org)
    e.sym, es.sym = es, e  # make edges mutually symmetrical
    e.onext, e.oprev = e, e
    es.onext, es.oprev = es, es
    edges.append(e)
    return e

def update_next_prev(e1: Quad_Edge, e2: Quad_Edge):
    """
    Either combines e1 and e2 into a single edge, or seperates them.
    Which one is determined by the orientation of e1 and e2.
    """
    if e1 == e2:
        return
    e1.onext.oprev = e2
    e2.onext.oprev = e1
    # Swap a.onext and b.onext
    e1.onext, e2.onext = e2.onext, e1.onext

def connect(e1: Quad_Edge, e2: Quad_Edge, edges) -> Quad_Edge:
    """
    Connecting destination of e1 with the origin of e2 with an edge
    O(1) time and O(1) space
    """
    e = create_edge(e1.dest, e2.org, edges)
    # Maintain the onext and oprev values
    update_next_prev(e, e1.sym.oprev)
    update_next_prev(e.sym, e2)
    return e

def mark_edge_deleted(e: Quad_Edge):
    """
    Delete edge from the edge list
    O(1) time and O(1) space
    """
    # Update the e.onext' and e.oprev's values
    update_next_prev(e, e.oprev)
    update_next_prev(e.sym, e.sym.oprev)
    # Mark the edge dest be deleted
    e.deleted = True
    e.sym.deleted = True
(1)create_edge(org, dest, edges)

This function is used to orgreturn destthe edge that is the starting point and the end point (the edges mentioned here and in [this section](#4.2.1 Quad-Edge) all refer to Quad-Edge), and put this edge into the edgeslist . We initialized the symmetric edge of the newly generated edge in this function, and correctly initialized the sum property of the new edge onextand its symmetric oprevedge, because the new edge is not topologically connected to any other edge, so these two properties are the two edge itself. For specific implementation details, see the operation proposed in the article [1]MakeEdge .

(2)update_next_prev(e1, e2)

This function is used to splice the e1 side and the e2 side, essentially updating onextthe sum oprevattribute of the two sides. For specific implementation details, see the operation proposed in the article [1]Splice .

(3)connect(e1, e2, edges)

This function uses a new edge to connect edge e1 and edge e2, and updates the topological relationship among the three. For specific implementation details, see the operation proposed in the article [1]Connect .

(4)mark_edge_deleted(e)

This function is used to delete an edge, essentially changing the topological relationship between the deleted edge and other edges. For specific implementation details, see the operation proposed in the article [1]DeleteEdge .

4.2.2 Tool algorithm

(1) Determine whether the point is within the circumcircle of the triangle

point ddd in triangle( a , b , c ) (a,b,c)(a,b,C )外接圆圆圆圆圆圆圆圆ó如丌灭式:
under = ∣ Axayax 2 + Ay 2 1 bxbybx 2 + By 2 1 Cxcycx 2 + 2 2 2 2 2 > ∣ 2 > ∣ 2 2 2 > ∣ 2 2 2 2 .) Vmatrix }a_x&a_y&a_x^2+a_y^2&1\\[0.3em]b_x&b_y&b_x^2+b_y^2&1\\[0.3em]c_x&c_y&c_x^2+c_y^2&1\\[0.3em]d_x&d_y&d_x^2+d_y^2&1\end{ vmatrix}>0\tag{4-1}ret= axbxcxdxaybycydyax2+ay2bx2+by2cx2+cy2dx2+dy21111 >0(4-1)
行列式每行减去第四行,得:
r e t = ∣ a x − d x a y − d y a x 2 + a y 2 − ( d x 2 + d y 2 ) 0 b x − d x b y − d y b x 2 + b y 2 − ( d x 2 + d y 2 ) 0 c x − d x c y − d y c x 2 + c y 2 − ( d x 2 + d y 2 ) 0 d x d y d x 2 + d y 2 1 ∣ > 0 (4-2) ret =\begin{vmatrix}a_x-d_x&a_y-d_y&a_x^2+a_y^2-(d_x^2+d_y^2)&0\\[0.3em]b_x-d_x&b_y-d_y&b_x^2+b_y^2-(d_x^2+d_y^2)&0\\[0.3em]c_x-d_x&c_y-d_y&c_x^2+c_y^2-(d_x^2+d_y^2)&0\\[0.3em]d_x&d_y&d_x^2+d_y^2&1\end{vmatrix}>0\tag{4-2} ret= axdxbxdxcxdxdxaydybydycydydyax2+ay2(dx2+dy2)bx2+by2(dx2+dy2)cx2+cy2(dx2+dy2)dx2+dy20001 >0(4-2)
简化得到下式:
r e t = ∣ a x − d x a y − d y a x 2 + a y 2 − ( d x 2 + d y 2 ) b x − d x b y − d y b x 2 + b y 2 − ( d x 2 + d y 2 ) c x − d x c y − d y c x 2 + c y 2 − ( d x 2 + d y 2 ) ∣ > 0 (4-3) ret =\begin{vmatrix}a_x-d_x&a_y-d_y&a_x^2+a_y^2-(d_x^2+d_y^2)\\[0.3em]b_x-d_x&b_y-d_y&b_x^2+b_y^2-(d_x^2+d_y^2)\\[0.3em]c_x-d_x&c_y-d_y&c_x^2+c_y^2-(d_x^2+d_y^2)\end{vmatrix}>0\tag{4-3} ret= axdxbxdxcxdxaydybydycydyax2+ay2(dx2+dy2)bx2+by2(dx2+dy2)cx2+cy2(dx2+dy2) >0( 4-3 )
Next, add− 2 dx -2d_x-2 d _xtimes, and then organize the elements in the third column into the form of the square difference to get the following formula:
ret = ∣ ax − dxay − dy ( ax − dx ) 2 + ( ay − dy ) 2 bx − dxby − dy ( bx − dx ) 2 + ( by − dy ) 2 cx − dxcy − dy ( cx − dx ) 2 + ( cy − dy ) 2 ∣ > 0 (4-4) ret =\begin{vmatrix}a_x-d_x&a_y-d_y&(a_x-d_x) ^2+(a_y-d_y)^2\\[0.3em]b_x-d_x&b_y-d_y&(b_x-d_x)^2+(b_y-d_y)^2\\[0.3em]c_x-d_x&c_y-d_y&(c_x- d_x)^2+(c_y-d_y)^2\end{vmatrix}>0\tag{4-4}ret= axdxbxdxcxdxaydybydycydy(axdx)2+(aydy)2(bxdx)2+(bydy)2(cxdx)2+(cydy)2 >0(4-4)
形如 a d x = a x − d x ad_x=a_x-d_x adx=axdxReplace the same elements to get the following formula:
ret = ∣ adxadyadx 2 + ady 2 bdxbdybdx 2 + bdy 2 cdxcdycdx 2 + cdy 2 ∣ > 0 (4-5) ret =\begin{vmatrix}ad_x&ad_y&ad_x^2+ad_y^2\\ [0.3em]bd_x&bd_y&bd_x^2+bd_y^2\\[0.3em]cd_x&cd_y&cd_x^2+cd_y^2\end{vmatrix}>0\tag{4-5}ret= adxbdxcdxadybdycdyadx2+ady2bdx2+bdy2cdx2+cdy2 >0(4-5)

def inCircle(a: Vertex, b: Vertex, c: Vertex, d: Vertex) -> bool:
    """判断点d是否在由a,b,c构成的三角形外接圆内"""
    adx = a.x - d.x
    ady = a.y - d.y
    bdx = b.x - d.x
    bdy = b.y - d.y
    cdx = c.x - d.x
    cdy = c.y - d.y

    alift = adx * adx + ady * ady
    blift = bdx * bdx + bdy * bdy
    clift = cdx * cdx + cdy * cdy
    
    return alift * (bdx * cdy - cdx * bdy) + blift * (cdx * ady - adx * cdy) + clift * (adx * bdy - bdx * ady) > 0
(2) Determine the relative position of the point and the edge

The starting point of the edge aaa , end pointbbb , the input point isppp
it = ∣ axay 1 bxby 1 pxpy 1 ∣ (4-6) it=\begin{vmatrix}a_x&a_y&1\\[0.3em]b_x&b_y&1\\[0.3em]p_x&p_y&1\end{vmatrix}\tag{4-6 } }d e t= axbxpxaybypy111 (4-6)

d e t = ( a x − p x ) ( b y − p y ) − ( a y − p y ) ( b x − p x ) (4-7) det=(a_x-p_x)(b_y-p_y)-(a_y-p_y)(b_x-p_x)\tag{4-7} d e t=(axpx)(bypy)(aypy)(bxpx)(4-7)

  • d e t > 0 det>0 d e t>0 p p p is to the left of the edge;
  • d e t < 0 det<0 d e t<0 p p p is to the right of the edge;
  • it = 0 it=0d e t=0 p p p is collinear on the edge.
def left_test(p, e):
    """
    Left test for point p relative to the line of edge e.
    """
    a, b = e.org, e.sym.org
    det1 = (a.x - p.x) * (b.y - p.y)
    det2 = (a.y - p.y) * (b.x - p.x)
    return det1 - det2


def toRight(p, e) -> bool:
    """Does point p lie to the right of the line of edge e?"""
    return left_test(p, e) < 0


def toLeft(p, e) -> bool:
    """Does point p lie to the left of the line of edge e?"""
    return left_test(p, e) > 0

4.2.3 Main program

Algorithm pseudocode:

image-20230606140652092

Main program:

class Divide_Delaunay:
    """Triangulate the points using the divide and conquer delaunay triangulation algorithm.
    """

    def __init__(self, points):
        self.points = points
        self.verticies = []
        self.init_points()  # 初始化点集
        self.edges = []
        self.div_and_conq_triangulate(self.verticies)  # 分治法构造三角网

        # Remove edges that are not part of the triangulation
        self.edges = [e for e in self.edges if e.deleted is False]

    def init_points(self):
        # Validate the input size
        if len(self.points) < 2:
            return

        # Sort points by x coordinate, y is a tiebreaker
        self.points.sort(key = lambda point: (point[0], point[1]))

        # Remove duplicates
        i = 0
        while i < len(self.points) - 1:
            if self.points[i] == self.points[i + 1]:
                del self.points[i]
            else:
                i += 1

        # Vertex naming
        for i, point in enumerate(self.points):
            self.verticies.append(Vertex(point[0], point[1], i))

    def div_and_conq_triangulate(self, points):
        """
        Computes the Delaunay triangulation of self.points and returns two edges, le and re,
        which are the counterclockwise convex hull edge out of the leftmost vertex and the clockwise
        convex hull edge out of the rightmost vertex, respectively.
        """
        n = len(points)
        # Base case: 2 points
        if n == 2:
            edge = create_edge(points[0], points[1], self.edges)
            return edge, edge.sym

        # Base case: 3 points
        elif n == 3:
            # Create edge S[0]-S[1] and edge S[1]-S[2]
            edge1 = create_edge(points[0], points[1], self.edges)
            edge2 = create_edge(points[1], points[2], self.edges)
            update_next_prev(edge1.sym, edge2)

            # Create edge S[2]-S[0]
            if toRight(points[2], edge1):  # Right
                connect(edge2, edge1, self.edges)
                return edge1, edge2.sym
            elif toLeft(points[2], edge1):  # Left
                edge3 = connect(edge2, edge1, self.edges)
                return edge3.sym, edge3
            else:  # Points are linear
                return edge1, edge2.sym

        # Recurively triangulate the left and right halves
        else:
            m = n // 2
            ldo, ldi = self.div_and_conq_triangulate(points[:m])
            rdi, rdo = self.div_and_conq_triangulate(points[m:])
            ldo_r, rdo_r = self.merge(ldo, ldi, rdi, rdo)

            return ldo_r, rdo_r

    def merge(self, ldo, ldi, rdi, rdo):
        """
        Takes 2 halves of the triangulation and merges them into a single triangulation.
        While doing so it uses previosly calculated values of these halves.
        Reference: https://github.com/alexbaryzhikov/triangulation
        """
        # Compute the upper common tangent of L and R.
        while True:
            if toRight(rdi.org, ldi):
                # Advance dest the next edge on the convex hull of L.
                ldi = ldi.sym.onext
            elif toLeft(ldi.org, rdi):
                # Advance dest the next edge on the convex hull of R.
                rdi = rdi.sym.oprev
            else:
                break

        # Create a first cross edge base.
        base = connect(ldi.sym, rdi, self.edges)

        # Adjust ldo and rdo
        if ldi.org.x == ldo.org.x and ldi.org.y == ldo.org.y:
            ldo = base
        if rdi.org.x == rdo.org.x and rdi.org.y == rdo.org.y:
            rdo = base.sym

        # Merge two halves
        while True:
            # Locate the first R and L points dest be encountered by the diving bubble.
            rcand, lcand = base.sym.onext, base.oprev

            # If both lcand and rcand are invalid, then base is the lower common tangent.
            v_rcand, v_lcand = toRight(rcand.dest, base), toRight(lcand.dest, base)
            if not (v_rcand or v_lcand):
                break

            # Delete R edges out of base.dest that fail the circle test.
            if v_rcand:
                while toRight(rcand.onext.dest, base) and inCircle(base.dest, base.org, rcand.dest, rcand.onext.dest):
                    t = rcand.onext
                    mark_edge_deleted(rcand)
                    rcand = t

            # Symmetrically, delete L edges.
            if v_lcand:
                while toRight(lcand.oprev.dest, base) and inCircle(base.dest, base.org, lcand.dest, lcand.oprev.dest):
                    t = lcand.oprev
                    mark_edge_deleted(lcand)
                    lcand = t

            # The next cross edge is dest be connected dest either lcand.dest or rcand.dest.
            # If both are valid, then choose the appropriate one using the in_circle test.
            if not v_rcand or (v_lcand and inCircle(rcand.dest, rcand.org, lcand.org, lcand.dest)):
                # Add cross edge base from rcand.dest dest base.dest.
                base = connect(lcand, base.sym, self.edges)
            else:
                # Add cross edge base from base.org dest lcand.dest
                base = connect(base.sym, rcand.sym, self.edges)

        return ldo, rdo

test program:

import time
import matplotlib.pyplot as plt
import numpy as np

from divide_delaunay import Divide_Delaunay

if __name__ == "__main__":
    np.random.seed(16)
    num = 10
    fig = plt.figure()
    plt.ion()
    ax = fig.add_subplot(111)
    xs = np.random.randint(0, 100, num)
    ys = np.random.randint(0, 100, num)
    # xs = np.array([0, 1, 2, 3, 4, 5])
    # ys = np.array([0, 2, 1, 1, 4, 3])

    start_time = time.time()
    dt = Divide_Delaunay(list(zip(xs, ys)))
    end_time = time.time()
    verticies, edges = dt.verticies, dt.edges
    print(f"Triangulating {
      
      len(verticies)} points takes {
      
      end_time - start_time} s")

    # draw points
    for vertex in verticies:
        ax.scatter(vertex.x, vertex.y, c = 'b')

    # draw edges
    for edge in edges:
        a, b = edge.org, edge.sym.org
        ax.plot([a.x, b.x], [a.y, b.y], 'bo-')
        plt.pause(0.5)

    import matplotlib.tri as tri

    # Plot the triangulation.
    triang = tri.Triangulation([v.x for v in verticies], [v.y for v in verticies])
    ax.triplot(triang, 'ro-')

    fig.show()
    plt.pause(0)

Test Results:

GIF 2023-6-6 14-11-07

In the code we wrote, the merging part first looks for the upper common tangent of the left and right subdivisions, and then connects and merges from top to bottom. Of course, you can also find the lower common tangent line, and then connect and merge from the bottom to the top. The two methods have the same effect, but the judgment logic is opposite.

In terms of efficiency, three sets of data were tested. The result is as follows:

Point set size/piece time-consuming/s
1000 0.09
4000 0.33
6000 0.49

The time complexity is roughly O ( nlogn ) O(nlogn)O(nlogn) n n n is the point set size.


For more details and extensions on mesh generation see: Lecture Notes on Delaunay Mesh Generation

Guess you like

Origin blog.csdn.net/qq_39784672/article/details/131067426