【论文精读】NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis (ECCV 2020)

NeRF 论文精读/代码详解

Abstract

本文提出一种方法,通过使用稀疏的输入视图集来优化底层连续体场景函数,从而在合成复杂场景的新视图方面取得了先进的成果。算法使用全连接 (非卷积) 深度网络来表示场景,输入的是单个连续的5D坐标 (即空间位置 (x,y,z) 和视角 (θ,ϕ)),输出是该空间的 volume density (体积密度) 和视角相关的 radiance (辐射亮度)。我们通过查询沿相机光线的5D坐标来合成视图,并使用经典的体渲染技术将输出的颜色和密度投影到图像上。由于 volume rendering 本身是可微分的,因此输入只需要已知相机姿态的图像。本文描述了如何有效的优化神经辐射场以渲染具有复杂几何形状和外观场景的逼真新视图,并展示了优于先前神经渲染和视图合成工作的结果。

结果不必多说,是当时最为先进的。

Introduction

screenshot_2026-01-16_14-30-38_1768545075266_0

我们将静态场景表示为一个连续的5D函数,该函数输出空间中每个点 (x,y,z) 沿各个方向 (θ,ϕ) 发射的辐射,以及每个点的 density (该密度类似于微分不透明度),控制光线穿过该点 (x,y,z) 时累积的辐射亮度。文章通过 MLP 来表示这个映射,该网络不包含任何卷积层,通过单个五维坐标 (x,y,z,θ,ϕ) 回归到单个体密度和与视角相关的 RGB 颜色来表示该函数。

为了渲染从特定角度的神经辐射场,我们:

  1. 将相机光线穿过场景以生成一组采样的3D点;
  2. 使用这些点及其对应的2D观察方向作为神经网络的输入,以生成一组颜色和密度的输出;
  3. 使用经典的体渲染技术将这些颜色和密度累计成2D图像。

由于该过程本身是可微的,我们可以使用梯度下降法来优化该模型,方式是最小化每个观测图像与我们模型渲染出的对应视图之间的误差。最小化多个视图之间的误差可以促使神经网络预测出场景的连贯模型,具体做法是为包含真实场景内容的区域赋予高密度值和准确的颜色。具体pipline如下图:

screenshot_2026-01-16_14-20-59_1768544480143_0

我们发现,针对复杂场景优化神经辐射场表示的基本实现方法无法收敛到足够高分辨率的表示,并且每个相机光线所需的采样数量也过高。为了解决这些问题,我们采用位置编码转换输入的5D坐标,使 MLP 能够表示更高频率的函数,此外,我们还提出了一种分层采样方法,以减少充分采样这种高频场景表示所需的查询次数。

本文提出的方法继承了体积表示的优势: 两者都能够表示复杂的真实世界几何形状和外观,并且都非常适合使用投影图像进行基于梯度的优化,至关重要的是,我们的方法克服了在高分辨率下对复杂场景进行建模时离散体积网络带来的高昂存储成本。总的来说,本文的主要贡献有:

  1. 把一种复杂的场景表示为5D神经辐射场的方法,并参数化为 MLP 网络;
  2. 基于可微渲染过程,我们优化这种场景表示。包括分层采样策略,用于将 MLP 的容量分配给具有可见场景内容的空间;
  3. 将每个输入的5D坐标映射到更高维度1的空间进行位置编码,是我们能够成功地优化神经辐射场,以表示高频场景内容。

CV 领域一个很有前景的新兴方向是将物体和场景编码到 MLP 的权重中,该 MLP 能够直接从 3D 空间位置映射到形状的隐式表示。然而,这些方法目前还无法像使用离散表示 (例如三角网格或体积网格) 的技术那样,以相同的保真度再现具有复杂几何形状的逼真场景。

Neural 3D shape representations

