跳到主要内容

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 紧凑表示。

它不是输出点云或网格,而是学习一个连续函数:

fθ(z,x)sf_\theta(\mathbf{z},\mathbf{x})\rightarrow s

其中:

  • z\mathbf{z} 是形状 latent code。
  • xR3\mathbf{x}\in\mathbb{R}^3 是查询点坐标。
  • ss 是该点到形状表面的 signed distance。

最终表面由零水平集给出:

S={xfθ(z,x)=0}S=\{\mathbf{x}\mid f_\theta(\mathbf{z},\mathbf{x})=0\}

DeepSDF 中 SDF 标量场和零水平集的重绘示意图

图源说明:根据 Park et al., DeepSDF 的 signed distance field 与 zero level set 概念重绘,非论文原图。

SDF 表示的直觉

Signed distance function 对空间中每个点返回一个数:

SDF(x)=s\operatorname{SDF}(\mathbf{x})=s

含义是:

  • s=0s=0:点在表面上。
  • s<0s<0:点在物体内部。
  • s>0s>0:点在物体外部。
  • s|s|:点到表面的距离。

所以一个形状不再被看成一堆三角形,而是被看成三维空间里的一个连续标量场。

这和你前面看的 implicit surface 很接近:

F(x,y,z)=0F(x,y,z)=0

只不过 DeepSDF 把这个 FF 学成了一个 neural network。

单形状 DeepSDF

如果只表示一个形状,可以训练一个网络:

fθ(x)SDF(x)f_\theta(\mathbf{x})\approx \operatorname{SDF}(\mathbf{x})

训练数据是一组空间采样点和真实 SDF 值:

X={(x,s):SDF(x)=s}X=\{(\mathbf{x},s):\operatorname{SDF}(\mathbf{x})=s\}

论文使用 clamped L1 loss:

L(fθ(x),s)=clamp(fθ(x),δ)clamp(s,δ)L(f_\theta(\mathbf{x}),s) = | \operatorname{clamp}(f_\theta(\mathbf{x}),\delta) - \operatorname{clamp}(s,\delta) |

其中:

clamp(x,δ)=min(δ,max(δ,x))\operatorname{clamp}(x,\delta) = \min(\delta,\max(-\delta,x))

这个截断距离 δ\delta 的作用是:让网络重点学习表面附近的 SDF,而不是把远离表面的距离也拟合得很精确。

提示

对重建来说,最重要的是零水平集附近是否准确,因为最终表面来自 fθ(x)=0f_\theta(\mathbf{x})=0

从单个形状到一类形状

只为每个形状训练一个网络没有太大意义。DeepSDF 更重要的部分是:用同一个 decoder 表示一整个 shape class。

网络输入变成:

(z,x)(\mathbf{z},\mathbf{x})

输出是:

fθ(z,x)SDFz(x)f_\theta(\mathbf{z},\mathbf{x})\approx \operatorname{SDF}_{\mathbf{z}}(\mathbf{x})

也就是说:

  • x\mathbf{x} 决定“查空间中的哪个点”。
  • z\mathbf{z} 决定“查哪一个形状”。

可以把 z\mathbf{z} 理解成形状压缩编码。不同 chair、airplane、sofa 会有不同 latent code,但共享同一个 decoder。

DeepSDF decoder 的输入输出关系重绘示意图

图源说明:根据 Park et al., DeepSDF 的 shape code conditioned decoder 概念重绘,非论文原图。

模型架构:输入与输出

DeepSDF 的核心网络不是 encoder-decoder,而是一个 conditioned MLP decoder。它学习的是连续 SDF 函数:

fθ(z,x)sf_\theta(\mathbf{z},\mathbf{x})\rightarrow s

decoder 的输入

形状 latent code z\mathbf{z}

zRd\mathbf{z}\in\mathbb{R}^d 表示某一个具体形状的低维编码。论文常用 d=256d=256。同一类别里的不同形状,例如不同椅子,会有不同的 z\mathbf{z}

查询点坐标 x\mathbf{x}

xR3\mathbf{x}\in\mathbb{R}^3 是连续 3D 空间中的任意点:

