孔洞修补算法

孔洞修补算法

一、算法概述

1.1 算法名称与定位

算法名称:基于网格邻域和CGAL三维几何库的三角网格孔洞修补算法

算法类型:孔洞检测 + 环带构造 + 三角化 + 各向同性重网格(Hole Detection + Ring Construction + Triangulation + Isotropic Remeshing)

1.2 算法解决的问题

三维重建过程中,由于以下原因,网格模型通常会出现孔洞:

  1. 遮挡问题:拍摄时物体某些区域被其他物体遮挡,导致重建缺失
  2. 反射表面:玻璃、水面等高光反射区域无法被相机捕捉
  3. 弱纹理区域:单色墙面、地面等区域特征点不足
  4. 传感器噪声:深度相机的深度缺失
  5. 数据采集不完整:无法拍摄到的结构底部、内部等区域

孔洞的存在会影响模型的完整性、封闭性和后续使用(如3D打印、有限元分析、体积计算、纹理映射等)。

1.3 算法核心思想

算法包含两种互补的修补策略

策略一:基于邻域特征的修补

  • 对于小型孔洞,提取孔洞周围的三角面作为"引导信息"
  • 构造一个环带(Ring Surface),连接孔洞边界和周围三角面
  • 对环带内部的空区域进行三角化 + 细化 + 平滑,使修补区域与周边几何特征自然过渡

策略二:基于边界折线的直接修补

  • 对于大型孔洞或开放性边界洞,直接使用孔洞的边界折线
  • 对边界多边形进行三角化
  • 再进行各向同性重网格优化三角面质量

二、算法原理与数学公式

2.1 孔洞的数学定义

在流形三角网格 M=(V,F)M = (V, F)M=(V,F) 中,孔洞(hole)定义为:

一条边 e=(vi,vj)e = (v_i, v_j)e=(vi,vj) 是边界边,当且仅当它只被一个三角面 fff 共享:

∣{f∈F:e⊂f}∣≤1|\{f \in F : e \subset f\}| \leq 1∣{f∈F:e⊂f}∣≤1

多个边界边首尾相连构成的闭合环 H=(v0,v1,...,vk−1)H = (v_0, v_1, ..., v_{k-1})H=(v0,v1,...,vk−1),其中 (vi,v(i+1) mod k)(v_i, v_{(i+1)\bmod k})(vi,v(i+1)modk) 都是边界边,构成一个孔洞。

2.2 边界与孔洞提取

半边识别

在半边数据结构中,每条边被分为两个方向相反的半边(halfedge)。边界半边是指其对面(opposite halfedge)不属于任何面的半边:

复制代码
遍历网格的所有半边:
  如果当前半边的对面不存在:
    将该半边标记为边界半边
    加入边界集合
环构建过程

将零散的边界半边连接成闭合环,是一个路径追踪过程:

复制代码
1) 从边界集合中取出一条边界半边作为环的起点
2) 沿着半边的 next 指针追踪到下一条半边
   - 记录当前半边的起点和终点作为环的一个顶点
   - 处理共享边(当多个环共享同一条边时,跳转到共享边的对面继续追踪)
3) 重复步骤2直到回到起点,形成一个闭合环
4) 从边界集合中移除已处理的所有半边
5) 如果边界集合非空,回到步骤1继续提取下一个环
共享边处理

提取过程中的关键难点是共享边的处理。当多个环共享同一条边时(例如外轮廓和内部孔洞之间),算法需要能够正确区分。通过哈希表记录所有已遍历的边,检测到两条边具有相同的起点和终点时,标记为共享边。环追踪时遇到共享边,跳转到共享边的对面继续追踪。

2.3 基于邻域特征的孔洞修补

环带构造原理

对于小型孔洞,修补时需要利用孔洞周围的三角面来推断孔洞区域应有的几何形状。做法是在孔洞边界周围构造一个环带

设孔洞边界由顶点序列 H={v0,v1,...,vn−1}\mathcal{H} = \{v_0, v_1, ..., v_{n-1}\}H={v0,v1,...,vn−1} 组成,以孔洞边界周围的三角面为输入信息。

切向量计算

