Delaunay三角剖分学习笔记

news2024/11/27 10:30:21

文章目录

  • Delaunay三角剖分学习笔记
    • 1 Voronoi \text{Voronoi} Voronoi
      • 1.1 定义与性质
    • 2 三角剖分
      • 2.1 定义与性质
      • 2.2 质量(quality)评定标准
    • 3 Delaunay三角剖分
      • 3.1 定义
      • 3.2 准则与性质
    • 4 Delaunay三角剖分算法
      • 4.1 Bowyer-Watson算法
        • 4.1.1 算法步骤:
        • 4.1.2 算法伪代码
        • 4.1.3 算法解释示例
        • 4.1.4 算法的优化
          • (1)排序优化
          • (2)快速点定位
        • 4.1.5 实现细节
          • (1)构造超级三角形
          • (2)计算三角形外接圆圆心和半径
        • 4.1.6 代码实现
      • 4.2 分治法
        • 4.2.1 Quad-Edge
          • (1)create_edge(org, dest, edges)
          • (2)update_next_prev(e1, e2)
          • (3)connect(e1, e2, edges)
          • (4)mark_edge_deleted(e)
        • 4.2.2 工具算法
          • (1)判断点是否在三角形外接圆内
          • (2)判断点与边的相对位置
        • 4.2.3 主要程序

Delaunay三角剖分学习笔记

1 Voronoi \text{Voronoi} Voronoi

在理解Delaunay三角剖分之前,先引入 Voronoi \text{Voronoi} Voronoi图。

1.1 定义与性质

定义

设由任意 n n n个互异平面点(也称基点)组成的点集 P = { p i ∣ i = 1 , . . . , n } P=\{p_i|i=1,...,n\} P={pii=1,...,n} P P P对应的 Voronoi \text{Voronoi} Voronoi图可以理解为对平面的一个区域(单元)划分。对于划分得到的每个区域(也称 Voronoi \text{Voronoi} Voronoi多边形),都应当满足:在基点 p i p_i pi对应的单元中任取一点,该点到 p i p_i pi的(欧式)距离一定小于到 p j , p j ∈ P , j ≠ i p_j,p_j\in{P},j\ne{i} pjpjP,j=i的(欧式)距离。

image-20230522154118839

Voronoi \text{Voronoi} Voronoi又称泰森多边形或 Dirichlet \text{Dirichlet} Dirichlet图。

平面上的 V-图可以看作是数据点集合 P 中的每个点作为生长点,以相同的速率向 外扩散,直到彼此相遇为止在平面上所形成的图形。除最外层的点形成开放的区域外, 其余的每个点都形成一个凸多边形。

一些性质

  • 每个 Voronoi \text{Voronoi} Voronoi多边形内仅包含一个基点;
  • Voronoi \text{Voronoi} Voronoi多边形内的任一点到相应基点的(欧式)距离最近;
  • 位于 Voronoi \text{Voronoi} Voronoi多边形边上的点到其两边的离散点的距离相等;
  • n n n个点的集合 P P P Voronoi \text{Voronoi} Voronoi图最多有 2 n − 5 2n-5 2n5个顶点和 3 n − 6 3n-6 3n6条边;
  • 假如任意四个基点都不共圆,则每个 Voronoi \text{Voronoi} Voronoi顶点恰好是三条 Voronoi \text{Voronoi} Voronoi边的交点,即由 P P P中的三点所形成三角形外接圆的圆心。

参考:

[1] 高莉. 改进的Delaunay三角剖分算法研究[D]. 兰州交通大学, 2015.

[2] 泰森多边形_百度百科 (baidu.com)

2 三角剖分

2.1 定义与性质

设由任意 n n n个平面点组成的点集 P = { p i ∣ i = 1 , . . . , n } P=\{p_i|i=1,...,n\} P={pii=1,...,n}。三角剖分是指用互不相交的直线段连接 P i P_i Pi P j P_j Pj 1 ≤ i , j ≤ n , i ≠ j 1≤i,j≤n,i≠j 1ijni=j,并且使其凸包中的每个划分区域都是一个三角形。