x=(x,y,z)\mathbf{x}=(x,y,z)

decoder 实际接收的是拼接后的向量:

[z,x][\mathbf{z},\mathbf{x}]

也就是说,z\mathbf{z} 决定“是哪一个形状”,x\mathbf{x} 决定“查询这个形状空间中的哪个位置”。

decoder 的输出

输出是一个标量 signed distance:

s=fθ(z,x)s=f_\theta(\mathbf{z},\mathbf{x})

它的含义是:

  • s<0s<0:查询点在物体内部。
  • s=0s=0:查询点在物体表面。
  • s>0s>0:查询点在物体外部。
  • s|s|:查询点到表面的距离。

最终 mesh 不是网络直接吐出来的,而是在大量查询点上计算 ss,再提取零水平集:

S={xfθ(z,x)=0}S=\{\mathbf{x}\mid f_\theta(\mathbf{z},\mathbf{x})=0\}

通常用 Marching Cubes 从这个零水平集中提取三角网格。

训练和推理时的输入输出差别

阶段输入优化/计算输出
训练多个训练形状的 SDF 样本 (xj,sj)(\mathbf{x}_j,s_j)同时优化 decoder 参数 θ\theta 和每个训练形状的 zi\mathbf{z}_i学到共享 decoder,以及训练集中每个形状的 latent code
推理新形状的部分 SDF / 点云观测,以及查询点 x\mathbf{x}固定 θ\theta,只优化新形状的 z\mathbf{z}新形状的 SDF 场
mesh 提取固定 z\mathbf{z} 后的大量 3D 查询点前向计算 fθ(z,x)f_\theta(\mathbf{z},\mathbf{x})零水平集 mesh

所以 DeepSDF 的“模型输出”严格来说是 SDF 值;重建出的 mesh 是对 SDF 场后处理得到的结果。

DeepSDF 训练和推理阶段的数据流向重绘示意图

图源说明:根据 Park et al., DeepSDF 的 auto-decoder 训练与推理机制重绘,非论文原图。

输出可以用来干什么

DeepSDF 的输出是连续空间中任意点的 signed distance:

s=fθ(z,x)s=f_\theta(\mathbf{z},\mathbf{x})

这个标量输出本身不是最终可视化模型,但它可以支持很多后续操作。

输出可以用来做什么具体做法
单点 SDF 值 ss判断点在物体内外s<0s<0 为内部,s>0s>0 为外部,s=0s=0 在表面
大量点的 SDF 场提取三角网格在规则网格上查询 SDF,再用 Marching Cubes 提取 s=0s=0 等值面
优化得到的 latent code z\mathbf{z}压缩表示一个形状用一个低维向量代表完整 3D 形状
不同形状的 latent codes形状插值z1\mathbf{z}_1z2\mathbf{z}_2 之间线性插值,再解码出连续变化的形状
部分观测优化出的 z\mathbf{z}形状补全用不完整点云约束 z\mathbf{z},再由 learned shape prior 补出缺失区域
SDF 关于坐标的梯度 xfθ\nabla_{\mathbf{x}}f_\theta估计表面法向在表面附近对 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:

zi\mathbf{z}_i

这些 code 和网络参数 θ\theta 一起通过反向传播优化。

DeepSDF auto-decoder 训练与推理流程重绘示意图

图源说明:根据 Park et al., DeepSDF 的 auto-decoder learning scheme 概念重绘,非论文原图。

训练目标

对于第 ii 个形状,其 SDF 样本为:

Xi={(xj,sj)}X_i=\{(\mathbf{x}_j,s_j)\}

论文先从 posterior 写起:

pθ(ziXi)=p(zi)(xj,sj)Xipθ(sjzi;xj)p_\theta(\mathbf{z}_i \mid X_i) = p(\mathbf{z}_i) \prod_{(\mathbf{x}_j,s_j)\in X_i} p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j)
为什么 posterior 可以写成 prior 乘以所有样本 likelihood?

这个公式是在说:给定第 ii 个形状的 SDF 观测样本 XiX_i,这个形状 latent code zi\mathbf{z}_i 有多可信。