对于孔洞边界上的每条边 (vi,vi+1)(v_i, v_{i+1})(vi,vi+1),计算其两侧三角面的切向量

t=d2−(d2⋅d^1)d^1∥d2−(d2⋅d^1)d^1∥\mathbf{t} = \frac{\mathbf{d}_2 - (\mathbf{d}_2 \cdot \hat{\mathbf{d}}_1)\hat{\mathbf{d}}_1}{\|\mathbf{d}_2 - (\mathbf{d}_2 \cdot \hat{\mathbf{d}}_1)\hat{\mathbf{d}}_1\|}t=∥d2−(d2⋅d^1)d^1∥d2−(d2⋅d^1)d^1

其中 d^1=p1−p0∥p1−p0∥\hat{\mathbf{d}}_1 = \frac{\mathbf{p}_1 - \mathbf{p}_0}{\|\mathbf{p}_1 - \mathbf{p}_0\|}d^1=∥p1−p0∥p1−p0(边方向),d2=p2−p0\mathbf{d}_2 = \mathbf{p}_2 - \mathbf{p}_0d2=p2−p0(第三个顶点方向)。

本质上这是Gram-Schmidt正交化过程:从 d2\mathbf{d}_2d2 中减去其在 d^1\hat{\mathbf{d}}_1d^1 方向上的投影,得到垂直于边的切向分量。

三种邻域类型处理

环带构造过程中,会遇到三种不同的邻域三角形配置情况:

情况一:孤立三角形构成孔洞

当孔洞边界只有一个三角形,且该三角形的两条边都是边界边时,切向量取两条边的向量和方向,将原顶点沿此方向平移一定宽度得到环带的上层顶点。

情况二:两个相邻三角形(共享顶点)

当两个邻域三角形共享一个顶点时,切向量取共享顶点方向,将原顶点沿此方向平移。

情况三:两个不相邻三角形(通用情况)

取两个切向量的平均方向作为平移方向,使环带的方向兼顾两侧三角形的几何特征。

环带网格生成

将原边界顶点(下层)和平移后的顶点(上层)配对,构成四边形条带,然后将每个四边形分裂为两个三角形:

复制代码
       p1[2] -------- p1[1]
       /|             /|
      / |            / |
  p1[3] -------- p1[0] |
    |   p0[2] -------- p0[1]
    |  /             | /
    | /              |/
  p0[3] -------- p0[0]

每个四边形 (p0[i], p0[i+1], p1[i+1], p1[i]) 分裂为两个三角形:
  △(p0[i], p0[i+1], p1[i])
  △(p0[i+1], p1[i+1], p1[i])
孔洞识别与填充
  1. 提取环带网格的所有边界环
  2. 遍历边界环,找到与孔洞边界起始点匹配的环(即待填充的孔洞)
  3. 对识别出的孔洞执行三角化 + 细化 + 平滑:
    • 三角化:将孔洞多边形三角化,形成初始的三角面填充
    • 细化:在三角化结果中插入新顶点,使网格达到足够的密度
    • 平滑:通过最小化曲率(fairing)使修补区域与周围网格平滑过渡
  4. 提取修补后的三角面,合并到原始网格中

2.4 基于边界折线的直接修补

孔洞多边形三角化

使用孔洞边界折线直接进行三角化,此过程将一个平面多边形剖分为若干三角形。不强制使用Delaunay三角化的原因是:当边界点分布不规则时,Delaunay约束可能导致算法陷入无限循环,使用一般三角化能够保证算法的稳定性。

目标边长计算

为后续重网格步骤计算合适的目标边长:

Ltarget=1n−1∑i=0n−2∥vi+1−vi∥2L_{target} = \sqrt{\frac{1}{n-1}\sum_{i=0}^{n-2}\|\mathbf{v}_{i+1} - \mathbf{v}_i\|^2}Ltarget=n−11i=0∑n−2∥vi+1−vi∥2

即孔洞边界顶点间的均方根边长,作为重网格的目标边长。此参数决定了重网格的精细程度------边长越小,修补网格越密。

各向同性重网格

三角化后的网格可能存在狭长三角面,需要通过各向同性重网格来优化。重网格的迭代步骤如下:

复制代码
对于每次迭代:
  步骤1: 分裂长边
    遍历所有边,如果边长 > (4/3) × L_target
    在该边的中点插入新顶点,将边一分为二
    关联的三角形也随之分裂

  步骤2: 塌缩短边
    遍历所有边,如果边长 < (4/5) × L_target
    将该边塌缩(将两个端点合并)
    移除退化的三角形

  步骤3: 翻转边
    遍历所有内部边,计算翻转前后的角度质量
    如果翻转能提高最小角或最大角的质量,则执行翻转

  步骤4: 切向松弛
    沿表面切向方向移动顶点位置
    使顶点分布更均匀,减少网格扭曲

边界保护:重网格过程中必须保护边界轮廓。如果不保护,由于计算精度误差,修补区域的边界会与原始网格的边界产生缝隙,导致修补区域与周围网格分离。


三、完整算法流程

3.1 基于邻域特征的修补流程

复制代码
输入: 三角网格 + 孔洞邻域三角形列表
输出: 修补后的三角网格

Step 1: 解析孔洞邻域三角面
   ├── 输入三角形列表围成孔洞边界
   └── 每个三角形提供其三个顶点坐标

Step 2: 遍历相邻三角形对,生成环带上下层顶点
   ├── 对每对相邻三角形 (tri[i], tri[i+1])
   ├── 判断邻域类型(孤立三角形/共享顶点/不相邻)
   ├── 根据类型计算切向量方向
   ├── points0[i] = 原边界顶点(下层)
   └── points1[i] = 沿切向量平移一定宽度(上层)

Step 3: 构造环带网格
   ├── 用 points0 和 points1 构成四边形条带
   └── 每个四边形分裂为两个三角形

Step 4: 修补环带内部的孔洞
   ├── 提取环带的所有边界环
   ├── 找到与孔洞起始点匹配的环
   ├── 执行三角化 + 细化 + 平滑
   └── 提取修补后的三角面

Step 5: 合并结果
   ├── 将修补三角面转换为目标格式
   └── 合并到原始三角网格中

3.2 基于边界折线的修补流程

复制代码
输入: 孔洞边界折线顶点列表
输出: 修补后的三角网格

Step 1: 数据格式转换
   └── 将输入顶点转换为算法库需要的格式

Step 2: 三角化孔洞多边形
   ├── 对边界折线进行三角化
   └── 构建临时三角网格

Step 3: 各向同性重网格优化
   ├── 计算目标边长(边界顶点均方根边长)
   ├── 检查网格是否为纯三角网格
   └── 执行 isotropic_remeshing(边界保护模式)

Step 4: 输出修补结果
   └── 将修补后的三角面写入目标网格

3.3 边界提取流程

复制代码
输入: 三角网格
输出: 所有边界环(含外轮廓和内部孔洞)

Step 1: 数据结构转换
   └── 将三角网格转换为半边数据结构

Step 2: 遍历所有半边,筛选边界半边
   ├── 遍历网格的所有半边
   └── 收集所有对面不存在的半边(边界半边)

Step 3: 将边界半边连接成环
   ├── 为每条边界半边创建节点
   ├── 建立前后继关系(双向链表)
   ├── 检测共享边并建立关联
   └── 从起点沿后继追踪直到回到起点

Step 4: 去重与格式化输出
   ├── 跳过共享边(只输出一次)
   ├── 跳过已遍历的边
   └── 返回所有边界环列表

四、应用场景

4.1 三维实景模型的孔洞修补

城市三维重建中,建筑物的遮挡面、车辆/树木遮挡区域会出现孔洞:

  • 利用 getBorderAndHole 检测所有孔洞
  • 对每个孔洞提取其邻域三角面
  • 调用基于邻域的修补或基于折线的修补
  • 可能需要反复迭代直到所有孔洞被修补

4.2 墙面修复

墙体表面因窗户、门洞等开口形成的孔洞,或者因遮挡形成的墙面缺失:

  • 用户框选或绘制需要修复的墙面区域
  • 对选定区域内的孔洞进行识别和修复
  • 拉直弯曲的墙线,同时修补拉直后产生的缝隙

4.3 水表面修复

水面区域的孔洞、断裂或不连续处:

  • 用户选取水面区域范围
  • 对水面区域内的孔洞进行填充
  • 与周边水面保持连续和光滑过渡

