
简介这是一套基于 Python 实现的二维点云边缘点提取资源算法以 Delaunay 三角网为核心通过分析三角形的内角、边长以及邻接三角形分布等几何与拓扑特征从散乱点云中准确识别边界点。资源主要面向测绘遥感、地理信息、计算机视觉、三维重建以及逆向工程等领域的开发者和研究人员同时也适合刚接触计算几何和点云处理的入门学习者作为理解边缘提取原理的参考实现。压缩包内容高度精简仅包含 1 个 .py 文件包体大小约 1KB代码虽短但结构清晰完整实现了 Delaunay 三角网的构建与基于邻域信息的边缘点判定便于用户直接阅读、运行及修改也可作为基础工具集成到更大的点云处理流程中。目前已有 226 人下载学习反映出资源对相关领域读者确有实用价值。通过该脚本读者可以直观获得二维点云边缘提取的完整代码逻辑并以此为出发点进一步拓展到三维点云边界探测、建筑物轮廓提取等更复杂的应用场景。1. 从凸包到 Delaunay点云边缘提取先想清楚要哪种边界一栋楼的地面激光扫描点云要提取墙面轮廓一片地形散点要圈出陡坎线一个机械零件切片要抽出内外轮廓。这三个需求都叫“点云边缘提取”但目标边界完全不同封闭轮廓、开口断裂线、内外圈嵌套。如果一上来就求凸包凹进去的墙角、台地拐弯和孔洞细节会全部被吞掉如果把点云栅格化成体素再做边缘轮廓的定位精度又会被网格分辨率锁死。基于 Python 的 Delaunay 三角网的点云边缘提取走的是第三条路让点云自身的密度决定邻接关系再通过三角形尺寸或邻域闭合性把边界点筛出来。它适合处理地形切片、扫描轮廓、不规则边界这类几何细节比凸包重要、又不想被体素网格绑架的场景。这套方法的核心不是画三角形本身而是把“边缘”翻译成三角网上一个可计算的拓扑属性。2. Delaunay 三角网的边界判定原理空圆、自由边与 alpha 取值2.1 空圆特性让邻接关系自适应点密度Delaunay 三角网的定义很干净对平面上的一组点做三角剖分使得每个三角形的外接圆内部不包含任何其他点这就是空圆特性。由此还派生出一个对边缘提取特别有用的性质三角形趋向于“饱满”不会出现刻意拉长的尖角三角形除非点集本身在几何上确实存在狭长分布。更重要的是这个剖分不依赖任何人工指定的邻域半径点密集的地方三角形小点稀疏的地方三角形大邻接关系跟着数据走。这一点在点云处理里非常关键。用 KNN 找邻域要定 K用半径搜索要定半径而点云密度在扫描过程中几乎必变近处墙面一平方米几万点远处可能只有几百点。Delaunay 不用你猜参数它直接把“哪些点该连在一起”交给空圆规则去裁决。后文的边缘提取算法全部建立在这个自适应邻接关系之上。2.2 边缘点的两种判定准则三角形半径与邻域不闭合拿到三角网后边缘在哪一个直觉答案是只被一个三角形拥有的边也就是自由边。问题是把整张 Delaunay 三角网的所有自由边连起来得到的只是点集的凸包边界内部的凹口和孔洞完全体现不出来。真正能被提取出来的边缘取决于我们允许哪些三角形“存活”。常见的判定准则有两种。第一种看外接圆半径对每个三角形算外接圆半径 r只保留 r 小于某个阈值 alpha 的三角形再取这些存活三角形的自由边。alpha 越小保留的三角形越小凹进细节越多alpha 越大越接近凸包。这就是 alpha shape 的基本思想和 Delaunay 空圆特性是同源的。第二种看邻域闭合性对每个点统计它关联的存活三角形是否在平面上围成了一个完整环。内部点的邻域三角形围绕该点转一整圈360 度闭合边界点的邻域三角形只覆盖一个扇形区域必然有一个缺口。把这个“缺口角度”或“关联三角形数量”和阈值比较也能定边界点。实际工程里两者经常配合使用alpha 控制几何尺度邻域闭合性用于剔除那些虽然半径够小、但孤零零挂在边界外的三角形碎片。2.3 alpha 的连续语义从凸包到碎片化边界alpha 不是边长而是三角形的空圆半径上限这一点新手最容易搞混。同一个 alpha 下边长很短的细长三角形可能存活边长中等但角度平缓的三角形反而被剔除。理解 alpha 的连续变化对后续调参至关重要我按经验把它划分为四个区间alpha 取值保留三角形形态提取结果表现适用场景趋于无穷大全部三角形保留退化为凸包边界只看整体外接范围中等偏大半径在阈值内的大三角形凹边轮廓孔洞消失建筑外轮廓、地块边界中等偏小只保留小半径三角形凹口和孔洞同时出现零件切片、树冠轮廓过小大量三角形被剔除边界断裂甚至只剩碎片不适用需回调实际调试时alpha 从大往小调边界会从“凸包”逐渐“陷”进凹区域直到某个临界点出现断裂。临界点附近的 alpha 值通常就是最优解附近。下一章直接用代码把这条判定链路跑通。3. 用 scipy.spatial.Delaunay 写一个可跑的边缘提取实现3.1 从三角形集合到边界边的完整函数scipy.spatial.Delaunay 是 Python 生态里最直接的三角网构建入口二维点集传入后simplices 属性直接给出每个三角形的顶点索引。基于它实现 alpha shape 边缘提取核心代码很短import numpy as np from scipy.spatial import Delaunay def alpha_shape(points, alpha): pts np.asarray(points) tri Delaunay(pts) keep [] # 存活的三角形索引 edge_owner {} # 边 - 被多少存活三角形共享 for simp in tri.simplices: i, j, k simp # 三边边长 a np.linalg.norm(pts[i] - pts[j]) b np.linalg.norm(pts[j] - pts[k]) c np.linalg.norm(pts[k] - pts[i]) # 海伦公式求面积退化时外接圆半径置为无穷 s (a b c) * 0.5 area np.sqrt(max(s * (s - a) * (s - b) * (s - c), 0.0)) if area 1e-12: r np.inf else: r a * b * c / (4.0 * area) # 外接圆半径 if r alpha: keep.append(simp) for e in ((min(i, j), max(i, j)), (min(j, k), max(j, k)), (min(k, i), max(k, i))): edge_owner[e] edge_owner.get(e, 0) 1 # 存活三角形中只被共享一次的边就是边界边 boundary_edges [e for e, cnt in edge_owner.items() if cnt 1] boundary_idx sorted({v for e in boundary_edges for v in e}) return boundary_edges, boundary_idx, keep, tri这段代码做了三件事构建三角网、按外接圆半径过滤三角形、统计存活三角形的共享边数。r 直接用三边长乘除四倍面积求得不需要解圆心坐标面积低于 1e-12 说明三点近似共线或重合按无穷半径处理避免除零。boundary_idx 是边界点的原始点云索引后续不管是回映射三维坐标还是可视化都直接拿这份索引操作。提示调用前先对点集做 np.unique 去重重复点会导致面积为 0 的退化三角形在 alpha 较小时产生大量虚假边界。3.2 用 matplotlib 观察三角网和提取结果算法跑通后第一件事永远是可视化不是为了出图而是为了确认“边界断了没有、凹口进没进去”。用上一节的返回值可以很方便地画出过滤后的三角网import matplotlib.pyplot as plt bd_edges, bd_idx, keep, tri alpha_shape(points, 0.5) plt.figure(figsize(8, 8)) # 画保留后的三角形 plt.triplot(points[:, 0], points[:, 1], np.array(keep), lw0.3, colorgray) # 画边界点 plt.scatter(points[bd_idx, 0], points[bd_idx, 1], s6, cred) plt.axis(equal) plt.show()triplot 的第二个参数接收三角形顶点索引数组这里直接传入 keep 列表画出来的就是 alpha 过滤后的三角网骨架。边界点用红色标出一眼就能看出这些点是否连成了预期的轮廓。如果红色散点断成了几截说明 alpha 太小如果红色区域比真实边界大了一圈说明 alpha 太大。建议先在大约 0.2 到 2 之间按 0.1 步长扫几轮观察边界形态变化再定最终参数。3.3 代码跑通了然后怎么判断结果好坏把边缘提取做成函数很容易难的是判断输出是否可信。我的验证顺序是先看边界点数是否在合理量级再看边界边总数最后看有没有游离在主体之外的小块三角形。边界点数突然翻倍多半是离群点被当成了边缘边界边总数远大于边界点数说明边界严重断裂每条边都是“一段孤路”。另一个容易被忽略的问题是返回的 boundary_idx 只是点的集合不携带边的连接顺序。如果需要把边缘画成一条连续曲线必须用 boundary_edges 里的成对索引做图遍历不能假设点索引相邻就是空间相邻。这点在第四章的闭合性验证里还会用到。4. alpha 参数怎么选密度不均点云的三个改造技巧4.1 从边长分布估计 alpha 的起步值alpha 不拍脑袋可以从三角网本身的边长分布起步。一次 Delaunay 剖分后统计所有三角形的边长中位数和 95 分位数分别代表了“典型邻接尺度”和“稀疏区域尺度”。alpha 取边长中位数的 1.2 到 1.5 倍通常能得到一个不过度丢失细节的轮廓取 95 分位数的 0.5 倍则偏向更紧凑的边缘。场景起步 alpha 参考调参方向地形边界提取边长中位数 1.5 倍过小则边界碎调大建筑轮廓边长中位数 2 倍以上优先保证闭合再回调机械零件切片边长中位数 0.8 倍细节优先可接受小断裂稀疏散点云95 分位数 0.4 倍用局部密度归一化替代全局 alpha这个表格只是起点真实数据里密度方差大全局 alpha 几乎一定会顾此失彼哪个区域都调不顺就需要下一节的改造。4.2 稀疏区域断边的局部密度归一化单一 alpha 对密度均匀的点云有效遇到“近处密、远处疏”的扫描数据就露馅稀疏区三角形本来就大外接圆半径普遍超标全被过滤掉边界断掉密集区三角形太小过滤不干净内部三角形残留。常见做法是把 alpha 改造为一个与局部密度相关的量而不是全局常数。from scipy.spatial import cKDTree tree cKDTree(pts) dist, idx tree.query(pts, k6) # 每个点取最近 5 个邻居的距离中位数作为局部尺度 local_scale np.median(dist[:, 1:], axis1) def alpha_local(simp): # 取三角形三个顶点的局部尺度均值乘以系数作为该三角形的半径阈值 return 1.2 * np.mean(local_scale[list(simp)])代码逻辑是把每个点的局部点间距中位数算出来三角形的 alpha 阈值等于其三个顶点的局部尺度均值乘系数。稀疏区的三角形阈值自动变大密集区自动变小避免了“一刀切”。这里注意 k 一般取 5 到 10太小受噪点影响大太大又平滑掉了密度差异。代价是计算量从 O(n log n) 变成 O(n log n nk)k 固定时整体仍然是准线性的。4.3 离群点、重复点与内部孔洞先处理再做三角网Delaunay 对点的存在性很敏感一个远离主体的离群点会强行连出几个超大三角形直接把局部边界撑变形。虽然 alpha 过滤能删掉大部分这种三角形但离群点本身还是会作为孤立边界点残留下来。更麻烦的是重复点两个坐标几乎相同的点会让三角形面积趋近于 0外接圆半径被算成巨大数值导致该区域的正常三角形也被连坐剔除。我的固定流程是先按统计滤波删离群点用每个点到最近邻居的距离分布设定阈值超过均值加三倍标准差就删再做坐标量化去重防止扫描仪拼接产生的重复点进入三角网。内部孔洞是另一个容易被误读的现象——孔洞边界和外部轮廓在算法眼里都是自由边如果只想提取外部轮廓可以在提取后做个连通性分析只保留最大连通块的外边界如果想连孔洞一起提取alpha 必须小到能“钻进”孔洞区域否则孔洞会被大三角形直接跨过去。5. 基于 Delaunay 三角网的三维点云边缘提取投影、法向约束与验证5.1 三维直接用四面体过滤为什么不好用scipy.spatial.Delaunay 并不是只能处理二维点集把三维坐标传进去它同样能完成剖分只是 simplices 从三角形变成四面体。理论上可以照搬 alpha shape 思路用四面体外接球半径过滤再提取自由三角面作为表面。但实际工程里我很少这么做三维 alpha shape 的参数敏感度比二维高太多了alpha 稍微调大表面孔洞全部消失稍微调小整个表面碎成几百块。而且四面体的自由面在视觉效果上不是封闭网格Open3D 和 CloudCompare 里都难以直接检查。更稳的路径是投影法。5.2 投影到二维平面提取边缘再回映射坐标把三维点云压到二维平面提取二维边缘再把边界点的索引映射回三维坐标这是最可靠的三维点云边缘提取流程。投影平面的选择决定提取结果。地形点云沿 Z 轴投影到 XY 平面提取的是水平轮廓立面扫描点沿 X 轴或 Y 轴投影提取的是立面外轮廓。投影之后密度会重叠扫描重叠区域的点挤在同一个二维位置对三角网不构成致命问题但会让边界点索引出现“多对一”情况。这时候要保留投影点到原始三维索引的映射表提取到的每个二维边界点都对应一个或多个三维点取最近的那个即可。如果边缘指的是断裂边、台阶边这类有高度跳变的边界而不是投影轮廓那必须在投影之前叠加法向约束把法向与投影方向接近垂直的点筛掉剩下的点才参与建网。因为真正的地形断裂线上点的法向是剧烈翻转的而平面上那些从上方看是“轮廓”的点法向基本不指向投影方向。5.3 闭合性与重叠度的验证方法投影法提取完成后验证边界是否闭合不需要肉眼去看用边的拓扑结构就能算。每个边缘点应该恰好属于两条边界边度数不为 2 的点就是断点from collections import defaultdict deg defaultdict(int) for i, j in bd_edges: deg[i] 1 deg[j] 1 open_ends [v for v, d in deg.items() if d ! 2] print(边界断点数量:, len(open_ends))断点数为 0 表示边界全部闭合大于 0 就说明 alpha 过小或离群点干扰。验证边缘点提取质量还有一个更通用的技巧把每个点的邻域闭合度作为置信度输出而不是只给一个布尔判断。闭合度定义为该点周围存活三角形覆盖的角度总和除以 360 度越接近 1 越像内部点越接近 0 越像孤立碎片点。调试时把置信度低于 0.5 的点单独渲染出来比反复调整 alpha 更直观也更方便定位是哪块区域先崩的。本文还有配套的精品资源点击获取