先看符号:

  • zi\mathbf{z}_i:第 ii 个形状的 latent code。
  • XiX_i:第 ii 个形状的全部 SDF 采样点集合。
  • (xj,sj)(\mathbf{x}_j,s_j):一个训练样本,表示在空间点 xj\mathbf{x}_j 处,真实 SDF 值是 sjs_j
  • p(zi)p(\mathbf{z}_i):latent code 的先验概率,表示这个 code 本身是否合理。
  • pθ(sjzi;xj)p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j):在给定 code zi\mathbf{z}_i 和查询点 xj\mathbf{x}_j 时,网络预测出 SDF 值 sjs_j 的概率。

从贝叶斯公式出发:

pθ(ziXi)=pθ(Xizi)p(zi)p(Xi)p_\theta(\mathbf{z}_i\mid X_i) = \frac{ p_\theta(X_i\mid \mathbf{z}_i)p(\mathbf{z}_i) }{ p(X_i) }

其中 p(Xi)p(X_i) 和要优化的 zi\mathbf{z}_i 无关,所以在最大化 posterior 时可以忽略这个分母:

pθ(ziXi)p(zi)pθ(Xizi)p_\theta(\mathbf{z}_i\mid X_i) \propto p(\mathbf{z}_i)p_\theta(X_i\mid \mathbf{z}_i)

接下来要展开:

pθ(Xizi)p_\theta(X_i\mid \mathbf{z}_i)

XiX_i 是很多 SDF 样本组成的集合:

Xi={(x1,s1),(x2,s2),,(xK,sK)}X_i=\{(\mathbf{x}_1,s_1),(\mathbf{x}_2,s_2),\ldots,(\mathbf{x}_K,s_K)\}

论文这里隐含了一个常见假设:在给定形状 code zi\mathbf{z}_i 后,各个采样点的 SDF 观测是条件独立的。

也就是说,一旦已经知道“这是哪个形状”,那么每个查询点的 SDF 值都可以由同一个 decoder 单独解释:

pθ(Xizi)=(xj,sj)Xipθ(sjzi;xj)p_\theta(X_i\mid \mathbf{z}_i) = \prod_{(\mathbf{x}_j,s_j)\in X_i} p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j)

把它代回去,就得到:

pθ(ziXi)p(zi)(xj,sj)Xipθ(sjzi;xj)p_\theta(\mathbf{z}_i\mid X_i) \propto p(\mathbf{z}_i) \prod_{(\mathbf{x}_j,s_j)\in X_i} p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j)

论文里直接写成:

pθ(ziXi)=p(zi)(xj,sj)Xipθ(sjzi;xj)p_\theta(\mathbf{z}_i \mid X_i) = p(\mathbf{z}_i) \prod_{(\mathbf{x}_j,s_j)\in X_i} p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j)

严格说,这里省略了归一化常数 p(Xi)p(X_i)。因为训练和推理时只关心哪个 zi\mathbf{z}_i 让 posterior 最大,常数项不会影响最优解。

直观理解就是:

一个 latent code 是否好,要同时满足两件事:它本身像一个合理 shape code,并且它能解释这个形状的所有 SDF 采样点。

其中:

p(zi)p(\mathbf{z}_i)

负责约束 code 不要跑到离谱位置;

jpθ(sjzi;xj)\prod_j p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j)

负责要求这个 code 能解释所有观测点的 SDF 值。

为什么是乘积?因为每个样本都要被解释好。如果某个点的 likelihood 很低,整个乘积就会变小,说明这个 code 对该形状整体解释不好。

实际优化时通常取负对数,把乘积变成求和:

logpθ(ziXi)=logp(zi)(xj,sj)Xilogpθ(sjzi;xj)+constant-\log p_\theta(\mathbf{z}_i\mid X_i) = -\log p(\mathbf{z}_i) - \sum_{(\mathbf{x}_j,s_j)\in X_i} \log p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j) + \text{constant}

如果把 likelihood 写成:

pθ(sjzi;xj)=exp(L(fθ(zi,xj),sj))p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j) = \exp\left( -L(f_\theta(\mathbf{z}_i,\mathbf{x}_j),s_j) \right)