近年的工作探索了连续3D形状隐式表示为水平集的方式,通过优化深度网络将 (x,y,z) 坐标映射到 signed distance functions 或 occupancy fields。然而,这些模型受限于对真实三维几何形状的访问,而这些真实三维几何形状通常来自合成三维形状数据集,例如ShapeNet。后续研究通过构建可微渲染函数放宽了对真实三维形状的要求,使得仅适用二维图像即可优化神经隐式形状表示。Niemeyer等人将物体表面表示为 3D occupancy fields,并使用数值方法找到每条光线的表面交点,然后使用隐式微分计算精确导数。每个光线交点位置都作为神经 3D texture field 的输入,这个 field 预测该点的漫反射颜色。Sitzmann等人使用一种更加不直接的神经 3D 表示,在每个连续的 3D 坐标处简单地输出一个特征向量和RGB颜色,并提出一个由 recurrent neural network 组成的可微渲染函数,该网络沿着每条射线行进以确定表面位置。

尽管这些技术理论上可以表示复杂的高分辨率几何体,但迄今为止它们仅限于几何复杂度较低的简单形状,导致渲染结果过于平滑。我们证明,通过优化网络来编码 5D 辐射场 (具有 2D 视角相关外观的 3D 体),可以表示更高分辨率的几何体和外观,从而渲染出复杂场景的逼真新视角。

View synthesis and image-based rendering

在密集的视图采样下,可以通过简单的光场采样插值技术重建逼真的新视图。对于稀疏视图下的新视图合成,计算机视觉和图形学领域通过从观测图像预测传统的几何和外观表示取得了显著进展。一类常用的方法是使用基于网格的场景表示,这些表示具有漫反射或视图相关的外观。Differentiable rasterizers 或 pathtracers 可以使用梯度下降直接优化网格表示以重现一组输入图像。然而,基于图像重投影的梯度网格优化通常比较困难,可能是由于局部最小值或损失函数的条件数较差造成的。此外,该策略需要在优化之前提供一个具有固定拓扑结构的模板网格作为初始化,这通常不适用于无约束的现实世界场景。

另一类方法是使用体积表示来解决一组输入的 RGB 图像中合成高质量逼真视图的任务。体积方法能真实的表示复杂的形状和材质,非常适合基于梯度的优化,并且比基于 Mesh 的方法产生的视觉干扰更少。早期的体积方法使用观测到的图像来直接给体积上色。最近,一些方法使用包含多个场景的大型数据集来训练深度网络,该网络能从一组输入图像中预测采样的体积表示,然后使用 alpha-compositing 或 沿光线学习的合成来渲染新的视图。其他研究针对每个特定场景优化了 CNN 和 采样体积网络的组合,使得 CNN 可以补偿来自低分辨率体积网络的离散化伪影,或者允许预测的体积网格根据输入的时间而变化。虽然这些技术在新视图合成方面取得不错的结果,但它们扩展到更高分辨率图像的能力受到了限制,因为离散采样导致时间和空间复杂度高,而渲染更高分辨率的图像需要对 3D 空间进行更精细的采样。本文提出方法在 MLP 的参数中编码连续的 volume 来规避这个问题,这不仅产生比先前体积方法更高质量的渲染,而且只需要更小的存储成本。

Neural Radiance Field Scene Representation

我们将连续场景表示为一个5D向量值函数,其输入为一个3D坐标点 𝐱=(x,y,z) 和 2D视角方向 (θ,ϕ) ,输出为发射颜色 𝐜=(r,g,b) 和体密度 σ 。实际上,我们将方向表示为3D笛卡尔单位向量 d 。我们使用MLP网络 : (𝐱,𝐝) → (𝐜,σ) 来近似这种连续的5D场景表示,并优化其权重 Θ,从而将每个输入的5D坐标映射到其对应的体密度和方向发射颜色。

pic -> 5D 向量(粒子的空间位姿)的前处理

4D 向量(粒子的颜色以及密度) -> pic 的后处理