由于三角剖分是一个平面图,故满足以下几个条件:

  • 图中不存在相交的边(除线段端点外,不存在重叠的边);
  • 图中的边不包含集合 P P P中的其他任何点,端点除外;
  • 图中所有的面片都是三角形,并且所有三角形的集合构成 P P P的凸包。

image-20230523011544467

2.2 质量(quality)评定标准

由于对给定点集的三角剖分不唯一,对于不同的三角剖分我们有如下质量评定标准:

质量评定标准解释
最小角(minimum angle)所有三角形的内角当中最小的角
纵横比(aspect ratio)三角形最短边与最长边的比例
半径比(radius ratio)三角形内接圆半径的两倍与外接圆半径的比例

参考:

[1] 技术分享:Delaunay三角剖分算法介绍 - 知乎 (zhihu.com)

3 Delaunay三角剖分

Delaunay剖分是一种三角剖分的标准。

3.1 定义

Delaunay边:假设 E E E E E E为点集的三角剖分的边集)中的一条边 e e e(两个端点分别为 a , b a,b a,b), e e e为Delaunay边的条件为:存在一个圆经过 a , b a,b a,b两点,圆内(最多三点共圆)不含点集 P P P中任何其他的点,这一特性又称空圆特性

Delaunay三角剖分:如果点集 P P P的一个三角剖分 T T T只包含Delaunay边,那么该三角剖分称为Delaunay三角剖分。

Delaunay三角剖分的另一种定义:因为Delaunay三角剖分与 Voronoi \text{Voronoi} Voronoi图是对偶关系,Delaunay三角剖分是 Voronoi \text{Voronoi} Voronoi图的伴生图形,两者可以相互转化。作 Voronoi \text{Voronoi} Voronoi图的对偶图,即对每条 Voronoi \text{Voronoi} Voronoi边(限有限长线段)作通过点集中某两点的中垂线,得到的即为Delaunay三角剖分。

image-20230524001225163

3.2 准则与性质

一个三角剖分必须符合以下两条重要准则,才可称之为Delaunay三角剖分。

  • 空圆特性

    Delaunay三角剖分是唯一的(任意四点不能共圆),在Delaunay三角剖分中任一三角形的外接圆内不会存在其它点。即满足Delaunay边的定义。

  • 最大化最小角特性

    在点集 P P P的所有可能的三角剖分中,Delaunay三角剖分所形成的三角形最小角最大。

Delaunay三角剖分具备以下几点重要的性质:

(1)唯一性:无论从点集的任何位置开始建网,最终得到的Delaunay三角剖分都是唯一的。

(2)最接近:三角形是以最邻近的三点组成的,并且所形成的三角形的各边都不会相交。

(3)最规则:假如将三角剖分中每个三角形的最小角度按升序进行排列,那么Delaunay三角剖分排列得到的数值最大。

(4)最优性:如果任意两个相邻三角形所组成的凸四边形的对角线相互交换,那么交换后两个三角形的六个内角中最小角角度不再增大。

(5)区域性:移动、新增、删除三角剖分中某个顶点时只会影响相邻的三角形。

(6)具有凸多边形的外壳:在所构建的三角剖分中,最外层的边界构成了点集的凸多边形“外壳”(即点集的凸包)。

4 Delaunay三角剖分算法

Delaunay三角剖分的算法可以分为逐点插入法、三角网生长法,分治算法等。分治算法的效率最高,逐点插人法实现简单高效,占用内存较小,但它的时间复杂度差。三角网生长法由于效率相对较低,目前采用较少。

image-20230525011734005

4.1 Bowyer-Watson算法

Bowyer-Watson算法属于逐点插入法的一种,易于理解和实现。

4.1.1 算法步骤:

Step1:构造一个包含点集中的所有点的超级三角形(super-triangle),放入三角形列表中;

这个三角形列表可以理解为一个三角剖分,目前只包含一个三角形,即超级三角形。