这里的 LL 是 loss function,也就是“预测 SDF 值和真实 SDF 值之间的误差”。

网络预测值是:

s^j=fθ(zi,xj)\hat{s}_j = f_\theta(\mathbf{z}_i,\mathbf{x}_j)

真实 SDF 值是:

sjs_j

所以:

L(fθ(zi,xj),sj)L(f_\theta(\mathbf{z}_i,\mathbf{x}_j),s_j)

就是在衡量:

网络在点 xj\mathbf{x}_j 处预测的 SDF,和真实 SDF 差了多少。

如果使用普通 L1 loss,就是:

L(s^j,sj)=s^jsjL(\hat{s}_j,s_j) = |\hat{s}_j-s_j|

DeepSDF 论文实际使用的是 clamped L1 loss:

L(s^j,sj)=clamp(s^j,δ)clamp(sj,δ)L(\hat{s}_j,s_j) = | \operatorname{clamp}(\hat{s}_j,\delta) - \operatorname{clamp}(s_j,\delta) |

其中:

clamp(x,δ)=min(δ,max(δ,x))\operatorname{clamp}(x,\delta) = \min(\delta,\max(-\delta,x))

也就是说,距离太远的 SDF 值会被截断到 [δ,δ][-\delta,\delta] 范围内。这样做是为了让网络更关注表面附近,因为最终曲面来自:

fθ(zi,x)=0f_\theta(\mathbf{z}_i,\mathbf{x})=0

为什么 likelihood 写成 exp(L)\exp(-L)

这是把“误差越小,概率越大”写成概率形式的一种常见方式:

  • 如果 L=0L=0,说明预测完全正确,则 exp(L)=1\exp(-L)=1
  • 如果 L=1L=1,则 exp(L)0.3679\exp(-L)\approx 0.3679
  • 如果 L=3L=3,则 exp(L)0.0498\exp(-L)\approx 0.0498

所以:

pθ(sjzi;xj)=exp(L)p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j) = \exp(-L)

表达的是:

预测误差越小,这个 SDF 观测样本在当前 latent code 下越可信;预测误差越大,likelihood 越低。

这里可以把它理解成一个未归一化的概率模型。DeepSDF 主要关心优化目标,因此关键是它取负对数以后刚好回到 loss:

那么:

logpθ(sjzi;xj)=L(fθ(zi,xj),sj)-\log p_\theta(s_j\mid \mathbf{z}_i;\mathbf{x}_j) = L(f_\theta(\mathbf{z}_i,\mathbf{x}_j),s_j)

如果先验是高斯:

p(zi)N(0,σ2I)p(\mathbf{z}_i)\sim \mathcal{N}(0,\sigma^2I)

那么负对数先验会变成 L2 正则:

logp(zi)1σ2zi22-\log p(\mathbf{z}_i) \propto \frac{1}{\sigma^2}\|\mathbf{z}_i\|_2^2

所以最大化 posterior 等价于最小化:

jL(fθ(zi,xj),sj)+1σ2zi22\sum_j L(f_\theta(\mathbf{z}_i,\mathbf{x}_j),s_j) + \frac{1}{\sigma^2}\|\mathbf{z}_i\|_2^2

这就是 DeepSDF auto-decoder 训练目标的来源。

训练时优化:

argminθ,{zi}i=1N(j=1KL(fθ(zi,xj),sj)+1σ2zi22)\arg\min_{\theta,\{\mathbf{z}_i\}} \sum_{i=1}^{N} \left( \sum_{j=1}^{K} L(f_\theta(\mathbf{z}_i,\mathbf{x}_j),s_j) + \frac{1}{\sigma^2}\|\mathbf{z}_i\|_2^2 \right)

这个目标包含两部分:

  • 重建误差:让网络预测的 SDF 接近真实 SDF。
  • latent code 正则:让 zi\mathbf{z}_i 不要跑得太散。

第二项很重要。它让 latent space 更紧凑、更连续,也让后续插值和补全更稳定。

为什么 latent code 要加 L2 正则?

如果没有正则,每个训练形状的 code 可以随便跑到 latent space 的任意位置。

