NeRF神经辐射场的核心算法复现:从光线采样到体渲染的实现细节 NeRF神经辐射场的核心算法复现从光线采样到体渲染的实现细节神经辐射场Neural Radiance Fields, NeRF是3D场景新视角合成的里程碑工作。其核心思想——用MLP隐式表示连续的场景密度和颜色场并通过可微体渲染进行端到端训练——看似简洁但实现中涉及众多数值计算细节。本文从相机光线的参数化出发逐步复现分层采样、位置编码、体渲染积分和层次化精炼四个核心组件的数学原理与PyTorch实现揭示这些细节如何共同保障高质量的新视角合成。一、相机光线的参数化与采样NeRF的输入是一组已知相机位姿的2D图像目标是学习一个连续函数$F_\Theta: (\mathbf{x}, \mathbf{d}) \to (\mathbf{c}, \sigma)$将3D空间中的点$\mathbf{x} (x, y, z)$和观察方向$\mathbf{d} (\theta, \phi)$映射为颜色$\mathbf{c} (r, g, b)$和体积密度$\sigma$。对于每个像素从相机光心$\mathbf{o}$出发沿方向$\mathbf{d}$发射一条光线$\mathbf{r}(t) \mathbf{o} t\mathbf{d}$。在光线上采样$N$个点${\mathbf{r}(t_i)}_{i1}^N$$t_i$在近平面$t_n$和远平面$t_f$之间。采样的关键是分层采样Stratified Sampling将$[t_n, t_f]$均匀分成$N$个区间在每个区间内随机采样一个点$$t_i \sim \mathcal{U}\left[t_n \frac{i-1}{N}(t_f - t_n), ; t_n \frac{i}{N}(t_f - t_n)\right]$$这种随机化确保了MLP在训练过程中看到的是连续的位置样本而非固定网格点。二、位置编码的高频映射直接使用3D坐标作为MLP输入会导致渲染结果过于平滑——MLP倾向于学习低频函数。NeRF通过位置编码将低维输入映射到高维空间来解决这一问题$$\gamma(p) (p, \sin(2^0\pi p), \cos(2^0\pi p), \sin(2^1\pi p), \cos(2^1\pi p), \ldots, \sin(2^{L-1}\pi p), \cos(2^{L-1}\pi p))$$对坐标$\mathbf{x}$使用$L10$映射到60维对方向$\mathbf{d}$使用$L4$映射到24维。import torch import torch.nn as nn import torch.nn.functional as F class PositionalEncoding(nn.Module): NeRF 的位置编码模块。 将 3D 坐标映射到高频空间使 MLP 能够表示高频几何细节。 def __init__( self, num_freq_xyz: int 10, # 坐标的 L 值 num_freq_dir: int 4, # 方向的 L 值 include_input: bool True, # 是否包含原始坐标p 本身 ): super().__init__() self.num_freq_xyz num_freq_xyz self.num_freq_dir num_freq_dir self.include_input include_input # 预计算频率系数 2^l * π # 形状: (num_freq,) self.freq_bands_xyz 2.0 ** torch.arange( num_freq_xyz, dtypetorch.float32 ) * torch.pi self.freq_bands_dir 2.0 ** torch.arange( num_freq_dir, dtypetorch.float32 ) * torch.pi def forward( self, xyz: torch.Tensor, # (N_rays, N_samples, 3) dirs: torch.Tensor # (N_rays, 3) ) - Tuple[torch.Tensor, torch.Tensor]: 对坐标和方向执行位置编码。 输出维度: xyz_encoded: include_input → 3 2*3*L_xyz 63 维 dirs_encoded: include_input → 3 2*3*L_dir 27 维 # 坐标编码 xyz_encoded [] if self.include_input: xyz_encoded.append(xyz) # 对每个频率计算 sin 和 cos for freq in self.freq_bands_xyz: # 将频率移到与 xyz 相同的设备 freq freq.to(xyz.device) xyz_scaled xyz * freq xyz_encoded.append(torch.sin(xyz_scaled)) xyz_encoded.append(torch.cos(xyz_scaled)) xyz_encoded torch.cat(xyz_encoded, dim-1) # 方向编码 dirs_encoded [] if self.include_input: dirs_encoded.append(dirs) for freq in self.freq_bands_dir: freq freq.to(dirs.device) dirs_scaled dirs * freq dirs_encoded.append(torch.sin(dirs_scaled)) dirs_encoded.append(torch.cos(dirs_scaled)) dirs_encoded torch.cat(dirs_encoded, dim-1) return xyz_encoded, dirs_encoded不使用位置编码时$L0$NeRF渲染出的图像严重模糊高频纹理和新视角的细节完全丢失。$L10$对坐标的编码是一个经验平衡——过低的$L$导致模糊过高的$L$如$L16$会引入高频伪影。三、体渲染积分的数值实现体渲染是NeRF训练可微的核心。对于光线上的$N$个采样点颜色通过以下积分近似计算$$\hat{C}(\mathbf{r}) \sum_{i1}^N T_i (1 - \exp(-\sigma_i \delta_i)) \mathbf{c}_i$$其中$T_i \exp\left(-\sum_{j1}^{i-1} \sigma_j \delta_j\right)$是从光线起点到第$i$个采样点的累积透射率$\delta_i t_{i1} - t_i$是相邻采样点之间的距离。def volume_rendering( raw_output: torch.Tensor, z_vals: torch.Tensor, rays_d: torch.Tensor, white_bg: bool False, ) - Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: NeRF 的体渲染积分可微分。 Args: raw_output: MLP 输出shape (N_rays, N_samples, 4) 最后一维为 [R, G, B, σ] z_vals: 采样点的深度值shape (N_rays, N_samples) rays_d: 光线方向向量用于计算相邻采样点间的实际距离 white_bg: 是否使用白色背景数据集背景为黑色时关闭 Returns: rgb_map: (N_rays, 3) 像素颜色 depth_map: (N_rays,) 期望深度 weights: (N_rays, N_samples) 每个采样点的贡献权重 # 提取 RGB 和密度 rgb torch.sigmoid(raw_output[..., :3]) # (N_rays, N_samples, 3) sigma F.relu(raw_output[..., 3]) # (N_rays, N_samples) # 计算相邻采样点之间的距离 # 注意这里不是简单的 z_vals[i1] - z_vals[i] # 需要乘以光线方向向量的模长来处理非单位方向向量 dists z_vals[..., 1:] - z_vals[..., :-1] # (N_rays, N_samples-1) # 最后一个采样点之后补一个远距离模拟无限远 dists torch.cat([ dists, torch.full_like(dists[..., :1], 1e10) # 远距离 → exp(-σ*1e10) ≈ 0 ], dim-1) # 乘以光线方向的 L2 范数 dists dists * torch.norm(rays_d[..., None, :], dim-1) # alpha: 每个采样点的不透明度 # alpha 1 - exp(-σ * δ) alpha 1.0 - torch.exp(-sigma * dists) # (N_rays, N_samples) # 累积透射率 T_i Π_{j1}^{i-1} (1 - alpha_j) # 使用 cumprod 实现 O(N) 计算 # 在序列前面加一个 1.0 确保 T_1 1.0 T torch.cumprod( torch.cat([ torch.ones_like(alpha[..., :1]), # T_1 1.0 1.0 - alpha 1e-10 # 加 epsilon 防止梯度消失 ], dim-1), dim-1 )[..., :-1] # 去掉最后的累积值 # 每个采样点对最终颜色的贡献权重 weights alpha * T # (N_rays, N_samples) # 体渲染颜色 rgb_map torch.sum(weights[..., None] * rgb, dim-2) # (N_rays, 3) # 深度图期望深度 Σ w_i * z_i depth_map torch.sum(weights * z_vals, dim-1) # (N_rays,) # 背景处理 if white_bg: # 白色背景累积 alpha总不透明度作为前景权 acc_map torch.sum(weights, dim-1) # (N_rays,) rgb_map rgb_map (1.0 - acc_map[..., None]) return rgb_map, depth_map, weights$T_i$的cumprod实现是一个性能关键点——它避免了在循环中重复计算累积乘积将时间复杂度从$O(N^2)$降至$O(N)$。四、层次化体渲染与训练效率NeRF同时使用粗网络Coarse, 64个采样点和精细网络Fine, 128个额外采样点。精细网络的采样点不是均匀分布在光线上而是基于粗网络得到的权重分布进行重要性采样Inverse Transform Sampling。这一设计的直觉是粗网络提供场景几何的粗略估计哪些区域密度高精细网络在有趣的区域增加采样密度。训练时的总损失为两个网络的MSE之和$$\mathcal{L} \sum_{\mathbf{r}} \left[ |\hat{C}_c(\mathbf{r}) - C(\mathbf{r})|^2_2 |\hat{C}_f(\mathbf{r}) - C(\mathbf{r})|^2_2 \right]$$训练一个完整的NeRF场景在单张V100上大约需要1-2天200K-300K迭代。后续的加速方案如Instant-NGP的多分辨率哈希编码、TensoRF的张量分解将这一时间缩短到分钟级但这些加速方案在核心的体渲染机制上与原始NeRF保持一致。五、总结本文完整复现了NeRF的四个核心组件分层采样通过随机化确保MLP在连续空间上训练位置编码通过高频映射解决MLP的低频偏好问题体渲染积分通过可微分的alpha合成将3D场景投影到2D图像层次化采样通过粗网络的引导在几何复杂区域分配更多采样点。这些组件的数值实现细节——如cumprod的线性复杂度累积透射率、Inverse Transform Sampling的采样策略——共同保障了NeRF端到端训练的有效性。理解这些细节对于调试视图合成质量和探索NeRF加速变体具有基础价值。