我们通过让网络把体积密度 σ 预测为仅与位置 x 有关,来保证这种表征方法在不同视图下是一致的,同时将RGB颜色 c 预测为位置和观察方向的函数。为了达到这个目标,MLP 首先使用8层全连接层 (使用 ReLU 激活函数,每层 256 个通道) 处理输入的3D坐标 x ,并输出 σ 和一个 256 维特征向量。然后,将该特征向量与相机光线的视角方向连接起来,并传递给另一个全连接层(使用 ReLU 激活函数,每层 128 个通道),该层输出与视角相关的RGB颜色。

screenshot_2026-01-16_11-26-53_1768534019108_0

代码详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class NeRF(nn.Module): // 网络结构: 输入 → 编码位置 (128维) → 8个全连接层(256通道) → 跳跃连接(第4层) → 编码方向(48维) → 额外FC层 → 输出(RGB + 密度)
def __init__(self, D=8, W=256, input_ch=3, input_ch_views=3, output_ch=4, skips=[4], use_viewdirs=False):
"""
"""
super(NeRF, self).__init__()
self.D = D // D = 8, W= 256 表示8个全连接层(256通道)
self.W = W
self.input_ch = input_ch // 编码后的位置维度
self.input_ch_views = input_ch_views // 编码后的方向维度
self.skips = skips
self.use_viewdirs = use_viewdirs

// 构建3D位置MLP,用于处理位置编码输入
self.pts_linears = nn.ModuleList(
[nn.Linear(input_ch, W)] + [nn.Linear(W, W) if i not in self.skips else nn.Linear(W + input_ch, W) for i in range(D-1)])