这样虽然训练集可能拟合得很好,但 latent space 会很松散:

  • 两个相近 code 不一定对应相近形状。
  • 插值时可能经过无意义区域。
  • 测试时优化 code 更容易陷入坏解。

论文从概率角度把 code 看成服从零均值高斯先验:

p(z)N(0,σ2I)p(\mathbf{z})\sim \mathcal{N}(0,\sigma^2 I)

对应到优化目标里,就是:

1σ2z22\frac{1}{\sigma^2}\|\mathbf{z}\|_2^2

所以 L2 正则的含义是:鼓励所有形状 code 聚集在原点附近,形成一个更紧凑的 shape manifold。

推理阶段怎么重建新形状

测试时,网络参数 θ\theta 已经固定。

对于一个新形状,没有 encoder 直接输出 latent code,所以 DeepSDF 通过优化求一个 code:

z^=argminz(xj,sj)XL(fθ(z,xj),sj)+1σ2z22\hat{\mathbf{z}} = \arg\min_{\mathbf{z}} \sum_{(\mathbf{x}_j,s_j)\in X} L(f_\theta(\mathbf{z},\mathbf{x}_j),s_j) + \frac{1}{\sigma^2}\|\mathbf{z}\|_2^2

这一步可以用于:

  • 已知完整形状的重建。
  • 给定部分深度图或点云时的 shape completion。

直观地说,就是在 latent space 里寻找一个形状 code,让它生成的 SDF 尽量符合观测数据。

DeepSDF 训练和推理到底分别在优化什么?

DeepSDF 最容易混淆的一点是:它没有 encoder,所以训练和推理都涉及“优化 latent code”。但两者优化的对象不一样。

训练阶段

训练时,已知训练集中有 NN 个形状:

{X1,X2,,XN}\{X_1,X_2,\ldots,X_N\}

每个形状 XiX_i 都是一组 SDF 采样:

Xi={(xj,sj)}j=1KX_i=\{(\mathbf{x}_j,s_j)\}_{j=1}^{K}

DeepSDF 给每个训练形状分配一个可学习 latent code:

z1,z2,,zN\mathbf{z}_1,\mathbf{z}_2,\ldots,\mathbf{z}_N

同时还有一个共享 decoder:

fθ(z,x)f_\theta(\mathbf{z},\mathbf{x})

训练时优化的是两类变量:

  1. decoder 参数 θ\theta
  2. 训练集中每个形状自己的 latent code zi\mathbf{z}_i

也就是:

θ,z1,,zN\theta,\mathbf{z}_1,\ldots,\mathbf{z}_N

都会通过反向传播更新。

训练目标是:

argminθ,{zi}i=1N(j=1KL(fθ(zi,xj),sj)+1σ2zi22)\arg\min_{\theta,\{\mathbf{z}_i\}} \sum_{i=1}^{N} \left( \sum_{j=1}^{K} L(f_\theta(\mathbf{z}_i,\mathbf{x}_j),s_j) + \frac{1}{\sigma^2}\|\mathbf{z}_i\|_2^2 \right)

可以把训练过程理解成:

给每个训练形状随机初始化一个 latent code
-> 随机采样该形状的一批 SDF 点
-> decoder 根据 z_i 和 x_j 预测 SDF
-> 和真实 s_j 比较,得到 loss
-> 同时更新 decoder 参数 theta 和该形状的 z_i
-> 重复很多轮

训练结束后,decoder 学到了一种“形状类”的连续 SDF 表示能力;训练集的 latent codes 则记录了每个训练形状在 latent space 中的位置。

推理阶段

推理时,来了一个新形状:

XnewX_{\text{new}}

此时 decoder 参数 θ\theta 已经固定,不再更新。

DeepSDF 要做的是:为这个新形状找一个新的 latent code:

znew\mathbf{z}_{\text{new}}

所以推理阶段优化的是:

znew\mathbf{z}_{\text{new}}

而不是 θ\theta

推理目标是:

z^=argminz(xj,sj)XnewL(fθ(z,xj),sj)+1σ2z22\hat{\mathbf{z}} = \arg\min_{\mathbf{z}} \sum_{(\mathbf{x}_j,s_j)\in X_{\text{new}}} L(f_\theta(\mathbf{z},\mathbf{x}_j),s_j) + \frac{1}{\sigma^2}\|\mathbf{z}\|_2^2

可以理解成:

固定训练好的 decoder theta
-> 随机初始化一个新 latent code z
-> 用当前 z 和观测点 x_j 查询 SDF
-> 比较预测 SDF 和观测 SDF
-> 只更新 z,不更新 theta
-> 优化收敛后得到 z_new

这一步有点像“给新形状配一个最合适的参数”,而不是重新训练整个模型。

推理完成后如何得到 mesh

得到 z^\hat{\mathbf{z}} 后,新形状就由下面这个连续函数表示:

fθ(z^,x)f_\theta(\hat{\mathbf{z}},\mathbf{x})

表面仍然是零水平集:

Snew={xfθ(z^,x)=0}S_{\text{new}} = \{\mathbf{x}\mid f_\theta(\hat{\mathbf{z}},\mathbf{x})=0\}

如果要可视化成 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 znew\mathbf{z}_{\text{new}}
  • 通过优化让这个 code 生成的 SDF 尽量匹配观测。
  • 最后用 fθ(znew,x)=0f_\theta(\mathbf{z}_{\text{new}},\mathbf{x})=0 提取完整表面。

这就是 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(x)=max(0,x)\operatorname{ReLU}(x)=\max(0,x)

也就是说:

  • 如果输入 x>0x>0,输出就是 xx
  • 如果输入 x<0x<0,输出就是 00

神经网络如果只有线性层,那么多层线性层叠在一起本质上还是一个线性函数,表达能力很有限。ReLU 的作用是引入非线性,让 MLP 可以拟合复杂函数。

DeepSDF 要拟合的是:

fθ(z,x)sf_\theta(\mathbf{z},\mathbf{x})\rightarrow s

也就是从空间坐标和形状 code 到 SDF 值的复杂连续函数。不同位置的距离值、inside/outside 边界、局部几何细节都不是简单线性关系,所以 hidden layers 需要 ReLU 这样的非线性激活。

在这里用 ReLU 的原因可以概括为:

  • 计算简单。
  • 梯度传播稳定。
  • 适合多层 fully connected network。
  • 可以把空间分成很多 piecewise linear 区域,从而逼近复杂 SDF 场。

Batch-normalization 是什么

Batch Normalization,简称 BatchNorm,通常会对一个 mini-batch 内某一层的激活值做标准化:

x^=xμBσB2+ϵ\hat{x} = \frac{x-\mu_B}{\sqrt{\sigma_B^2+\epsilon}}

其中 μB\mu_BσB2\sigma_B^2 是当前 batch 的均值和方差。

然后再学习一个缩放和平移:

y=γx^+βy=\gamma\hat{x}+\beta

它的常见作用是:

  • 减少不同 batch 间激活分布变化。
  • 让训练更稳定。
  • 允许使用更大的学习率。
  • 在 CNN 分类任务里经常很有效。

一个具体数值例子:

假设某一层在一个 mini-batch 里,对同一个 channel 得到 4 个激活值:

x=[1,2,3,4]x=[1,2,3,4]

batch 均值为:

μB=1+2+3+44=2.5\mu_B=\frac{1+2+3+4}{4}=2.5

batch 方差为:

σB2=(12.5)2+(22.5)2+(32.5)2+(42.5)24=1.25\sigma_B^2 = \frac{(1-2.5)^2+(2-2.5)^2+(3-2.5)^2+(4-2.5)^2}{4} = 1.25

暂时忽略很小的 ϵ\epsilon,标准差为:

σB21.118\sqrt{\sigma_B^2}\approx 1.118

那么第一个激活值 x=1x=1 会被标准化为:

x^=12.51.1181.34\hat{x} = \frac{1-2.5}{1.118} \approx -1.34

第四个激活值 x=4x=4 会被标准化为:

x^=42.51.1181.34\hat{x} = \frac{4-2.5}{1.118} \approx 1.34

如果学习到的缩放和平移是:

γ=2,β=0.5\gamma=2,\qquad \beta=0.5

那么第一个值最终变成:

y=2(1.34)+0.5=2.18y=2(-1.34)+0.5=-2.18

第四个值最终变成:

y=2(1.34)+0.5=3.18y=2(1.34)+0.5=3.18

这个例子说明:BatchNorm 的输出依赖当前 batch 的均值和方差。也就是说,同一个输入激活值,如果放在不同 batch 里,归一化结果可能不同。

但 DeepSDF 论文中说 BatchNorm 不稳定,原因可以这样理解:DeepSDF 的输入不是普通图像 batch,而是大量空间查询点和 SDF 值。网络要学习的是一个连续标量场:

xSDF(x)\mathbf{x}\mapsto \operatorname{SDF}(\mathbf{x})

如果使用 BatchNorm,某个点的中间特征会依赖“同一个 batch 里还有哪些点”。这对分类任务通常没问题,但对 SDF 回归会比较麻烦。

因为 SDF 需要满足一种点到点之间连续、稳定的函数关系。如果同一个查询点 x\mathbf{x} 在不同 batch 组合中经过不同的归一化统计量,输出可能产生额外扰动。

所以在 DeepSDF 这里,BatchNorm 可能破坏连续场回归的稳定性,尤其会影响表面附近精确的零水平集:

fθ(z,x)=0f_\theta(\mathbf{z},\mathbf{x})=0

Weight-normalization 是什么

Weight Normalization,简称 WeightNorm,不是归一化 batch 里的激活值,而是重参数化神经网络权重。

普通线性层可以写成:

y=wTx+by=\mathbf{w}^T\mathbf{x}+b

WeightNorm 把权重向量写成“方向 + 长度”:

w=gvv\mathbf{w} = g\frac{\mathbf{v}}{\|\mathbf{v}\|}

其中:

  • v\mathbf{v} 控制方向。
  • gg 控制尺度。

一个具体数值例子:

假设某个神经元的原始方向参数为:

v=[3,4]\mathbf{v}=[3,4]

它的长度是:

v=32+42=5\|\mathbf{v}\| = \sqrt{3^2+4^2} = 5

所以单位方向为:

vv=[35,45]=[0.6,0.8]\frac{\mathbf{v}}{\|\mathbf{v}\|} = \left[ \frac{3}{5}, \frac{4}{5} \right] = [0.6,0.8]

如果可学习尺度为:

g=2g=2

那么真正用于线性层的权重是:

w=gvv=2[0.6,0.8]=[1.2,1.6]\mathbf{w} = g\frac{\mathbf{v}}{\|\mathbf{v}\|} = 2[0.6,0.8] = [1.2,1.6]

给定输入:

x=[10,5]\mathbf{x}=[10,5]

忽略 bias,输出为:

y=wTx=1.2×10+1.6×5=20y = \mathbf{w}^T\mathbf{x} = 1.2\times 10+1.6\times 5 = 20

这个例子说明:WeightNorm 直接控制权重本身的方向和尺度,不需要统计当前 batch 里其他样本的激活值。

这样做的好处是:把权重的方向和大小解耦,让优化更稳定。

和 BatchNorm 相比,WeightNorm 不依赖当前 mini-batch 的统计量。也就是说,同一个查询点 x\mathbf{x} 的输出不会因为 batch 里混了哪些其他点而变化。

这对 DeepSDF 很重要,因为它希望网络表示的是一个稳定的连续函数:

fθ(z,x)f_\theta(\mathbf{z},\mathbf{x})

而不是一个强烈依赖 batch 统计的函数。

所以论文选择:

不用 BatchNorm
改用 WeightNorm

可以理解为:既想保留归一化带来的优化稳定性,又不想让 SDF 回归依赖 batch 统计。

放到 DeepSDF 里整体理解

DeepSDF 这套设计服务于一个目标:稳定地学习连续 SDF 场。