Step2:将点集中的点逐一插入现有的三角剖分中,并进行如下调整:

  • 在三角形列表中,找出所有外接圆包含该插入点的三角形(称为该点的影响三角形),所有影响三角形的合集构成一个“星形多边形”(star shaped polygon)。星形多边形的含义是多边形的任何一个顶点到插入点的连线都在多边形内部。

  • 对于上述星形多边形,将其内部的三角形全部删除,形成一个“空穴”。将空穴边界的顶点与新插入的点连接得到新的三角形,替代剖分中被删除的三角形,从而完成一个点在Delaunay三角形列表中的插入,得到一个包含插入点的新Delaunay三角剖分。

还有一种说法是:在三角形列表中,找出其外接圆包含插入点的三角形(称为该点的影响三角形),删除影响三角形的公共边,将插入点同影响三角形的全部顶点连接起来,从而完成一个点在Delaunay三角形列表中的插入。该步骤的图示如下:

img

Step3:循环执行Step2,直到所有点插入完毕。

Step4:最后从三角形列表中删除与超级三角形关联的三角形,得到点集的Delaunay三角剖分。

4.1.2 算法伪代码

版本一

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

版本二

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 算法解释示例

这里我们以A,B,C,D四个点为例,画图说明整个Bowyer-Watson算法的流程。

  • 首先建立一个超级三角形(p1,p2,p3),这个三角形要把点集中所有的点都包含进去。将这个超级三角形加入三角形列表。

    image-20230525133130002

    关于如何构建超级三角形,详见[4.1.5 实现细节](#4.1.5 实现细节)。

  • 我们先插入点A,因为原来的三角形列表中唯一的超级三角形外接圆必定包含点A,所以我们可以将三角形▲(p1,p2,p3)看作一个星形多边形,其内部没有三角形,所以我们直接将点A与其顶点相连,将原来的三角形拆分为三个三角形(▲(p1,A,p2),▲(p2,A,p3),▲(p3,A,p1))。

    image-20230525135633264

  • 再插入点B,做三角形列表中各个三角形的外接圆,我们找到了点B的影响三角形,即▲(p1,A,p3),▲(p2,A,p3)。

    image-20230525140915264

  • 删除影响三角形组成的星形多边形内部的三角形(公共边,这里即边(A,p3))。然后将点B同影响三角形的全部顶点连接起来。

    image-20230525141540743

  • 接下来再插入点C,同上面插入点B的流程一样,找到影响三角形,删除公共边,连接影响三角形的各个顶点。

    image-20230525142157546

  • 然后相同的方法插入点D。

    image-20230525142614560

  • 最后删除与超级三角形顶点(p1,p2,p3)相关联的三角形。

    image-20230525142914270

  • 最终点A,B,C,D的三角剖分结果如下图。

    image-20230525143023480

4.1.4 算法的优化

Bowyer-Watson算法比较耗时的一步是关于新插入点的定位。在上文中,我们通过遍历三角形列表中的所有三角形,然后分别判断插入点是否落在其外接圆内。随着点集规模增大,三角形列表在Delaunay三角剖分的构造过程中会逐渐增大,插入点定位的耗时也会随之增加。因此针对点定位问题,有很多优化的改进算法。

(1)排序优化

该算法优化的思路如下:首先将原始点集中的点按x坐标从小到大进行排序。在插入时,不再是随机插入,而是按照排序顺序进行。保证了新插入的点不会出现在之前插入点的左侧。还有一点不同,该算法中除了点集的列表外,还有已确定的三角形列表和未确定的三角形列表。每次对插入点进行定位时,只需要在未确定的三角形列表中对新插入点进行定位计算,而不再对所有生成的三角形进行查询。如个插入点在查询三角形外接圆的右侧,则说明查询三角形为合法的Delaunay三角形,移存到已确定的三角形列表中;若在外接圆外且不在右侧,则说明该查询三角形仍是一个未确定三角形,不进行任何操作;如在外接圆内,则说明该查询三角形不为Delaunay三角形,从未确定三角形列表中移除。

伪代码:

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

细节:

当插入点在查询三角形外接圆右侧时。由于插入点按照x坐标大小从左往右依次插入,所以剩余的其他点都必然在该外接圆的右侧,即该查询三角形满足空圆特性,为Delaunay三角形。

img

在查询三角形的外接圆中,插入点1时,符合在外侧的条件,但是不能保证后面所有的点都保持在外接圆外侧。如图中的点2及后面可能出现的点很有可能出现在圆内,而使该三角形被按边分解。所以在该优化算法中,如果碰到在点在外侧且非右侧的话,会跳过,该三角形一直在temp triangles中被检验,直到碰到下一个点在圆内或圆右才会从未确定三角形列表中去除,进行后面的操作。

而当点在圆上时,也是根据在圆内的方法对其进行操作,实际情况中会出现这种情况,该现象称为“退化”。

img

参考:

[1] 三角剖分算法(delaunay) - 纸异兽 - 博客园 (cnblogs.com)

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

(2)快速点定位

TODO:

参考:

[1] 计算几何学习——点定位_Mathematic_feng的博客-CSDN博客

[2] 刘琴琴.平面域Delaunay三角网点定位算法研究综述[J].电子设计工程,2017,25(01):47-51.DOI:10.14022/j.cnki.dzsjgc.2017.01.012.

[3] 伯格 著, 邓俊辉. 计算几何:算法与应用(第3版)[M]. 清华大学出版社, 2009.

4.1.5 实现细节

(1)构造超级三角形

方法一:

image-20230525231438652

其中绿色四边形为点集的包围盒。

方法二:

img

根据相似三角形定理求得与矩形一半的小矩形的对角三角形,扩大一倍后则扩大后的直角三角形斜边经过点(Xmax,Ymin)。为了将所有的点包含在超级三角形内,在右下角对该三角形的顶点进行了横和高的扩展,并要保证这个扩展三角形底大于高。

参考自:三角剖分算法(delaunay) - 纸异兽 - 博客园 (cnblogs.com)

其他方法:

上面两种方法的初始化图形都是一个三角形。除此之外,还有学者提出使用一个包含所有点的初始矩形,然后将矩形的任意一条对角线连接,划分为两个三角形加入初始三角剖分列表中。

image-20230525232322324

上图中, K \text{K} K为一个正位移值。

(2)计算三角形外接圆圆心和半径

参考:

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

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

求出外接圆圆心坐标后,圆心与三角形任一顶点的距离即是外接圆半径。

如果三点共线,那么其外接圆圆心将在无穷远处。

4.1.6 代码实现

首先定义点、边、三角形的数据结构。

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)]