// 构建视角条件MLP,负责将 位置特征 + 视角编码 映射为 RGB
### Implementation according to the official code release (https://github.com/bmild/nerf/blob/master/run_nerf_helpers.py#L104-L105)
self.views_linears = nn.ModuleList([nn.Linear(input_ch_views + W, W//2)])

### Implementation according to the paper
# self.views_linears = nn.ModuleList(
# [nn.Linear(input_ch_views + W, W//2)] + [nn.Linear(W//2, W//2) for i in range(D//2)])

if use_viewdirs:
self.feature_linear = nn.Linear(W, W) // 处理位置MLP输出
self.alpha_linear = nn.Linear(W, 1) // 输出体积密度
self.rgb_linear = nn.Linear(W//2, 3) // 输出颜色
else:
self.output_linear = nn.Linear(W, output_ch) // 直接用一层线性输出 output_ch(RGB+alpha)

def forward(self, x):
input_pts, input_views = torch.split(x, [self.input_ch, self.input_ch_views], dim=-1)

// 将位置输入传入位置MLP,使用ReLU激活
h = input_pts
for i, l in enumerate(self.pts_linears):
h = self.pts_linears[i](h)
h = F.relu(h)
if i in self.skips: // 如果当前层是跳跃层,则将原始输入拼接到隐藏层(跳跃连接)。
h = torch.cat([input_pts, h], -1)

if self.use_viewdirs:
alpha = self.alpha_linear(h)
feature = self.feature_linear(h)
h = torch.cat([feature, input_views], -1)

for i, l in enumerate(self.views_linears):
h = self.views_linears[i](h)
h = F.relu(h)

rgb = self.rgb_linear(h)
outputs = torch.cat([rgb, alpha], -1)
else:
outputs = self.output_linear(h)

return outputs

def load_weights_from_keras(self, weights):
assert self.use_viewdirs, "Not implemented if use_viewdirs=False"

# Load pts_linears
for i in range(self.D):
idx_pts_linears = 2 * i
self.pts_linears[i].weight.data = torch.from_numpy(np.transpose(weights[idx_pts_linears]))
self.pts_linears[i].bias.data = torch.from_numpy(np.transpose(weights[idx_pts_linears+1]))

# Load feature_linear
idx_feature_linear = 2 * self.D
self.feature_linear.weight.data = torch.from_numpy(np.transpose(weights[idx_feature_linear]))
self.feature_linear.bias.data = torch.from_numpy(np.transpose(weights[idx_feature_linear+1]))

# Load views_linears
idx_views_linears = 2 * self.D + 2
self.views_linears[0].weight.data = torch.from_numpy(np.transpose(weights[idx_views_linears]))
self.views_linears[0].bias.data = torch.from_numpy(np.transpose(weights[idx_views_linears+1]))

# Load rgb_linear
idx_rbg_linear = 2 * self.D + 4
self.rgb_linear.weight.data = torch.from_numpy(np.transpose(weights[idx_rbg_linear]))
self.rgb_linear.bias.data = torch.from_numpy(np.transpose(weights[idx_rbg_linear+1]))

# Load alpha_linear
idx_alpha_linear = 2 * self.D + 6
self.alpha_linear.weight.data = torch.from_numpy(np.transpose(weights[idx_alpha_linear]))
self.alpha_linear.bias.data = torch.from_numpy(np.transpose(weights[idx_alpha_linear+1]))

Volume Rendering with Radiance Fields

我们的5D神经辐射场将场景表示为空间中任意点的体积密度合方向性辐射。我们使用经典的体渲染原理来渲染穿过场景的任意光线的颜色。体积密度 σ(𝐱) 可以解释为光线终止于位置 x 处无穷小粒子的微分概率。相机光线 𝐫(t)=𝐨+t𝐝 在近边界 和远边界 处的预期颜色 C(𝐫) 为:

函数 T(t) 表示从 到 t 的 光线沿线的积累投射率,即光线从 到 t 传播而不击中任何其他粒子的概率。从我们的连续神经辐射场渲染新视图,需要估计穿过所需虚拟相机每个像素的相机光线的积分 C(r).

体渲染是渲染技术的一个分支,目的是解决云/烟/果冻等非刚性物体的渲染建模,将物质抽象成一团飘忽不定的粒子群,光线在穿过时,是光子跟粒子发生碰撞的过程。

粒子的采集

某一个像素 (u, v) 的颜色是沿着某一条射线上的无数个发光点的颜色“和“,可以利用相机模型去反推这条射线,这个射线的表示方法就是 𝐫(t)=𝐨+t𝐝 。O 为射线原点,d 是方向,t 是距离,理论上 t 从 0 -> 无穷。

screenshot_2026-01-16_19-24-59_1768562721181_0

代码详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def get_rays(H, W, K, c2w): 
// H:图像高度(像素数) W:图像宽度(像素数)
// K:相机内参矩阵(3x3),包含焦距 fx, fy 和主点 cx, cy
// c2w:相机外参矩阵(4x4),表示相机坐标系到世界坐标系的变换

// 构建像素坐标网格,i是像素x的坐标网格,j是像素y的坐标网格,meshgrid生成网格矩阵
i, j = torch.meshgrid(torch.linspace(0, W-1, W), torch.linspace(0, H-1, H)) # pytorch's meshgrid has indexing='ij'
i = i.t()
j = j.t()

// dirs就是归一化的相机坐标
dirs = torch.stack([(i-K[0][2])/K[0][0], -(j-K[1][2])/K[1][1], -torch.ones_like(i)], -1)

// rays_d就是在世界坐标系下射线的方向
# Rotate ray directions from camera frame to the world frame
rays_d = torch.sum(dirs[..., np.newaxis, :] * c2w[:3,:3], -1) # dot product, equals to: [c2w.dot(dir) for dir in dirs]

// rays_o就是世界坐标系下的原点
# Translate camera frame's origin to the world frame. It is the origin of all rays.
rays_o = c2w[:3,-1].expand(rays_d.shape)
return rays_o, rays_d

def train():
...
// 从一张图像采样随机像素点生成训练批次
coords = torch.reshape(coords, [-1,2]) # (H * W, 2)
select_inds = np.random.choice(coords.shape[0], size=[N_rand], replace=False) # (N_rand,)
select_coords = coords[select_inds].long() # (N_rand, 2)
rays_o = rays_o[select_coords[:, 0], select_coords[:, 1]] # (N_rand, 3)
rays_d = rays_d[select_coords[:, 0], select_coords[:, 1]] # (N_rand, 3)
batch_rays = torch.stack([rays_o, rays_d], 0)
target_s = target[select_coords[:, 0], select_coords[:, 1]] # (N_rand, 3)

// 给定一组射线 rays_o 和 rays_d,在每条射线上采样一系列 3D 点 z_vals
def render_rays(...):
near, far = bounds[...,0], bounds[...,1] # [-1,1] // near:射线起始深度 far:射线终止深度
...
t_vals = torch.linspace(0., 1., steps=N_samples)
if not lindisp:
z_vals = near * (1.-t_vals) + far * (t_vals)
else:
z_vals = 1./(1./near * (1.-t_vals) + 1./far * (t_vals))

z_vals = z_vals.expand([N_rays, N_samples])

// 随机扰动
if perturb > 0.:
# get intervals between samples
mids = .5 * (z_vals[...,1:] + z_vals[...,:-1])
upper = torch.cat([mids, z_vals[...,-1:]], -1)
lower = torch.cat([z_vals[...,:1], mids], -1)
# stratified samples in those intervals
t_rand = torch.rand(z_vals.shape)

# Pytest, overwrite u with numpy's fixed random numbers
if pytest:
np.random.seed(0)
t_rand = np.random.rand(*list(z_vals.shape))
t_rand = torch.Tensor(t_rand)

z_vals = lower + (upper - lower) * t_rand

我们使用数值积分法来估计这个连续积分。确定性积分法通常用于渲染离散化的体积网格,但它会有效的限制我们表示的分辨率,因为 MLP 只能在一组固定的离散位置进行查询。因此,我们采用分层抽样的方法,将 [,] 划分为 N 个等间距的区间,然后从每个区间内均匀随机抽取一个样本:

尽管我们使用离散的样本来估计积分,但分层抽样使我们能够表示连续的场景,因为它使得 MLP 在优化过程中在连续的位置进行评估。我们使用这些样本,并根据 quadrature rule 估计 C(r),也就是:

其中, 表示相邻样本之间的距离。该函数用于根据集合 的值计算 ,其微分性质显而易见,并可简化为使用 alpha 值 的传统 alpha 合成方法。

Optimizing a Neural Radiance Field

在前一节中,我们描述了将场景建模为神经辐射场并从中渲染新视图所需的核心组件。然而,我们发现这些组件不足以达到最先进的质量。为了能够表示高分辨率的复杂场景,我们引入了两项改进。第一项改进是对输入坐标进行位置编码,以辅助 MLP 表示高频函数; 第二项改进是采用分层采样方法,以便高效地对这种高频表示进行采样。

Positional encoding

尽管神经网络是一个通用的函数逼近器,我们发现直接对5D输入进行操作会导致渲染结果在表示颜色和几何形状的高频变化方面表现不佳。这与 Rahaman 等人的最新研究结果一致,该研究表明深度网络倾向于学习低频函数。他们还指出,在将输入传递给网络之前,使用高频函数将其映射到更高维的空间,可以更好地拟合包含高频变化的数据。

我们将这些发现应用于神经场景表征,并证明将 重新定义为两个函数的组合 (其中一个是学习得到的,另一个是未学习得到的),明显提高了性能。这里, γ 是从 ℝ 到更高维空间 $ℝ^{2L}$ 的映射,而 仍然是一个普通的 MLP。形式上,我们使用的编码函数为:

这个函数 γ(⋅) 分别应用于 x 中的三个坐标值(这些坐标值已归一化到 [-1, 1] 中) 以及笛卡尔观察方向单位向量 d 的三个分量 (根据定义,这些分量位于 [−1,1] 中)。在我们的实验中,我们将γ(𝐱) 设置为 L=10 ,将 γ(𝐝) 设置为 L=4 。

在Transformer 架构中也使用了类似的映射,它被称为位置编码 。然而,Transformer 使用它的目的不同,它是将序列中标记的离散位置作为输入提供给一个不包含任何顺序概念的架构。相比之下,我们使用这些函数将连续的输入坐标映射到更高维的空间,从而使我们的 MLP 更容易逼近更高频率的函数。

代码详解

alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Embedder:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.create_embedding_fn()

def create_embedding_fn(self):
embed_fns = []
d = self.kwargs['input_dims'] // 输入维度 d, 如 d = 3 表示 [x,y,z]
out_dim = 0
if self.kwargs['include_input']:
embed_fns.append(lambda x : x)
out_dim += d

// 构造频率带,若log_sampling=True:freq_bands=[2^0,2^1,2^2,…,2^max_freq]; 否则线性采样频率
max_freq = self.kwargs['max_freq_log2']
N_freqs = self.kwargs['num_freqs']

if self.kwargs['log_sampling']:
freq_bands = 2.**torch.linspace(0., max_freq, steps=N_freqs)
else:
freq_bands = torch.linspace(2.**0., 2.**max_freq, steps=N_freqs)

// 为每个频率应用周期函数,对应数序公式如上
for freq in freq_bands:
for p_fn in self.kwargs['periodic_fns']:
embed_fns.append(lambda x, p_fn=p_fn, freq=freq : p_fn(x * freq))
out_dim += d

self.embed_fns = embed_fns
self.out_dim = out_dim

// 将所有映射函数作用到输入并拼接,得到最终高维特征向量
def embed(self, inputs):
return torch.cat([fn(inputs) for fn in self.embed_fns], -1)

// kwargs 用于接收任意关键字参数
// {
'input_dims': 3, # 输入维度,例如3D坐标
'include_input': True, # 是否在输出中保留原始输入
'max_freq_log2': multires - 1, # 最大频率为 2^(multires - 1)
'num_freqs': multires, # 总共 multires 个频率
'log_sampling': True, # 是否对频率做对数采样
'periodic_fns': [torch.sin, torch.cos] # 使用的周期函数
}

Hierarchical volume sampling

我们采用的渲染策略是沿每条相机光线在 N 查询点处密集评估神经辐射场网络,但这种策略效率低下:对渲染图像没有贡献的自由空间和遮挡区域仍然会被重复采样。我们从体渲染的早期工作中提取灵感,通过分层样本来提高渲染效率,是其与最终渲染的预期效果成正比。

代码详解

1
2
3
4
5
6
7
8
9
// 分层采样
// 第1阶段:粗采样(64个点)
rgb_0, acc_0 = render_rays(coarse_model) # 得到权重分布

// 第2阶段:重要性采样(增加64个点)
z_samples = sample_pdf(weights, N_importance)

// 第3阶段:精细采样(总128个点)
rgb_map = render_rays(fine_model)

我们不仅使用单个网络来表示场景,而是同时优化两个网络: 一个“粗略”网络和一个“精细”网络。我们首先使用分层抽样对一组 位置进行采样,并评估这些位置的“粗略”网络。给定这个“粗略”网络的输出,我们随后生成沿着每条射线更精确的点采样,其中采样点偏向于体积的相关部分。为此,我们首先在方程式中重写粗略网络 的 alpha 合成颜色作为沿射线的所有采样颜色 的加权和:

将这些权重归一化为 沿射线生成分段常数概率密度函数。我们使用逆变换采样从该分布中采样第二组 个位置,在第一组和第二组采样点的并集上评估我们的“精细”网络,并使用等式3计算射线的最终渲染颜色 ,但使用所有 个采样点。此过程将更多样本分配给我们期望包含可见内容的区域。 这达到了与importance sampling类似的目标,但我们将采样值用作整个积分域的非均匀离散化,而不是将每个样本视为整个积分的独立概率估计。

代码详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 计算相邻采样点之间的距离
dists = z_vals[...,1:] - z_vals[...,:-1]
dists = torch.cat([dists, torch.Tensor([1e10]).expand(dists[...,:1].shape)], -1) # [N_rays, N_samples]
dists = dists * torch.norm(rays_d[...,None,:], dim=-1)

rgb = torch.sigmoid(raw[...,:3]) # [N_rays, N_samples, 3]
noise = 0.
if raw_noise_std > 0.:
noise = torch.randn(raw[...,3].shape) * raw_noise_std

# Overwrite randomly sampled data if pytest
if pytest:
np.random.seed(0)
noise = np.random.rand(*list(raw[...,3].shape)) * raw_noise_std
noise = torch.Tensor(noise)

// 应用激活函数得到不透明度
alpha = raw2alpha(raw[...,3] + noise, dists) # [N_rays, N_samples]
# weights = alpha * tf.math.cumprod(1.-alpha + 1e-10, -1, exclusive=True)
// 累积权重(透射率 × 不透明度)
weights = alpha * torch.cumprod(torch.cat([torch.ones((alpha.shape[0], 1)), 1.-alpha + 1e-10], -1), -1)[:, :-1]
// 最终颜色
rgb_map = torch.sum(weights[...,None] * rgb, -2) # [N_rays, 3]

// 深度和视差
depth_map = torch.sum(weights * z_vals, -1)
disp_map = 1./torch.max(1e-10 * torch.ones_like(depth_map), depth_map / torch.sum(weights, -1))

Implementation details

我们针对每个场景优化一个独立的神经连续体表示网络。这仅需要场景的RGB图像数据集、相应的相机位姿和内参以及场景边界 (对于合成数据,我们使用真实相机位姿、内参和边界; 对于真实数据,我们使用 COLMAP 运动结构重建包历来估计这些参数)。在每次优化迭代中,我们从数据集中的所有像素中随机抽取一批相机光线,然后按照分层采样方法,从粗略网络中查询 样本,从精细网络中查询 样本。接下来,我们使用渲染过程来渲染两组样本中每条光线的颜色。我们的损失函数是粗略渲染和精细渲染像素颜色和真实像素颜色之间的总平方误差:

其中,ℛ 表示每个批次中的光线集合, C(𝐫) 、 分别表示光线 𝐫 的真实值、粗略体积预测值和精细体积预测值的 RGB 颜色。需要注意的是,尽管最终渲染结果来自 ,但我们也最小化了 的损失,以便利用粗略网络的权重分布来分配精细网络中的样本。

在我们的实验中,我们使用 4096 条光线的批次大小,每条光线在粗略体积的 $N_c$=64 坐标处采样,并在精细体积的 =128 个额外坐标处采样。我们使用 Adam 优化器,学习率从 开始,并在优化过程中呈指数衰减至 (其他 Adam 超参数保持默认值 )。单个场景的优化通常在单个 NVIDIA V100 GPU 上需要大约 10 万到 30 万次迭代才能收敛(大约需要 1 到 2 天)。

Results

screenshot_2026-01-16_17-29-11_1768555762505_0

Conclusion

我们的工作直接解决了先前使用 MLP 将物体和场景表示为连续函数的不足之处。我们证明,将场景表示为 5D 神经辐射场 (一种输出体密度和与视角相关的辐射亮度 (作为 3D 位置和 2D 视角方向的函数) 的 MLP) 比之前主流的训练深度卷积网络输出离散体素表示的方法能产生更好的渲染效果。

尽管我们提出了一种分层采样策略来提高渲染的采样效率 (包括训练和测试),但在研究如何高效优化和渲染神经辐射场方面,仍有许多工作要做。未来的另一个研究方向是可解释性: 诸如体素网格和网格状结构之类的采样表示可以推断渲染视图的预期质量和故障模式,但当我们将场景编码到深度神经网络的权重中时,如何分析这些问题尚不明确。我们相信,这项工作朝着基于真实世界图像的图形管线迈出了重要一步,在该管线中,复杂的场景可以由从真实物体和场景图像中优化得到的神经辐射场构成。

Reference

  1. 【论文精读】NeRF详解
  2. NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis
  3. 【较真系列】讲人话-NeRF全解(原理+代码+公式)

【论文精读】NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis (ECCV 2020)
https://uiuiqkjn.github.io/2026/01/16/NeRF-Representing-Scenes-as-Neural-Radiance-Fields-for-View-Synthesis-ECCV-2020/
作者
咕噜咕哩布
发布于
2026年1月16日
更新于
2026年3月31日
许可协议