ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

2DGS高斯溅射三维重建:从高斯中心点到三角网格的完整提取方法

2DGS高斯溅射三维重建:从高斯中心点到三角网格的完整提取方法 2DGS全称是 2D Gaussian Splatting最近在三维重建圈子里热度不低。它把场景表示成一张张二维高斯基元训练完之后最有意思的一件事是你可以直接从这些高斯中心点生成三角网格不需要再走一遍“点云→隐式函数→等值面提取”的传统老路。这篇文章就围绕“如何从高斯生成三角网格”这个主题把背后的核心原理、算法选择和实现细节一步步拆开。想快速拿到干净网格的开发者、以及准备把2DGS接进渲染管线的朋友都能在下面找到可以直接参考的方案。1. 为什么是2D高斯而不是3D高斯从椭球到平面1.1 3DGS的基元是“体积”不是“表面”3D Gaussian Splatting 把每个基元定义成一个三维高斯椭球有位置、协方差矩阵、不透明度和颜色。它在渲染上确实漂亮但轮到几何提取时就成了麻烦事椭球在空间里有体积彼此层层重叠不透明度又是连续过渡的整个场景没有一个明确的表面边界。直接把椭球中心拿出来连成网格拓扑完全对不上走 Marching Cubes 又得先在体素格上构造标量场而 3DGS 并没有提供这个标量场的可靠定义。常见的办法是把高斯中心点转成稠密点云再用 Poisson 重建。这种做法我在早期项目里试过很多次效果能用但细节丢得很厉害。原因是 Poisson 重建本质上是在解一个隐式场它会把高斯之间的软过渡平滑成一块“平均表面”那些薄结构、小孔洞和边缘棱角最容易消失。而且点云密度不均匀时重建结果会出现明显鼓包。3DGS 在渲染层面越强这个几何提取的短板就越刺眼。1.2 2DGS的基元自带法向和切平面2DGS 的变化从名字就能看出来每个基元不是三维椭球而是一个二维高斯平面。数学上用一个中心点 p、两个切向量 t_u 和 t_v、以及两个尺度 s_u 和 s_v 定义。这个东西展开之后就是空间里一个有方向、有大小、边缘柔软的小平面片有点像一堆会发光的“贴纸”贴在场景表面上。这带来一个关键性质每个基元自带法向法向量直接等于 normalized(cross(t_u, t_v))。意味着在训练阶段模型就已经在优化“表面朝向”这个信息了。网格提取的核心难点是搞清楚“表面在哪里、表面朝向哪里”2DGS 等于把答案直接写在了参数里所以后处理阶段只需要把 patch 之间的连接关系找出来难度降了一个量级。我个人的感受是2DGS 与其说是一个渲染方案不如说是一个可微表面表示三角网格只是它的自然产物。1.3 从3DGS到2DGS几何约束发生了什么变化很多人一听到“高斯”就想到高斯模糊、高斯滤波、GMM 聚类这些虽然都姓“高斯”但与 2DGS 完全是两码事。高斯模糊是图像卷积核GMM 是概率聚类模型而 2DGS 和 3DGS 属于同一个可微渲染技术家族。真正值得注意的是 3DGS 到 2DGS 之间几何约束的转变。3DGS 的协方差矩阵有 6 个自由度椭球可以被拉伸成任何方向它其实是在用体积代理表面靠大量半透明基元叠出视觉结果2DGS 则把基元限制在二维平面上自由度虽然减少了但换来的是明确的几何语义。每一个 patch 都是一个“表面候选”法向、切向、尺度全部有物理含义。配合深度正则化和法向一致性约束训练出来的高斯分布会主动贴合真实表面这为后面直接提取三角网格打下了基础。2. 从2D高斯中心点走向三角网格整体设计思路2.1 把高斯中心点当作带法向的表面采样点要理解 2DGS 的网格提取最直观的方式是放弃“高斯基元”这个视角把每个中心点看成一次表面采样。每个点有一个位置、一个法向、一个覆盖半径本质上就是带法向的稀疏点云。三角网格的任务就是给这些点确定连接关系。那么连接关系从哪里来3D 点云重建里有个经典陷阱没有法向信息的点云任何剖分算法都容易在薄板表面和内部之间乱连。2DGS 的优势在于每个点自带法向所以两个点能不能连成三角形可以先看它们法向是否一致再看它们之间的距离是否小于 patch 覆盖范围。这两个条件同时满足才说明它们大概率属于同一个表面片。这样生成的网格拓扑从一开始就是“贴表面”的而不是在三维空间里硬猜。2.2 连接关系从哪里来尺度、重叠和邻接图真正构建连接关系时高斯尺度 s_u 和 s_v 是关键参数。每个 patch 在表面上有一定的覆盖面积中心点距离越近、尺度越大两个 patch 的重叠区域就越大这条边就越可靠。我在工程里不是用固定 k 近邻而是用“半径搜索 尺度引导”先算每个点的平均 patch 半径然后以该半径的一定倍数作为搜索半径找邻居。这个设计是有原因的。固定 k 近邻在密度均匀的小场景里没问题但大场景近处和远处的高斯密度差异很大同样取 20 个邻居近处可能已经覆盖了大半个墙面远处却连一个表面都还没跨过去。尺度引导的半径搜索则可以自适应地匹配 patch 大小避免把不相关的表面强行连到一起。实际操作中半径取相邻高斯基元平均尺度的 1.5 倍左右比较稳太小会破洞太大会跨表面。2.3 为什么不是泊松重建每次聊到从点云出网格一定会有人问为什么不直接泊松重建我并不是否定泊松它确实成熟但用在 2DGS 上不是最优选择。泊松重建需要一个连续、一致的定向法向场。2DGS 训练出来的法向整体质量不错但 patch 之间多少会有微小裂缝和噪声泊松会把它们当作真实表面起伏结果要么细节被整体磨平要么在不该鼓包的地方鼓包。相比之下基于 Delaunay 的剖分直接使用高斯基元的拓扑信息不做隐式平滑能保留更多锐利边缘。还有一个工程原因是大规模重建时的内存问题。泊松重建在百万级点云上要解一个大型稀疏线性系统内存占用很容易把机器搞崩三角剖分则可以分块计算、分块合并配合最远点采样几十万点的场景在消费级显卡上也能跑完。所以在 2DGS 的流水线里我默认选择基于剖分的提取方案。2.4 一条完整的提取管线把整套流程抽象出来大概是下面这个顺序从训练完成的 2DGS 模型导出中心点、切向量、尺度和不透明度。按不透明度阈值过滤掉透明基元清理异常尺度点。计算每个点的法向并做全局一致性定向。如果用点数太多做最远点采样或体素栅格下采样。基于尺度做半径搜索构建邻接图。对每个点的邻居投影到局部切平面做 2D Delaunay 剖分。合并所有局部三角形去重边、清理退化三角形。做顶点位置优化或拉普拉斯平滑得到最终网格。后面几个小节会按这个顺序逐个拆开讲重点放在我实际踩过坑的地方。3. 实现拆解可直接参考的高斯→网格流程3.1 第一步读取高斯参数并做预处理训练好的 2DGS 模型导出的文件里通常包含这几组数据means 是 N×3 的中心点scales 是 N×2 的尺度tangents 是 N×3×2 的切向量opacity 是 N×1 的不透明度另外还有球谐系数做颜色。不同开源实现存储格式不完全一样有的直接给切向量有的给旋转四元数需要反算读取前一定要先确认字段含义。预处理阶段我会做三件事。第一不透明度小于 0.1 的基元直接丢掉它们对表面几何几乎没有贡献留着只会把邻接关系搅浑。第二尺度异常大的 patch 要处理如果某几个 patch 的尺度比其他 patch 大两个数量级它们会像“桥”一样把不同表面连接到一起导致后续剖分出现跨表面三角形。第三清洗 NaN 和极端坐标值训练后期偶尔会有高斯漂移到场景外不清理会在剖分阶段引发莫名其妙的缝合线。3.2 第二步法向计算与全局定向法向计算本身很简单就是切向量叉乘。真正麻烦的是方向定向。因为 t_u 和 t_v 的顺序在训练中并不稳定叉乘得到的法向可能指向表面内侧也可能指向外侧整个场景的法向场是“朝向混乱”的直接拿去剖分一定会出问题。我常用的办法是 BFS 传播定向。首先建 k 近邻图从任意一点开始如果邻居点法向与当前点法向点积为负就把邻居法向翻转然后继续传播。这个方法在开放场景下很有效大部分 patch 最终会保持一致朝向。但有一个坑遇到封闭物体时内表面和外表面天然朝向相反BFS 会把其中一个表面的法向强行翻到另一边导致拓扑错乱。这种情况下我建议改做视线定向统计每个高斯被哪些相机看到取这些相机方向作为参考如果法向与参考方向的平均夹角超过 90 度就翻转。以“表面在相机一侧”作为先验通常比纯几何传播更可靠。3.3 第三步切平面投影Delaunay剖分Delaunay 剖分在点云重建里是最常用的工具之一它保证三角形最小角最大化可以减少狭长三角形。但直接在三维点云上做 Delaunay 会在内部生成大量四面体不是我们想要的表面网格所以正确的思路是“降维处理”把每个点周围的邻居投影到当前点的切平面上在二维空间里做 Delaunay再把三角形顶点映射回三维。这个过程实现起来有几个细节。首先每个点都要构建一个局部正交坐标系以法向为 z 轴另选一个与法向不平行的向量作为 x 轴再叉乘出 y 轴。选 x 轴时最好根据法向的绝对值来避兔数值退化比如当法向接近 z 轴时就不要选 (0,0,1) 作为候选向量。然后把邻居点与该点做差分别点乘 x 轴和 y 轴得到二维坐标。接下来用 scipy.spatial.Delaunay 对二维点做剖分最后把 simplices 索引映射回原始三维点索引。我在实际项目中会额外加一个法向一致性过滤只有当邻居法向与当前点法向点积大于 0.5 时才参与投影。这个简单的条件可以过滤掉很多跨表面的错误连接效果非常明显。3.4 第四步三角形网格的后处理与顶点优化剖分得到的网格顶点都位于高斯中心点上拓扑基本正确但表面会有一点粗糙因为中心点之间并不完全贴合真实表面。此时可以做两件事第一是拉普拉斯平滑。每个顶点移动到邻接顶点的平均值位置连续迭代几轮小锯齿会被磨掉。注意不要迭代太多否则会把锐利边缘磨圆。第二是顶点位置优化。把顶点坐标设为可优化变量能量函数包含两部分一部分是顶点与原始高斯中心点的距离防止跑偏另一部分是拉普拉斯项控制平滑度。我习惯用 PyTorch 写几十行代码做几十步优化实测下来比单纯拉普拉斯平滑更稳因为它在“贴合原始数据”和“表面顺滑”之间取了平衡。顺便提一句如果只是想要一个低模预览效果前面的步骤已经够了。如果想要可以放进 DCC 工具里继续雕刻的高模建议再做一次表面细分和纹理重投影后面会细说。3.5 一份可跑的Python代码骨架下面这段代码是提取管线里最核心的切平面投影 Delaunay 剖分部分。它接收中心点、法向和每点的局部半径返回一个三角形索引数组。import numpy as np from scipy.spatial import cKDTree, Delaunay def tangent_delaunay(pos, normal, radius): pos: (N, 3) 中心点 normal: (N, 3) 单位法向 radius: (N,) 每个点的搜索半径 tree cKDTree(pos) all_tris [] for i in range(len(pos)): # 1. 半径搜索邻居 idx tree.query_ball_point(pos[i], rradius[i]) idx [j for j in idx if j ! i] # 2. 法向一致性过滤 idx [j for j in idx if np.dot(normal[i], normal[j]) 0.5] if len(idx) 3: continue # 3. 构造局部正交标架 z normal[i] / (np.linalg.norm(normal[i]) 1e-8) ref np.array([1.0, 0.0, 0.0]) if abs(z[0]) 0.9: ref np.array([0.0, 1.0, 0.0]) x ref - np.dot(ref, z) * z x x / (np.linalg.norm(x) 1e-8) y np.cross(z, x) # 4. 投影到切平面 pts2d [] for j in idx: d pos[j] - pos[i] pts2d.append([np.dot(d, x), np.dot(d, y)]) pts2d np.array(pts2d) # 5. 二维 Delaunay tri Delaunay(pts2d) local_v np.array(idx)[tri.simplices] all_tris.append(local_v) if len(all_tris) 0: return np.zeros((0, 3), dtypeint) return np.vstack(all_tris)这段代码生成的是“局部三角形集合”会有重复边和重叠三角形。下一步需要把相同边去重并基于边邻接关系提取连通三角面片推荐用 Open3D 或者 trimesh 处理这层逻辑。另外函数是逐点循环大场景下建议先用最远点采样把点数压下来再跑。3.6 性能与内存优化高斯数量上到两百万以后逐点做剖分会非常慢。我一般先用体素栅格下采样把点数降到五十万以内网格精度损失其实很小因为高斯基元本身就有重叠中心点密度大于 patch 覆盖需求。接着可以按空间分块处理把场景切成若干 bbox每个块独立做剖分最后在块边界处缝合。缝合时相邻块要预留一部分重叠高斯否则边界会出现明显的缝。实测在单张 RTX 4090 上五十万点做一遍剖分大概三到五分钟如果用了半径搜索过滤能更快。内存方面最终三角形数量会比输入点数多一到两倍五十万点约产生一百万到一百五十万个三角形这个量级的网格在现代软件里已经能非常流畅地编辑。4. 实战中常见的坑与排查速查4.1 三角网格破洞破洞是网格提取时最常遇到的问题症状是某些区域出现空腔或者大片三角形缺失。原因几乎都指向密度不足也就是说这些区域的高斯中心点间距大于半径搜索的覆盖范围。2DGS 训练时场景边缘、自遮挡区域、弱纹理区域的 patch 密度天然会稀疏等提取时再补救就比较被动。遇到破洞我建议先回到训练阶段检查深度正则化是否加够了。2DGS 的深度正则化会让 patch 更贴近表面密度分布也会更均匀如果训练时只追求 PSNR很容易得到渲染好看但几何松散的模型。训练已经结束后补救手段是适当放大搜索半径以及用最远点采样后的局部插值把孔洞区域补一点伪点。但伪点会影响精度只能作为过渡方案。4.2 法向翻转导致剖分错乱这是提取出烂网格的头号原因。症状是三角形出现大面积穿插或者同一个平面被拆成内外两层甚至出现“蝴蝶结”一样纠缠的面。根本原因就是 3.2 小节里说的法向朝向不一致。BFS 定向在开放场景有奇效但在封闭物体上很容易翻车。我现在的项目做法是“先视线定向再 BFS 微调”。训练阶段每个高斯都能记录它影响了哪些视角统计这些视角的平均方向如果法向和平均方向夹角大于 90 度就翻转。然后用 BFS 对局部不一致的地方做微调。视线定向依赖相机位姿正好 2DGS 训练数据里一定有位姿信息所以实现成本很低。4.3 高斯密度不均匀密度不均匀通常表现为很多细节的区域法向正确、三角形小平坦的区域高斯量少三角形巨大甚至直接连成一条长条。根因是各个区域的观测频率不同训练算法把资源集中在信息多的地方。遇到这种情况网格后处理阶段可以做一个自适应细分对大三角形按面积阈值继续细分保持密度均衡。但这只是表面功夫真正有效的解决方案还是在训练时加一个局部密度正则强制 patch 的尺度和分布尽量均匀。正则权重不用太高我一般从 0.01 起步观察网格提取结果逐步微调。4.4 提取网格和渲染画面不一致这个问题挺容易让人困惑渲染结果又锐利又清晰导出网格却显得模糊、平滑过度。原因是 2DGS 的渲染颜色主要由大量半透明 patch 叠加产生颜色信息集中在 patch 中心而网格提取只使用了中心点的几何连接颜色细节仍留在球谐系数里。网格平面上的顶点之间并没有做真正的纹理映射。所以提取网格后别忘了做颜色重投影。把每个顶点的颜色从相邻高斯基元的球谐系数中采样出来或者直接做纹理烘焙。2DGS 开源仓库里提供了这类工具我建议优先用官方实现比自己从零写要省不少时间。做完这一步网格画面和渲染画面才真正对齐。整理成速查表就是下面这样现象根本原因常用解法网格破洞局部高斯密度不足训练时加深度正则、提取时放大半径或插值补点三角形穿插法向定向不一致视线定向BFS微调组合大面积三角形密度分布不均后处理自适应细分、训练时加密度正则网格视觉模糊颜色信息未重投影从球谐系数重采样顶点颜色并做纹理烘焙跨表面错误连接尺度异常patch桥接过滤异常尺度、法向一致性过滤最后说点实操体会把 2DGS 接入资产管线这段时间我最大的感受是网格提取本身只占三成工作量七成在训练阶段。高斯参数的质量决定后面所有操作的成败。如果你只是想要一个能进游戏引擎的低模不需要拼命保留所有 patch勇敢做最远点采样和顶点优化一张精度合理、面数可控的三角网格往往比追求毫米级重建更容易落地。最后再分享一个实用技巧提取之前把透明度过低的高斯删掉看起来只是减少了渲染基元实际上会让网格提取的半径搜索干净很多这个规则我用了好几次每次都奏效。
返回列表