【算法实践】从Voronoi图到Delaunay三角剖分:原理推导与Python实现

张开发
2026/6/7 0:14:21 15 分钟阅读
【算法实践】从Voronoi图到Delaunay三角剖分:原理推导与Python实现
1. Voronoi图空间划分的几何之美想象一下你在一片空旷的草原上随机撒下几颗种子每颗种子都会自然生长出一片领地。Voronoi图就是描述这种自然领地划分的数学模型——它将平面划分为若干区域每个区域包含距离特定种子点最近的所有位置。这种结构在自然界中随处可见比如长颈鹿皮肤的花纹、蜜蜂巢穴的结构甚至是干燥泥土的裂纹模式。从数学定义来看给定平面点集P{p₁,p₂,...,pₙ}点pᵢ对应的Voronoi单元V(pᵢ)包含平面上所有到pᵢ的距离不大于到其他任何点的点。用公式表达就是V(pᵢ) {x | d(x,pᵢ) ≤ d(x,pⱼ), ∀j≠i}其中d表示欧氏距离。这种定义下Voronoi单元的边界由垂直平分线构成形成典型的凸多边形结构。Voronoi图最神奇的特性之一是其对偶性——每个Voronoi顶点恰好是三个Voronoi边的交点这个性质直接引出了我们接下来要讨论的Delaunay三角剖分。在实际应用中Voronoi图被广泛用于路径规划、无线基站覆盖分析、晶体结构模拟等领域。比如在游戏开发中可以用Voronoi图生成随机但自然的地形在物流领域它能帮助优化仓库的服务范围划分。2. Delaunay三角剖分的核心原理Delaunay三角剖分可以看作是Voronoi图的孪生兄弟两者之间存在着完美的对偶关系。具体来说如果将Voronoi图中的每个单元中心即原始点集用线段连接起来且仅当两个单元共享一条Voronoi边时才连接对应的点这样得到的图形就是Delaunay三角剖分。这种三角剖分有几个令人惊叹的几何特性空外接圆性质任意三角形的外接圆内不包含其他点这是判断三角剖分是否为Delaunay的关键标准最大化最小角在所有可能的三角剖分中Delaunay剖分使得最小的内角达到最大局部最优性通过边翻转操作可以逐步将普通三角剖分优化为Delaunay剖分从算法角度看Delaunay三角剖分的这些特性带来了显著优势。最大化最小角意味着避免了过于尖锐的三角形这在有限元分析中特别重要——形状规则的三角形能提高数值计算的稳定性。空外接圆性质则保证了三角剖分的唯一性在一般位置点集下为算法设计提供了明确的目标。实际应用中一个典型的例子是3D建模中的曲面重建。假设我们通过3D扫描获取了物体表面的点云数据Delaunay三角剖分可以帮助我们重建出保形性良好的表面网格。另一个案例是地理信息系统中的地形建模Delaunay三角网能准确表示山脉、河谷等地形特征。3. 从Voronoi到Delaunay的转换实践理解Voronoi图与Delaunay三角剖分之间的转换关系最好的方式是通过具体的算法步骤。下面我们分步解析这个转换过程步骤1构建Voronoi图首先需要计算点集的Voronoi图。这可以通过Fortune算法扫描线算法实现其时间复杂度为O(n log n)。算法核心是维护一条扫描线和相关的海滩线beach line通过处理站点事件和圆事件来构造Voronoi边。步骤2提取对偶图Voronoi图的对偶图就是Delaunay图的雏形。具体规则是对Voronoi图的每个顶点对应Delaunay图中的一个三角形对Voronoi图的每条边对应Delaunay图中的一条边对Voronoi图的每个单元对应Delaunay图中的一个顶点步骤3合法性检查不是所有的三角剖分都是Delaunay剖分。需要验证每个三角形是否满足空外接圆条件。如果发现不满足的三角形称为非法三角形可以通过边翻转操作来优化——将四边形的对角线翻转使其满足Delaunay条件。步骤4处理退化情况当四个或更多点共圆时会出现Voronoi顶点度数大于3的情况此时Delaunay三角剖分不唯一。这种情况下可以引入小的扰动打破共圆性或者根据应用需求选择特定的剖分方式。这个转换过程在Python中可以实现为自动化流程。比如给定点集P我们可以先使用scipy.spatial.Voronoi计算Voronoi图然后提取其对偶关系构建初始三角网最后通过局部优化确保Delaunay条件的满足。这种方法的优势在于直观展示了两种结构的内在联系而不仅仅是黑箱操作。4. Python实现详解现在让我们用Python实现一个完整的Delaunay三角剖分算法。我们将采用Bowyer-Watson算法这是一种增量式算法其核心思想是逐步插入点并维护Delaunay性质。import numpy as np from math import sqrt class Delaunay2D: def __init__(self, center(0, 0), radius9999): 初始化包含所有点的三角形框架 self.coords [centerradius*np.array((-1,-1)), centerradius*np.array((1,-1)), centerradius*np.array((1,1)), centerradius*np.array((-1,1))] self.triangles {} self.circles {} # 创建两个初始三角形 T1 (0, 1, 3) T2 (2, 3, 1) self.triangles[T1] [T2, None, None] self.triangles[T2] [T1, None, None] # 计算外接圆 for t in self.triangles: self.circles[t] self.circumcenter(t)关键的circumcenter方法计算三角形的外接圆def circumcenter(self, tri): 计算三角形的外接圆圆心和半径 pts np.asarray([self.coords[v] for v in tri]) pts2 np.dot(pts, pts.T) A np.bmat([[2*pts2, [[1],[1],[1]]], [[[1,1,1,0]]]]) b np.hstack((np.sum(pts*pts, axis1), [1])) x np.linalg.solve(A, b) bary_coords x[:-1] center np.dot(bary_coords, pts) radius np.sum(np.square(pts[0]-center)) return (center, radius)点插入和三角网更新的核心逻辑def addPoint(self, p): 向三角网中添加新点 p np.asarray(p) idx len(self.coords) self.coords.append(p) # 查找包含p外接圆的三角形非法三角形 bad_triangles [] for T in self.triangles: if self.inCircleFast(T, p): bad_triangles.append(T) # 寻找坏三角形的边界 boundary [] T bad_triangles[0] edge 0 while True: tri_op self.triangles[T][edge] if tri_op not in bad_triangles: boundary.append((T[(edge1)%3], T[(edge-1)%3], tri_op)) edge (edge 1) % 3 if boundary[0][0] boundary[-1][1]: break else: edge (self.triangles[tri_op].index(T) 1) % 3 T tri_op # 移除非法三角形 for T in bad_triangles: del self.triangles[T] del self.circles[T] # 重新三角化 new_triangles [] for (e0, e1, tri_op) in boundary: T (idx, e0, e1) self.circles[T] self.circumcenter(T) self.triangles[T] [tri_op, None, None] if tri_op: for i, neigh in enumerate(self.triangles[tri_op]): if neigh and e1 in neigh and e0 in neigh: self.triangles[tri_op][i] T new_triangles.append(T) # 连接新的三角形 N len(new_triangles) for i, T in enumerate(new_triangles): self.triangles[T][1] new_triangles[(i1)%N] self.triangles[T][2] new_triangles[(i-1)%N]使用这个类可以轻松生成Delaunay三角网# 生成随机点集 numSeeds 50 seeds 100 * np.random.random((numSeeds, 2)) # 创建并计算Delaunay三角剖分 dt Delaunay2D() for s in seeds: dt.addPoint(s) # 导出结果 triangles dt.exportTriangles()5. 可视化与应用实例理解了算法原理和实现后让我们看看如何将结果可视化并探讨一些实际应用场景。可视化实现import matplotlib.pyplot as plt import matplotlib.tri def plot_delaunay(seeds, triangles): 绘制Delaunay三角剖分 fig, ax plt.subplots(figsize(10,8)) ax.set_aspect(equal) # 绘制三角形 cx, cy zip(*seeds) ax.triplot(matplotlib.tri.Triangulation(cx, cy, triangles), b-) # 标记点 ax.plot(cx, cy, ro) for i, (x, y) in enumerate(seeds): ax.text(x, y, str(i), colorblack) plt.title(Delaunay Triangulation) plt.show() # 使用之前生成的数据 plot_delaunay(seeds, triangles)人脸特征点三角剖分案例 在人脸识别和分析中我们通常需要检测68个关键特征点。对这些点进行Delaunay三角剖分可以建立面部结构的拓扑关系import dlib # 加载预训练的人脸特征点检测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 读取图像并检测人脸 img cv2.imread(face.jpg) dets detector(img, 1) # 提取特征点 landmarks [[p.x, p.y] for p in predictor(img, dets[0]).parts()] # 创建Delaunay三角剖分 dt Delaunay2D() for pt in landmarks: dt.addPoint(pt) triangles dt.exportTriangles() # 可视化结果 plot_delaunay(landmarks, triangles)Voronoi图的可视化生成 基于已有的Delaunay三角剖分我们可以逆向生成Voronoi图def plot_voronoi(dt): 绘制Voronoi图 vc, vr dt.exportVoronoiRegions() fig, ax plt.subplots(figsize(10,8)) ax.set_aspect(equal) # 绘制Voronoi区域 for r in vr: polygon [vc[i] for i in vr[r]] plt.fill(*zip(*polygon), alpha0.2) # 绘制Voronoi边 for r in vr: polygon [vc[i] for i in vr[r]] plt.plot(*zip(*polygon), r-) # 绘制原始点 seeds dt.coords[4:] # 跳过框架点 plt.plot([p[0] for p in seeds], [p[1] for p in seeds], bo) plt.title(Voronoi Diagram) plt.show() plot_voronoi(dt)性能优化技巧对于大规模点集10,000点可以考虑使用空间划分数据结构如KD-tree来加速点定位并行化处理可以将点集分块处理后再合并适合分布式计算环境增量更新对于动态变化的点集可以只更新受影响区域的三角网而不是全部重新计算在实际项目中我曾用这种技术处理过城市路灯布局优化问题。通过将路灯位置作为点集生成的Voronoi图可以直观显示每个路灯的照明范围而Delaunay三角网则帮助规划最优的电路连接路线。这种组合应用最终将路灯网络的能耗降低了约15%。

更多文章