然后定义一个用于delaunay三角剖分的类。

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

其中exportTriangles函数用于导出matplotlib库绘制剖分结果所需的数据。算法测试代码如下:

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()

其中seted_points = set(zip(xs, ys))用于去除随机生成点集中的重复点,满足输入点集的坐标互异要求。否则,若随机生成的两点坐标相同,在计算点外接圆时,如下代码中,D为0,会抛出ZeroDivisionError: division by zero异常。

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

如下两行代码的作用是,调用matplotlib.tri.Triangulation构建三角剖分。用两种不同的颜色分别绘制我们编写的代码和调用第三方库函数进行三角剖分的结果,可以验证算法的正确性。

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

测试结果如下:

image-20230528205450735

经过测试发现,matplotlib.tri.Triangulation函数得到的三角剖分在随机生成点集数量比较大时,会和本算法的剖分结果有差异。

matplotlib.tri.Triangulation函数剖分结果:

image-20230528205916411

我们编写的Delaunay_Triangulation类的剖分结果:

image-20230528205904484

出现这种问题的原因,我猜测可能是因为某四点共圆了。

在Delaunay三角剖分的性质中,若点集 P P P中任意四点不共圆,则存在唯一的Delaunay三角剖分 T T T。若点集 P P P中四点 A , B , C , D A,B,C,D A,B,C,D共圆,且△ABC,△BCD属于Delaunay三角剖分T,那么将边 B C BC BC翻转后得到的三角剖分 T ’ T’ T(包含△ABD,△ACD)同样是一个Delaunay三角剖分。

技术分享:Delaunay三角剖分算法介绍 - 知乎 (zhihu.com)