4.4 桥接(两个孔洞间)

在两个孔洞或多个网格碎片之间生成桥接网格:

  • 用户选择两个孔洞的边界边
  • 在两条边界边之间等分插值生成顶点
  • 按四边形条带模式生成三角面

4.5 单体化建模

在建筑物单体化中,提取的屋顶、墙面等单体模型常常包含孔洞,需要修补后才能进行纹理映射和布尔运算。


五、算法复杂度

阶段 时间复杂度 说明
边界提取 O(F)O(F)O(F) 遍历所有半边,FFF为面数
环带构造 O(k)O(k)O(k) kkk为孔洞边界顶点数
三角化+细化+平滑 O(k2)O(k^2)O(k2)~O(k3)O(k^3)O(k3) 取决于孔洞大小
各向同性重网格 O(mlog⁡m)O(m\log m)O(mlogm) mmm为修补后三角面数

空间复杂度:O(V+E+F)O(V+E+F)O(V+E+F)


六、算法优缺点

优点

  1. 双策略互补:基于邻域(保持局部特征)和基于折线(处理大孔洞)两种策略
  2. 高质量修补:包含平滑步骤,修补区域与周围网格过渡自然
  3. 三角面质量优化:重网格确保修补后无狭长三角面
  4. 边界保护:防止修补区域与原网格分离
  5. 功能全面:一函数同时提取外轮廓和内部孔洞

缺点

  1. 大型孔洞质量有限:跨结构的大孔洞可能产生不合理的几何形状
  2. 无纹理预测:修补区域无纹理,需额外步骤补全
  3. 非流形输入容错差:输入必须为流形网格
  4. 参数依赖:目标边长和迭代次数需根据模型尺寸调整

七、关键公式汇总

  1. 边界边判定:∣{f∈F:e⊂f}∣≤1|\{f \in F : e \subset f\}| \leq 1∣{f∈F:e⊂f}∣≤1

  2. 切向量计算(Gram-Schmidt):t=d2−(d2⋅d^1)d^1\mathbf{t} = \mathbf{d}_2 - (\mathbf{d}_2 \cdot \hat{\mathbf{d}}_1)\hat{\mathbf{d}}_1t=d2−(d2⋅d^1)d^1

  3. 目标边长:Ltarget=1n−1∑i=0n−2∥vi+1−vi∥2L_{target} = \sqrt{\frac{1}{n-1}\sum_{i=0}^{n-2}\|\mathbf{v}_{i+1} - \mathbf{v}_i\|^2}Ltarget=n−11∑i=0n−2∥vi+1−vi∥2

  4. 重网格长边分裂阈值:Lsplit=43LtargetL_{split} = \frac{4}{3} L_{target}Lsplit=34Ltarget

  5. 重网格短边塌缩阈值:Lcollapse=45LtargetL_{collapse} = \frac{4}{5} L_{target}Lcollapse=54Ltarget

  6. 法线偏差:Ndev=1−nTnew⋅nToriginalN_{dev} = 1 - \mathbf{n}_T^{new} \cdot \mathbf{n}_T^{original}Ndev=1−nTnew⋅nToriginal

相关推荐
随意起个昵称1 小时前
线性dp-计数类题目9(斐波那契字符串)
算法·动态规划
菜菜的顾清寒1 小时前
力扣HOT100(49)动态规划 -- 打家劫舍
算法·leetcode·动态规划
葡萄城技术团队1 小时前
观察生活:人是如何分词的
算法·生活
装不满的克莱因瓶1 小时前
什么是特征分解?从数学定义到现实问题的映射
人工智能·数学·算法·机器学习·ai·特征分解
killerbasd1 小时前
总结 6.1
算法
「維他檸檬茶」1 小时前
大模型算法学习2026.6.1
学习·算法·大模型·nlp
玖釉-1 小时前
LeetCode Hot 100 知识点总结与算法指南
c++·windows·算法·leetcode
填满你的记忆1 小时前
《动态规划-基础篇》
算法·动态规划·力扣
进击的荆棘1 小时前
优选算法——队列+宽搜
数据结构·c++·算法·leetcode·bfs·队列