技术是什么在 DeepSDF 里的作用
ReLUhidden layer 的非线性激活提高 MLP 表达能力,拟合复杂 SDF
BatchNorm用 batch 统计量归一化激活论文发现不稳定,可能干扰连续 SDF 回归
WeightNorm重参数化权重方向和尺度稳定优化,同时不依赖 batch 统计
tanh输出层激活把 SDF 输出限制在有限范围,配合 clamped SDF loss
dropout随机屏蔽部分神经元减少过拟合,提高泛化
Adam自适应梯度优化器优化网络参数和 latent code 更方便

因此,这段网络设计可以概括为:

用 ReLU MLP 提供足够表达能力,用 WeightNorm 稳定训练,用 tanh 和 clamped loss 把学习重点压到表面附近,最终得到一个可微的连续 SDF 函数。

训练完成后,表面是零水平集:

fθ(x)=0f_\theta(\mathbf{x})=0

可以用 raycasting 或 Marching Cubes 可视化。

而法向可以直接对输入坐标求导:

n=xfθ(x)xfθ(x)\mathbf{n} = \frac{ \nabla_\mathbf{x}f_\theta(\mathbf{x}) }{ \|\nabla_\mathbf{x}f_\theta(\mathbf{x})\| }

这也是神经隐式 SDF 的一个重要优势:网络不仅能给出表面,还能通过反向传播给出解析梯度。

数据采样

训练 DeepSDF 时,需要从网格生成 SDF 样本。

论文的做法大致是:

  1. 把形状归一化到单位球附近。
  2. 从多个虚拟相机渲染深度,得到有方向的表面点。
  3. 在表面附近更密集地采样空间点。
  4. 对每个采样点计算 signed distance。

表面附近采样更密集是因为:

fθ(z,x)=0f_\theta(\mathbf{z},\mathbf{x})=0

附近决定最终表面的位置和细节。

实验结果

论文主要验证了四类能力:

  1. 表示训练集中已知形状。
  2. 重建未见过的新形状。
  3. 从单视角深度图或噪声点云完成 shape completion。
  4. 在 latent space 中进行形状插值。

DeepSDF 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 梯度得到:

n=xfθ(z,x)xfθ(z,x)\mathbf{n} = \frac{\nabla_\mathbf{x} f_\theta(\mathbf{z},\mathbf{x})} {\|\nabla_\mathbf{x} f_\theta(\mathbf{z},\mathbf{x})\|}

这点和传统隐式曲面一致。

局限

这篇论文也有明显局限:

  • 没有 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 的曲面法向:隐式曲面法向来自梯度 f\nabla f
  • 第 3.3 的离散算子:DeepSDF 是连续场,和网格上的离散 Laplacian 形成对照。
  • 多视图重建中的 shape prior:DeepSDF 的 latent code 可以看成一种学习到的形状先验。

我的理解

DeepSDF 的关键不是“用神经网络拟合一个 SDF”这么简单,而是:

把一类形状压缩进 latent space,再用同一个连续 decoder 把 latent code 展开成一个完整的 SDF 场。

因此它同时做了两件事:

  • 表示:一个形状由 z\mathbf{z} 和 decoder 共同表示。
  • 先验:latent space 约束了哪些形状是合理的。

这解释了为什么它能做 shape completion:即使输入只有一部分点云,优化出来的 z\mathbf{z} 也会倾向于落在训练过的合理形状流形上,从而补出缺失部分。

阅读时要抓住的主线

  1. SDF 把表面表示成零水平集。
  2. DeepSDF 用 MLP 学习连续 SDF。
  3. shape code z\mathbf{z} 控制具体是哪一个形状。
  4. 训练时同时优化 decoder 参数和每个训练形状的 code。
  5. 测试时固定 decoder,只优化新形状的 code。
  6. 这种表示紧凑、连续、能表达复杂拓扑,但推理优化较慢。

后续问题

读完这篇后,可以继续追问:

  1. DeepSDF 和 Occupancy Networks 的区别是什么?
  2. 为什么 SDF 比 occupancy 更适合提取法向和表面细节?
  3. 如果加入 encoder,能不能加速测试时的 latent code 求解?
  4. DeepSDF 如何和多视图重建或 SLAM 结合?
  5. 后来的 NeRF、NeuS、VolSDF 和 DeepSDF 有什么关系?