通过输入一系列共圆点,得到下图结果。两种都属于Delaunay三角剖分。

image-20230529004839957

在效率上,测试了三组数据。结果如下:

点集规模/个耗时/s
10004
400060
6000150

时间复杂度大概为 O ( n 2 ) O(n^2) O(n2) n n n为点集大小。

4.2 分治法

参考文献:

[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 和 J. Stolfi 提出了Quad-Edge数据结构,并使用其简化了1975年Shamos和Hoey提出的Delaunay三角剖分分治算法。在上述的参考文献中,作者专门用了一节对分治法三角剖分进行介绍,并附有详细的伪代码。

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

分治法三角剖分中我们使用Quad-Edge作为边的数据结构。下面我们将介绍一些边的拓扑操作。

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)

该函数用于返回以org为起点,dest为终点的边(这里及[本节](#4.2.1 Quad-Edge)中提到的边都指Quad-Edge),同时将这条边放入edges列表中。我们在该函数中初始化了新生成边的对称边,并且正确初始化了新边及其对称边的onextoprev属性,因为新边未与任何其他边建立拓扑联系,所以这两个属性都是这两条边自身。具体实现细节参见文章[1]中提出的MakeEdge操作。

(2)update_next_prev(e1, e2)

该函数用于拼接e1边和e2边,实质上是更新了两条边的onextoprev属性。具体实现细节参见文章[1]中提出的Splice操作。

(3)connect(e1, e2, edges)

该函数使用一条新边连接e1边和e2边,并更新三者的拓扑关系。具体实现细节参见文章[1]中提出的Connect操作。

(4)mark_edge_deleted(e)

该函数用于删除一条边,本质上是改变删除边和其他边的拓扑关系。具体实现细节参见文章[1]中提出的DeleteEdge操作。

4.2.2 工具算法

(1)判断点是否在三角形外接圆内

d d d在三角形 ( a , b , c ) (a,b,c) (a,b,c)外接圆内满足如下不等式:
r e t = ∣ a x a y a x 2 + a y 2 1 b x b y b x 2 + b y 2 1 c x c y c x 2 + c y 2 1 d x d y d x 2 + d y 2 1 ∣ > 0 (4-1) ret =\begin{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)
接下来,将式(4-3)中行列式第三列加上第一列和第二列的 − 2 d x -2d_x 2dx倍,然后将第三列元素整理化成平方差形式,得到下式:
r e t = ∣ a x − d x a y − d y ( a x − d x ) 2 + ( a y − d y ) 2 b x − d x b y − d y ( b x − d x ) 2 + ( b y − d y ) 2 c x − d x c y − d y ( c x − d x ) 2 + ( c y − d y ) 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=axdx替换相同元,得到下式:
r e t = ∣ a d x a d y a d x 2 + a d y 2 b d x b d y b d x 2 + b d y 2 c d x c d y c d x 2 + c d y 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)判断点与边的相对位置

设边的起点 a a a,终点 b b b,输入点为 p p p
d e t = ∣ a x a y 1 b x b y 1 p x p y 1 ∣ (4-6) det=\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} det= 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} det=(axpx)(bypy)(aypy)(bxpx)(4-7)

  • d e t > 0 det>0 det>0 p p p在边左边;
  • d e t < 0 det<0 det<0 p p p在边右边;
  • d e t = 0 det=0 det=0 p p p在边共线。
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 主要程序

算法伪代码:

image-20230606140652092

主程序:

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

测试程序:

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)

测试结果:

GIF 2023-6-6 14-11-07

在我们写的代码中,合并部分最开始寻找的是左右子剖分的上公切线,然后从上到下进行连接合并。当然,也可以寻找下公切线,然后从下向上进行连接合并,两种方式都是一样的效果,是不过判断逻辑相反。

在效率上,测试了三组数据。结果如下:

点集规模/个耗时/s
10000.09
40000.33
60000.49

时间复杂度大概为 O ( n l o g n ) O(nlogn) O(nlogn) n n n为点集大小。


