DeepSDF:学习连续符号距离函数
论文:DeepSDF: Learning Continuous Signed Distance Functions for Shape Representation
作者:Jeong Joon Park, Peter Florence, Julian Straub, Richard Newcombe, Steven Lovegrove
arXiv:1901.05103v1
本地文件:/root/other/paper/1901.05103v1.pdf
这篇论文解决什么问题
传统 3D 表示各有明显缺点:
| 表示 | 优点 | 问题 |
|---|---|---|
| 点云 | 轻量,贴近传感器输入 | 没有显式拓扑,不直接给出封闭表面 |
| 网格 | 表面表达直接,适合渲染 | 顶点数量、拓扑结构不固定,学习起来麻烦 |
| 体素 | 规则网格,适合 CNN | 三维分辨率成本极高,细节容易受限 |
| 传统 SDF | 有 inside/outside 和距离信息 | 通常只表示单个形状,离散 SDF 仍然吃内存 |
DeepSDF 的目标是:
用一个神经网络学习一类物体的连续 signed distance function,使 3D 形状可以通过低维 latent code 紧凑表示。
它不是输出点云或网格,而是学习一个连续函数:
其中:
- 是形状 latent code。
- 是查询点坐标。
- 是该点到形状表面的 signed distance。
最终表面由零水平集给出:
图源说明:根据 Park et al., DeepSDF 的 signed distance field 与 zero level set 概念重绘,非论文原图。
SDF 表示的直觉
Signed distance function 对空间中每个点返回一个数:
含义是:
- :点在表面上。
- :点在物体内部。
- :点在物体外部。
- :点到表面的距离。
所以一个形状不再被看成一堆三角形,而是被看成三维空间里的一个连续标量场。
这和你前面看的 implicit surface 很接近:
只不过 DeepSDF 把这个 学成了一个 neural network。
单形状 DeepSDF
如果只表示一个形状,可以训练一个网络:
训练数据是一组空间采样点和真实 SDF 值:
论文使用 clamped L1 loss:
其中:
这个截断距离 的作用是:让网络重点学习表面附近的 SDF,而不是把远离表面的距离也拟合得很精确。
对重建来说,最重要的是零水平集附近是否准确,因为最终表面来自 。
从单个形状到一类形状
只为每个形状训练一个网络没有太大意义。DeepSDF 更重要的部分是:用同一个 decoder 表示一整个 shape class。
网络输入变成:
输出是:
也就是说:
- 决定“查空间中的哪个点”。
- 决定“查哪一个形状”。
可以把 理解成形状压缩编码。不同 chair、airplane、sofa 会有不同 latent code,但共享同一个 decoder。
图源说明:根据 Park et al., DeepSDF 的 shape code conditioned decoder 概念重绘,非论文原图。
模型架构:输入与输出
DeepSDF 的核心网络不是 encoder-decoder,而是一个 conditioned MLP decoder。它学习的是连续 SDF 函数:
decoder 的输入
形状 latent code
表示某一个具体形状的低维编码。论文常用 。同一类别里的不同形状,例如不同椅子,会有不同的 。
查询点坐标
是连续 3D 空间中的任意点:
decoder 实际接收的是拼接后的向量:
也就是说, 决定“是哪一个形状”, 决定“查询这个形状空间中的哪个位置”。
decoder 的输出
输出是一个标量 signed distance:
它的含义是:
- :查询点在物体内部。
- :查询点在物体表面。
- :查询点在物体外部。
- :查询点到表面的距离。
最终 mesh 不是网络直接吐出来的,而是在大量查询点上计算 ,再提取零水平集:
通常用 Marching Cubes 从这个零水平集中提取三角网格。
训练和推理时的输入输出差别
| 阶段 | 输入 | 优化/计算 | 输出 |
|---|---|---|---|
| 训练 | 多个训练形状的 SDF 样本 | 同时优化 decoder 参数 和每个训练形状的 | 学到共享 decoder,以及训练集中每个形状的 latent code |
| 推理 | 新形状的部分 SDF / 点云观测,以及查询点 | 固定 ,只优化新形状的 | 新形状的 SDF 场 |
| mesh 提取 | 固定 后的大量 3D 查询点 | 前向计算 | 零水平集 mesh |
所以 DeepSDF 的“模型输出”严格来说是 SDF 值;重建出的 mesh 是对 SDF 场后处理得到的结果。
图源说明:根据 Park et al., DeepSDF 的 auto-decoder 训练与推理机制重绘,非论文原图。
输出可以用来干什么
DeepSDF 的输出是连续空间中任意点的 signed distance:
这个标量输出本身不是最终可视化模型,但它可以支持很多后续操作。
| 输出 | 可以用来做什么 | 具体做法 |
|---|---|---|
| 单点 SDF 值 | 判断点在物体内外 | 为内部, 为外部, 在表面 |
| 大量点的 SDF 场 | 提取三角网格 | 在规则网格上查询 SDF,再用 Marching Cubes 提取 等值面 |
| 优化得到的 latent code | 压缩表示一个形状 | 用一个低维向量代表完整 3D 形状 |
| 不同形状的 latent codes | 形状插值 | 在 和 之间线性插值,再解码出连续变化的形状 |
| 部分观测优化出的 | 形状补全 | 用不完整点云约束 ,再由 learned shape prior 补出缺失区域 |
| SDF 关于坐标的梯度 | 估计表面法向 | 在表面附近对 SDF 求空间导数,得到可用于渲染和几何处理的 normal |
最常见的用途是 3D reconstruction:
latent code z
-> query dense 3D grid
-> get SDF value at each grid point
-> extract zero level set
-> mesh
因此 DeepSDF 的输出可以理解为“一个连续可查询的形状场”。mesh、normal、补全结果、插值结果,都是在这个 SDF 场基础上进一步得到的。
Auto-decoder 思想
DeepSDF 没有训练 encoder。
普通 auto-encoder 是:
shape -> encoder -> latent code -> decoder -> reconstructed shape
DeepSDF 使用 auto-decoder:
latent code + query point -> decoder -> SDF value
训练时,每个训练形状直接维护一个可优化的 latent code:
这些 code 和网络参数 一起通过反向传播优化。
图源说明:根据 Park et al., DeepSDF 的 auto-decoder learning scheme 概念重绘,非论文原图。
训练目标
对于第 个形状,其 SDF 样本为:
论文先从 posterior 写起:
为什么 posterior 可以写成 prior 乘以所有样本 likelihood?
这个公式是在说:给定第 个形状的 SDF 观测样本 ,这个形状 latent code 有多可信。
先看符号:
- :第 个形状的 latent code。
- :第 个形状的全部 SDF 采样点集合。
- :一个训练样本,表示在空间点 处,真实 SDF 值是 。
- :latent code 的先验概率,表示这个 code 本身是否合理。
- :在给定 code 和查询点 时,网络预测出 SDF 值 的概率。
从贝叶斯公式出发:
其中 和要优化的 无关,所以在最大化 posterior 时可以忽略这个分母:
接下来要展开:
是很多 SDF 样本组成的集合:
论文这里隐含了一个常见假设:在给定形状 code 后,各个采样点的 SDF 观测是条件独立的。
也就是说,一旦已经知道“这是哪个形状”,那么每个查询点的 SDF 值都可以由同一个 decoder 单独解释:
把它代回去,就得到:
论文里直接写成:
严格说,这里省略了归一化常数 。因为训练和推理时只关心哪个 让 posterior 最大,常数项不会影响最优解。
直观理解就是:
一个 latent code 是否好,要同时满足两件事:它本身像一个合理 shape code,并且它能解释这个形状的所有 SDF 采样点。
其中:
负责约束 code 不要跑到离谱位置;
负责要求这个 code 能解释所有观测点的 SDF 值。
为什么是乘积?因为每个样本都要被解释好。如果某个点的 likelihood 很低,整个乘积就会变小,说明这个 code 对该形状整体解释不好。
实际优化时通常取负对数,把乘积变成求和:
如果把 likelihood 写成:
这里的 是 loss function,也就是“预测 SDF 值和真实 SDF 值之间的误差”。
网络预测值是:
真实 SDF 值是:
所以:
就是在衡量:
网络在点 处预测的 SDF,和真实 SDF 差了多少。
如果使用普通 L1 loss,就是:
DeepSDF 论文实际使用的是 clamped L1 loss:
其中:
也就是说,距离太远的 SDF 值会被截断到 范围内。这样做是为了让网络更关注表面附近,因为最终曲面来自:
为什么 likelihood 写成 ?
这是把“误差越小,概率越大”写成概率形式的一种常见方式:
- 如果 ,说明预测完全正确,则 。
- 如果 ,则 。
- 如果 ,则 。
所以:
表达的是:
预测误差越小,这个 SDF 观测样本在当前 latent code 下越可信;预测误差越大,likelihood 越低。
这里可以把它理解成一个未归一化的概率模型。DeepSDF 主要关心优化目标,因此关键是它取负对数以后刚好回到 loss:
那么:
如果先验是高斯:
那么负对数先验会变成 L2 正则:
所以最大化 posterior 等价于最小化:
这就是 DeepSDF auto-decoder 训练目标的来源。
训练时优化:
这个目标包含两部分:
- 重建误差:让网络预测的 SDF 接近真实 SDF。
- latent code 正则:让 不要跑得太散。
第二项很重要。它让 latent space 更紧凑、更连续,也让后续插值和补全更稳定。
为什么 latent code 要加 L2 正则?
如果没有正则,每个训练形状的 code 可以随便跑到 latent space 的任意位置。
这样虽然训练集可能拟合得很好,但 latent space 会很松散:
- 两个相近 code 不一定对应相近形状。
- 插值时可能经过无意义区域。
- 测试时优化 code 更容易陷入坏解。
论文从概率角度把 code 看成服从零均值高斯先验:
对应到优化目标里,就是:
所以 L2 正则的含义是:鼓励所有形状 code 聚集在原点附近,形成一个更紧凑的 shape manifold。
推理阶段怎么重建新形状
测试时,网络参数 已经固定。
对于一个新形状,没有 encoder 直接输出 latent code,所以 DeepSDF 通过优化求一个 code:
这一步可以用于:
- 已知完整形状的重建。
- 给定部分深度图或点云时的 shape completion。
直观地说,就是在 latent space 里寻找一个形状 code,让它生成的 SDF 尽量符合观测数据。
DeepSDF 训练和推理到底分别在优化什么?
DeepSDF 最容易混淆的一点是:它没有 encoder,所以训练和推理都涉及“优化 latent code”。但两者优化的对象不一样。
训练阶段
训练时,已知训练集中有 个形状:
每个形状 都是一组 SDF 采样:
DeepSDF 给每个训练形状分配一个可学习 latent code:
同时还有一个共享 decoder:
训练时优化的是两类变量:
- decoder 参数 。
- 训练集中每个形状自己的 latent code 。
也就是:
都会通过反向传播更新。
训练目标是:
可以把训练过程理解成:
给每个训练形状随机初始化一个 latent code
-> 随机采样该形状的一批 SDF 点
-> decoder 根据 z_i 和 x_j 预测 SDF
-> 和真实 s_j 比较,得到 loss
-> 同时更新 decoder 参数 theta 和该形状的 z_i
-> 重复很多轮
训练结束后,decoder 学到了一种“形状类”的连续 SDF 表示能力;训练集的 latent codes 则记录了每个训练形状在 latent space 中的位置。
推理阶段
推理时,来了一个新形状:
此时 decoder 参数 已经固定,不再更新。
DeepSDF 要做的是:为这个新形状找一个新的 latent code:
所以推理阶段优化的是:
而不是 。
推理目标是:
可以理解成:
固定训练好的 decoder theta
-> 随机初始化一个新 latent code z
-> 用当前 z 和观测点 x_j 查询 SDF
-> 比较预测 SDF 和观测 SDF
-> 只更新 z,不更新 theta
-> 优化收敛后得到 z_new
这一步有点像“给新形状配一个最合适的参数”,而不是重新训练整个模型。
推理完成后如何得到 mesh
得到 后,新形状就由下面这个连续函数表示:
表面仍然是零水平集:
如果要可视化成 3D mesh,通常做法是:
在 3D grid 上采样很多 x
-> 计算 f_theta(z_hat, x)
-> 得到规则网格上的 SDF 值
-> 用 Marching Cubes 提取 F(x)=0 的等值面
-> 得到 triangle mesh
也可以用 raycasting 直接渲染隐式表面。
完整例子
假设训练集中有 10000 把椅子。
训练阶段:
- 每把椅子都有自己的 latent code。
- 所有椅子共享同一个 decoder。
- 训练时同时更新 decoder 和这些训练椅子的 latent code。
推理阶段:
- 来了一把新的椅子,只看到部分点云或部分 SDF 采样。
- 固定训练好的 decoder。
- 新建一个 latent code 。
- 通过优化让这个 code 生成的 SDF 尽量匹配观测。
- 最后用 提取完整表面。
这就是 DeepSDF 能做 shape completion 的原因:推理时优化出来的 code 会被训练好的 shape prior 约束,倾向于落在“合理椅子”的 latent space 区域里。
一句话总结
训练时:
更新 decoder theta + 所有训练形状的 latent code
推理时:
固定 decoder theta,只优化新形状的 latent code
所以 DeepSDF 的推理不是一次普通 forward pass,而是一个 optimization-based inference。
网络结构
论文使用的是多层全连接网络,而不是 3D CNN。
核心结构:
- 输入:latent code + 3D query coordinate。
- 多层 fully connected layers。
- ReLU hidden activation。
- 输出:一个 SDF 标量。
- 输出层使用 tanh。
补充材料中给出的实验结构是 8 层 FC,hidden dimension 为 512。
一个关键设计是 skip connection:把输入的 latent code 和坐标不仅送入第一层,也在中间层再次拼接进去。论文实验显示,这能提升拟合能力,尤其是网络变深时更明显。
ReLU、BatchNorm 和 WeightNorm 在这里分别是什么?为什么这样用?
论文中这一段描述的是 DeepSDF 的具体 MLP decoder 设计:
input: latent code z + 3D query point x
-> 8 fully connected layers
-> hidden layers: 512 channels + ReLU
-> dropout
-> output: tanh
-> optimizer: Adam
-> normalization: WeightNorm, not BatchNorm
ReLU 是什么
ReLU 是一种激活函数:
也就是说:
- 如果输入 ,输出就是 。
- 如果输入 ,输出就是 。
神经网络如果只有线性层,那么多层线性层叠在一起本质上还是一个线性函数,表达能力很有限。ReLU 的作用是引入非线性,让 MLP 可以拟合复杂函数。
DeepSDF 要拟合的是:
也就是从空间坐标和形状 code 到 SDF 值的复杂连续函数。不同位置的距离值、inside/outside 边界、局部几何细节都不是简单线性关系,所以 hidden layers 需要 ReLU 这样的非线性激活。
在这里用 ReLU 的原因可以概括为:
- 计算简单。
- 梯度传播稳定。
- 适合多层 fully connected network。
- 可以把空间分成很多 piecewise linear 区域,从而逼近复杂 SDF 场。
Batch-normalization 是什么
Batch Normalization,简称 BatchNorm,通常会对一个 mini-batch 内某一层的激活值做标准化:
其中 和 是当前 batch 的均值和方差。
然后再学习一个缩放和平移:
它的常见作用是:
- 减少不同 batch 间激活分布变化。
- 让训练更稳定。
- 允许使用更大的学习率。
- 在 CNN 分类任务里经常很有效。
一个具体数值例子:
假设某一层在一个 mini-batch 里,对同一个 channel 得到 4 个激活值:
batch 均值为:
batch 方差为:
暂时忽略很小的 ,标准差为:
那么第一个激活值 会被标准化为:
第四个激活值 会被标准化为:
如果学习到的缩放和平移是:
那么第一个值最终变成:
第四个值最终变成:
这个例子说明:BatchNorm 的输出依赖当前 batch 的均值和方差。也就是说,同一个输入激活值,如果放在不同 batch 里,归一化结果可能不同。
但 DeepSDF 论文中说 BatchNorm 不稳定,原因可以这样理解:DeepSDF 的输入不是普通图像 batch,而是大量空间查询点和 SDF 值。网络要学习的是一个连续标量场:
如果使用 BatchNorm,某个点的中间特征会依赖“同一个 batch 里还有哪些点”。这对分类任务通常没问题,但对 SDF 回归会比较麻烦。
因为 SDF 需要满足一种点到点之间连续、稳定的函数关系。如果同一个查询点 在不同 batch 组合中经过不同的归一化统计量,输出可能产生额外扰动。
所以在 DeepSDF 这里,BatchNorm 可能破坏连续场回归的稳定性,尤其会影响表面附近精确的零水平集:
Weight-normalization 是什么
Weight Normalization,简称 WeightNorm,不是归一化 batch 里的激活值,而是重参数化神经网络权重。
普通线性层可以写成:
WeightNorm 把权重向量写成“方向 + 长度”:
其中:
- 控制方向。
- 控制尺度。
一个具体数值例子:
假设某个神经元的原始方向参数为:
它的长度是:
所以单位方向为:
如果可学习尺度为:
那么真正用于线性层的权重是:
给定输入:
忽略 bias,输出为:
这个例子说明:WeightNorm 直接控制权重本身的方向和尺度,不需要统计当前 batch 里其他样本的激活值。
这样做的好处是:把权重的方向和大小解耦,让优化更稳定。
和 BatchNorm 相比,WeightNorm 不依赖当前 mini-batch 的统计量。也就是说,同一个查询点 的输出不会因为 batch 里混了哪些其他点而变化。
这对 DeepSDF 很重要,因为它希望网络表示的是一个稳定的连续函数:
而不是一个强烈依赖 batch 统计的函数。
所以论文选择:
不用 BatchNorm
改用 WeightNorm
可以理解为:既想保留归一化带来的优化稳定性,又不想让 SDF 回归依赖 batch 统计。
放到 DeepSDF 里整体理解
DeepSDF 这套设计服务于一个目标:稳定地学习连续 SDF 场。
| 技术 | 是什么 | 在 DeepSDF 里的作用 |
|---|---|---|
| ReLU | hidden layer 的非线性激活 | 提高 MLP 表达能力,拟合复杂 SDF |
| BatchNorm | 用 batch 统计量归一化激活 | 论文发现不稳定,可能干扰连续 SDF 回归 |
| WeightNorm | 重参数化权重方向和尺度 | 稳定优化,同时不依赖 batch 统计 |
| tanh | 输出层激活 | 把 SDF 输出限制在有限范围,配合 clamped SDF loss |
| dropout | 随机屏蔽部分神经元 | 减少过拟合,提高泛化 |
| Adam | 自适应梯度优化器 | 优化网络参数和 latent code 更方便 |
因此,这段网络设计可以概括为:
用 ReLU MLP 提供足够表达能力,用 WeightNorm 稳定训练,用 tanh 和 clamped loss 把学习重点压到表面附近,最终得到一个可微的连续 SDF 函数。
训练完成后,表面是零水平集:
可以用 raycasting 或 Marching Cubes 可视化。
而法向可以直接对输入坐标求导:
这也是神经隐式 SDF 的一个重要优势:网络不仅能给出表面,还能通过反向传播给出解析梯度。
数据采样
训练 DeepSDF 时,需要从网格生成 SDF 样本。
论文的做法大致是:
- 把形状归一化到单位球附近。
- 从多个虚拟相机渲染深度,得到有方向的表面点。
- 在表面附近更密集地采样空间点。
- 对每个采样点计算 signed distance。
表面附近采样更密集是因为:
附近决定最终表面的位置和细节。
实验结果
论文主要验证了四类能力:
- 表示训练集中已知形状。
- 重建未见过的新形状。
- 从单视角深度图或噪声点云完成 shape completion。
- 在 latent space 中进行形状插值。
图源说明:根据 Park et al., DeepSDF 的 latent space interpolation 概念重绘,非论文原图。
对比方法包括:
- OGN:octree / voxel 类方法。
- AtlasNet:parametric mesh 类方法。
- 3D-EPN:voxel SDF completion 方法。
论文报告的主要结论是:
- DeepSDF 在已知形状表示和未知形状重建上质量更好。
- 模型大小明显更小。
- 可以表示复杂拓扑和闭合表面。
- 可以通过 SDF 梯度得到 oriented normals。
- completion 效果比 3D-EPN 更好。
优点
DeepSDF 的优点很清楚:
- 连续表示,不受固定体素分辨率限制。
- 隐式表面天然适合复杂拓扑。
- SDF 同时编码 inside/outside 和距离。
- latent code 紧凑,适合压缩和插值。
- 可以用 Marching Cubes 从零水平集提取网格。
- 可以通过网络对输入坐标求导得到法向。
法向可以由 SDF 梯度得到:
这点和传统隐式曲面一致。
局限
这篇论文也有明显局限:
- 没有 encoder,测试时需要优化 latent code,推理较慢。
- 依赖 canonical pose,物体需要处于标准坐标系。
- 对训练类别有依赖,跨类别泛化不是本文重点。
- 生成可渲染网格仍需 Marching Cubes 或 raycasting。
- 训练需要较高质量的 SDF 采样数据。
论文结论中也提到,completion 阶段由于要显式优化 latent vector,耗时较高;未来可以考虑更高效的 Gauss-Newton 类方法。
和前面笔记的关系
这篇论文和前面的 polygon mesh processing、三维重建笔记联系很紧:
- 第 1 章的 implicit surface:DeepSDF 本质上是学习隐式曲面。
- 第 1.5 的 representation conversion:DeepSDF 输出 SDF 后,可以通过 Marching Cubes 转成 mesh。
- 第 3.2 的曲面法向:隐式曲面法向来自梯度 。
- 第 3.3 的离散算子:DeepSDF 是连续场,和网格上的离散 Laplacian 形成对照。
- 多视图重建中的 shape prior:DeepSDF 的 latent code 可以看成一种学习到的形状先验。
我的理解
DeepSDF 的关键不是“用神经网络拟合一个 SDF”这么简单,而是:
把一类形状压缩进 latent space,再用同一个连续 decoder 把 latent code 展开成一个完整的 SDF 场。
因此它同时做了两件事:
- 表示:一个形状由 和 decoder 共同表示。
- 先验:latent space 约束了哪些形状是合理的。
这解释了为什么它能做 shape completion:即使输入只有一部分点云,优化出来的 也会倾向于落在训练过的合理形状流形上,从而补出缺失部分。
阅读时要抓住的主线
- SDF 把表面表示成零水平集。
- DeepSDF 用 MLP 学习连续 SDF。
- shape code 控制具体是哪一个形状。
- 训练时同时优化 decoder 参数和每个训练形状的 code。
- 测试时固定 decoder,只优化新形状的 code。
- 这种表示紧凑、连续、能表达复杂拓扑,但推理优化较慢。
后续问题
读完这篇后,可以继续追问:
- DeepSDF 和 Occupancy Networks 的区别是什么?
- 为什么 SDF 比 occupancy 更适合提取法向和表面细节?
- 如果加入 encoder,能不能加速测试时的 latent code 求解?
- DeepSDF 如何和多视图重建或 SLAM 结合?
- 后来的 NeRF、NeuS、VolSDF 和 DeepSDF 有什么关系?