更多关于网格划分生成的细节和拓展参见:Lecture Notes on Delaunay Mesh Generation

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/615677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学会这样写代码,一看就是资深工程师,代码简洁之道PHP版本

文章目录 一、前言二、规范2.1 整体结构规范2.1.1 类的括号前括号单独一行2.1.2 方法的前括号单独一行2.1.3 方法内部语句前括号不换行 2.2 变量与常量2.2.1 变量的命名尽量要有语义2.2.2 同一个实体要用相同的变量名2.2.3 尽量使用有语义的常量表述2.2.4 使用自解释型变量2.2.…

六面钻调试第八步Cam参数调试(一)之压轮压板 ,机械规格调试

Cam参数调试 &#xff08;1&#xff09;.压轮压板参数调试 注&#xff1a;压板的规格测量设置 压板的位置相对基准钻的位置设置 &#xff08;2&#xff09;机械规格调试 1.定位气缸的类型 2.活动式定位气缸的Y向宽度 4.定位杆与夹钳Y边缘的最小间隙 5.活动式定位气缸相对基准…

CVPR视频理解论文

Learning Transferable Spatiotemporal Representations from Natural Script Knowledge 时空TransformerCLIP的对比学习思路

if/while/for/语句/分支/路径覆盖的控制流程图+数据流分析(DU)

if/while/for/语句覆盖/分支覆盖/路径覆盖的控制流程图数据流分析(DU) 语句的线性序列Linear Sequences of Statements “If” Constructs “While” Constructs “For” Constructs 语句覆盖率Statement Coverage 测试套件应执行 CFG 的所有节点 也被称为&#xff1a;…

为什么客服系统必备知识库?提高客服效率的秘密武器!

对任何想要成功的企业而言&#xff0c;客户服务是必不可少的。依据提供的客户支持质量&#xff0c;可建立或破坏一个品牌的声誉。为了提供高标准的客户服务&#xff0c;企业必须能够获得可靠的信息&#xff0c;并能够为用户提供快速有效的解决方案。下面&#xff0c;我们就详细…

matlab学习1--基础

文章目录 基本语法保存和加载数组/矩阵矩阵运算 输出多个值绘制向量图 基本语法 和弱语言一样&#xff0c;变量赋值不需要声明类型直接 a 1 2命名规则 以字母开头&#xff0c;并且仅包含字母、数字和下划线 (_) 区分大小写清除命令行窗口 clc保存和加载 保存 保存到xxx.…

python开发构建轻量级卷积神经网络模型实现手写甲骨文识别系统

手写汉字、手写数字、手写字母识别模型都已经做过很多了&#xff0c;但是手写甲骨文识别这个应该都是很少有听说过的吧&#xff0c;今天也是看到这个数据集就想着基于这批手写甲骨文数据集开发构建识别模型&#xff0c;首先来看下效果图&#xff1a; 接下来看下对应使用的数据集…

字典的学习笔记

列表 [] 单身什么是字典 {} 二人世界 python内置的数据结构之一&#xff0c;与列表一样是一个可变序列(可以增删改操作的) 以键值对的方式存储数据&#xff0c;字典是一个无序的序列 -> hash(key) 通过哈希函数来计算存储位置,key一定是不可变的字典的创建 使用花…

读论文-MHFormer

论文&#xff1a;Multi-hypothesis representation learning for transformer-based 3D human pose estimation 摘要 尽管取得了重大进展&#xff0c;但由于深度模糊和自遮挡&#xff0c;从单目视频中估计3D人体姿势仍然是一项具有挑战性的任务。大多数现有的作品都试图通过利用…

驱动程序接口和HAL层区别和联系

驱动程序接口&#xff08;Device Driver Interface&#xff09;和硬件抽象层&#xff08;Hardware Abstraction Layer&#xff0c;HAL&#xff09;是在软件系统中用于处理硬件的两个不同层次的概念。 驱动程序接口&#xff08;Device Driver Interface&#xff09;&#xff1a;…

设计模式(二、三):创建型之工厂方法和抽象工厂模式

设计模式系列文章 设计模式(一)&#xff1a;创建型之单例模式 设计模式(二、三)&#xff1a;创建型之工厂方法和抽象工厂模式 设计模式(四)&#xff1a;创建型之原型模式 设计模式(五)&#xff1a;创建型之建造者模式 设计模式(六)&#xff1a;结构型之代理模式 目录 一、…

校园综合能效平台建设的意义

摘 要&#xff1a;为响应国家绿色校园建设的号召&#xff0c;切实提高高校能源利用水平&#xff0c;促进学校能源资源合理配置&#xff0c;服务学校高质量发展大局&#xff0c;根据教育部印发的《关于开展节能减排学校行动的通知》《关于勤俭节约办教育建设节约型校园的通知》…

Linux内核超级装备eBPF技术详细研究

定义一&#xff08;http://ebpf.io的定义&#xff09; eBPF (which is no longer an acronym for anything) is a revolutionary technology with origins in the Linux kernel that can run sandboxed programs in a privileged context such as the operating system kernel.…

物联网通信之串口服务器,RS485/RS232双串口并行、远程虚拟串口调试

随着现代工业信息技术发展&#xff0c;串口服务器在工业应用中越来越常见&#xff0c;那么什么是串口服务器呢&#xff0c;今天智联物联小编就与大家分享一下物联网通信中的串口服务器。 为帮助大家理解&#xff0c;智联物联小编从串口服务器的接口为大家开始讲解&#xff0c;一…

高分子PEG:mPEG-Maleimide MW:3400,甲氧基-聚乙二醇—马来酰亚胺,常用作聚合物试剂

【产品描述】 陕西新研博美生物科技有限公司供应的​mPEG-Maleimide属于高分子PEG&#xff0c;马来酰亚胺和巯基的偶合是蛋白和多肽偶联中的一个非常有用的反应。mPEG-MAL被用来合成具有确定结构和生物活性的PEG-蛋白质偶合物。mPEG-MAL也常用作聚合物试剂来选择性诱捕含巯基的…

易基因:小檗碱通过介导m6A mRNA甲基化调控斑马鱼肝细胞氧化应激、凋亡和自噬|科研进展

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 中药小檗碱&#xff08;Berberine&#xff0c;BBR&#xff0c;化学式C20H18NO4&#xff09;是从几种药用植物中分离出的一种异喹啉季生物碱&#xff0c;包括小檗&#xff08;Berberis ar…

让你不再好奇怎样同声传译

众所周知&#xff0c;同声传译技术在国际交流和商务领域发挥着重要的作用&#xff0c;它能够帮助人们跨越语言障碍&#xff0c;促进人们之间的有效沟通。那么&#xff0c;你知道如何同声传译吗&#xff1f;接下来我将教你三个方法&#xff0c;帮助你更好的进行同声传译操作。 方…

springboot+vue餐厅点餐系统在线点餐系统(含源码+数据库)

1.系统分析 系统用例图如下所示。 从用户、餐厅等方面进行需求分析如下。 1.用户需求&#xff1a;系统应该提供简单易用的用户界面&#xff0c;用户可以浏览餐厅菜单&#xff0c;选择菜品&#xff0c;下订单。此外&#xff0c;应该允许用户管理个人信息和查看历史订单。 2.餐…

SQL-多表查询-事务

SQL-多表查询-事务 多表查询顾名思义就是从多张表中一次性的查询出我们想要的数据 前期表准备 DROP TABLE IF EXISTS emp; DROP TABLE IF EXISTS dept;# 创建部门表CREATE TABLE dept(did INT PRIMARY KEY AUTO_INCREMENT,dname VARCHAR(20));# 创建员工表CREATE TABLE emp (i…

一文3000字从0到1用Python做安全测试攻击实战(建议收藏)

在本文中&#xff0c;我们将使用Python进行一次安全测试的实战演练&#xff0c;目标是找出并利用应用程序的安全漏洞。请注意&#xff0c;这个演练仅用于教育和研究目的&#xff0c;切勿将这些技术用于非法活动。 注意&#xff1a;未经授权的攻击是违法的。确保你在拥有明确权…