CT重建(一) | NeRF 原理详解

NeRF 原理详解

面向"从零理解 NeRF"的说明文档。内容结合本仓库(Ben Mildenhall 等人的官方 TensorFlow 实现,ECCV 2020)的代码,回答一系列常见疑问:它解决什么问题、5D 坐标是什么、MLP 长什么样、为什么输入是射线、为什么做 3D 重建却输出 2D 图、以及粗/细网络分别是什么。

目录

  • [1. 这个项目是什么](#1. 这个项目是什么)
  • [2. 它解决了之前的什么问题](#2. 它解决了之前的什么问题)
  • [3. 核心概念:5D 坐标](#3. 核心概念:5D 坐标)
  • [4. 整体流程:从输入到输出](#4. 整体流程:从输入到输出)
  • [5. 为什么是"按像素生成射线"](#5. 为什么是"按像素生成射线")
  • [6. 输入向量:63 维 = 60 编码 + 原始 3](#6. 输入向量:63 维 = 60 编码 + 原始 3)
  • [7. 网络结构:就是一个 MLP 吗](#7. 网络结构:就是一个 MLP 吗)
  • [8. 输出与损失:2D 预测 vs 2D 真实](#8. 输出与损失:2D 预测 vs 2D 真实)
  • [9. 粗网络与细网络](#9. 粗网络与细网络)
  • [10. 为什么做 3D 重建却输出 2D 图像](#10. 为什么做 3D 重建却输出 2D 图像)

1. 这个项目是什么

本仓库是 NeRF(Neural Radiance Fields,神经辐射场) 的官方 TensorFlow 实现,对应论文:

NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis

Ben Mildenhall, Pratul P. Srinivasan, Matthew Tancik, Jonathan T. Barron, Ravi Ramamoorthi, Ren Ng

ECCV 2020(Oral,最佳论文荣誉提名)

一句话:用一个全连接神经网络(权重约 5MB)表示单个场景,让网络直接记住这个场景,从而从任意新视角渲染出照片级图像。

主要文件:

文件 作用
run_nerf.py 主训练 / 渲染脚本
run_nerf_helpers.py 网络结构、位置编码、体积渲染等辅助函数
load_blender.py 合成数据集(Blender)加载
load_deepvoxels.py DeepVoxels 数据集加载
load_llff.py 真实前向场景(LLFF)数据加载
config_fern.txt 等 示例配置
tiny_nerf.ipynb 教学版 Colab 教程
extract_mesh.ipynb 用 marching cubes 从 NeRF 提取网格

2. 它解决了之前的什么问题

NeRF 处理的核心任务是 新视角合成(novel view synthesis) :给一组 2D 图片,重建场景并生成任意新视角的图像。之前的方法在质量、连续性、存储上难以兼顾:

之前的方法 局限
基于图像的插值 / 光场 视角一变、遮挡关系一变就失效,不理解三维结构
显式几何重建(mesh / 点云 / 体素) 分辨率受内存限制;难处理毛发、半透明、反光;重建误差直接体现为伪影
离散化的体表示(如 Neural Volumes) 内存随分辨率三次方增长,分辨率上不去,表示不连续
纯 2D 神经网络渲染 视角间不一致,难表达复杂遮挡与光照

NeRF 的贡献:

  • 连续隐式函数表示场景:MLP 把 5D 坐标映射为颜色 + 密度,不受离散分辨率限制。
  • 可微体积渲染 :只用多视角 2D 图像的渲染损失端到端训练,无需 3D 监督、无需显式几何。
  • 位置编码:把坐标映射到高频正弦基,克服 MLP 的"频谱偏置"(倾向学低频、结果过度平滑),这是质量飞跃的关键。
  • 分层采样:粗网络 + 细网络,把采样点集中到真正有内容的地方,提升细节且不过度增加算力。
  • 视角相关效果:颜色依赖观察方向,可表达镜面高光、反光。

代价与后续:训练慢(数小时~两天)、渲染慢、每个场景单独训练无法跨场景泛化。后续工作:Instant-NGP(哈希网格加速)、Mip-NeRF(抗锯齿)、Plenoxels / 3D Gaussian Splatting(换显式表示换速度)。


3. 核心概念:5D 坐标

"5D" 指的是 5 个数字,描述"看哪个点、从哪个方向看":

  • 3D 位置 (x, y, z):空间中的一个点,即"光线走到哪儿"。
  • 2D 方向 (θ, φ) / 归一化方向向量:观察方向,即"从哪个角度看这个点"。

方向本来是 3 个分量,但单位向量满足 dx² + dy² + dz² = 1,自由度只有 2(球面经纬度),所以 3 + 2 = 5D。

网络映射:

复制代码
F: (x, y, z, θ, φ) -> (r, g, b, σ)
  • 输出 rgb 是颜色,σ 是体积密度(该点有多"不透明")。
  • 密度只用位置 (x,y,z) 决定,与观察方向无关;颜色用位置 + 方向共同决定,用来表达高光/反光。

4. 整体流程:从输入到输出

一句话概括:

一批图像 + 相机位姿 -> 每个像素发一条射线 -> 射线上采样若干点 -> 每点送进 MLP 得到颜色和密度 -> 沿射线做体积渲染积分 -> 得到像素颜色 -> 与真实像素算 L2 损失 -> 反向传播更新网络。

逐步拆解(对应 run_nerf.py / run_nerf_helpers.py):

  1. 射线 :对每个像素,根据相机位姿得到起点 rays_o(相机光心)和方向 rays_d(单位向量)。对应 get_rays / get_rays_np;真实前向场景再做 ndc_rays 变换。
  2. 采样点 :在 near 到 far 之间分层采样 N_samples 个点,加抖动。每个点坐标 pts = rays_o + t * rays_d。
  3. 位置编码 :坐标 xyz 编码为 60 维(10 频段 × sin/cos × 3),方向编码为 24 维(4 频段)。
  4. MLP 前向:主干 8 层 × 256 通道 ReLU,第 5 层后跳连接,输出分两支(密度 / 颜色)。
  5. 体积渲染 (raw2outputs):
    • δ_i = t_{i+1} - t_i
    • α_i = 1 - exp(-σ_i · δ_i)
    • T_i = exp(-Σ_{j<i} σ_j · δ_j)
    • w_i = T_i · α_i
    • 像素颜色 C = Σ w_i · rgb_i,深度 = Σ w_i · t_i
  6. 分层采样 :用粗网络的权重 w_i 做逆变换采样,在物体处加密,再跑细网络。
  7. 损失:粗、细输出都与真实像素算 L2,加和反传。

5. 为什么是"按像素生成射线"

关键点:NeRF 的输入输出不是"图 -> 图",而是"几何光线 -> 颜色"。

  • 普通 CNN(分类、超分、生成):一次"看到"整张 H×W×C 张量,用卷积处理像素间关系。
  • NeRF:网络一次前向只回答**"空间中某一点,从某方向看,是什么颜色、多密"。它不认识"图像",也不认识"第几行第几列"。要渲染一张图,就必须逐像素发一条射线"问"网络**。

一条射线 = 从相机光心出发、穿过某像素、射向场景的一条直线:

  • rays_o:起点(相机光心,所有像素共用)。
  • rays_d:方向(由像素位置决定,每个像素不同)。

一张 H×W 的图 → H×W 条射线。

为什么必须这样 :场景是连续的 3D 场,不是 2D 图。网络定义在 3D 空间上,只有"一条穿过空间的线"才有意义。渲染新视角时只要换一套相机位姿重新发射线即可,无需重新训练------这正是 NeRF 能做新视角合成的原因。

类比:传统 CNN 像"看照片";NeRF 像"逐个像素发射雷达探测线",问 3D 空间中每一点的颜色与透明度,再沿途累加成一个像素。


6. 输入向量:63 维 = 60 编码 + 原始 3

一个 3D 采样点坐标经过位置编码后,送进主干网络的向量是 63 维:

  • 原始 3 :采样点世界坐标 (x, y, z),原样保留,保证网络能拿到未经变换的绝对位置。

  • 60 编码 :对 x, y, z 每个坐标分别做位置编码:

    γ(p) = [ sin(2^0·π·p), cos(2^0·π·p), ..., sin(2^(L-1)·π·p), cos(2^(L-1)·π·p) ]

每个坐标用 L = 10 个频段,每频段产生 sin、cos 各一个 -> 每个坐标 20 维 -> 三个坐标共 60 维。

代码对应 run_nerf_helpers.py 的 Embedder / get_embedder:

python 复制代码
freq_bands = 2.**np.linspace(0., max_freq, num_freqs)  # [1, 2, 4, ..., 512]
x_enc = x * freq_bands                                  # 每个频段缩放一次
# sin、cos 各拼一份

为什么要编码:MLP 存在"频谱偏置",偏爱低频、会过度平滑;把坐标升到高频正弦基上,网络才能表达头发丝、边缘、纹理等高频细节。低频 sin/cos 抓大结构,高频抓细节。

方向编码对比 :方向 (dx,dy,dz) 通常用 multires=4,即 3 × 2 × 4 = 24 维,不加原始 3 维 (方向已归一化到单位球,原始值信息冗余)。颜色分支的输入是 256(第 8 层特征)+ 24(方向编码)。


7. 网络结构:就是一个 MLP 吗

是,NeRF 的骨干就是一个相当"朴素"的 MLP------没有卷积、没有注意力、没有循环。但它有几处关键设计:

特点 说明
纯 MLP 主干 8 层全连接,每层 256 单元,激活 ReLU
输入是位置编码 63 维高频编码,而非原始 (x,y,z)
跳连接 第 5 层后把原始输入(63 维)再拼回来,类似残差,防止深层丢失空间信息
双分支 密度 σ:只用第 8 层特征 -> 1 维(ReLU 保证非负);颜色 rgb:第 8 层特征 + 方向编码 -> 一层 128 通道 -> 3 维(sigmoid)
共享权重 / 连续函数 对射线上所有采样点分别查询,网络是"连续函数求值器"

关键创新不在网络架构 ,而在 表示方式(连续隐式场)+ 位置编码 + 可微体积渲染。每个场景单独训练,网络只需"记住"这一个场景,不需要泛化,普通 MLP 够用。

这也是它的局限来源:逐点前向慢、每场景单独训练、无法跨场景泛化。后续工作(Instant-NGP、Plenoxels、3DGS)主要是在换表示、加速,网络反而更简单甚至被去掉。


8. 输出与损失:2D 预测 vs 2D 真实

  • 输入 :一批射线上的 3D 采样点 ,每个点是 5D 坐标 (x, y, z, θ, φ)。
  • 输出 :这些点经体积渲染积分后,得到 2D 图像上的像素颜色。
  • 损失:渲染出的 2D 像素 vs 真实照片对应的 2D 像素,做 L2(均方误差)。

run_nerf.py 训练循环:

python 复制代码
# 随机采一批像素(如 1024 条射线),取真实颜色 target_s
# 渲染出预测颜色:粗 rgb_0 + 细 rgb
loss = mean((rgb   - target)**2) + mean((rgb_0 - target)**2)

要点:

  • 每次迭代只随机采一部分像素(不是整张图),省显存、加速。
  • 监督信号只有 2D 图像,没有任何 3D 几何 / 深度 / 体素标签。
  • 梯度从 2D 像素一路反传到 3D 场:像素颜色是射线上各点颜色的加权和,因此每个点的颜色与密度都能收到梯度------这就是 3D 结构被"逼"出来的机制。

术语修正:输入应说"3D 空间中的采样点",而不是"3D 采样图"(后者易被误解为体素/3D 图像)。


9. 粗网络与细网络

粗、细是同一个场景的两个渲染版本,区别只在"沿射线如何采样点"。

粗网络 rgb_0

  • 用均匀采样 在 [near, far] 取 N_coarse(默认 64)个点,加抖动。
  • 送进网络,做体积渲染,输出 rgb_0、视差 disp_0、累计不透明度 acc_0。
  • 特点:快、粗糙;主要价值不只是出图,而是给出每条射线上哪一段有物体 (权重 w_i)。

细网络 rgb

  • 用粗网络的权重 w_i 归一化成概率密度,做逆变换采样 ,在高权重区域加密采 N_importance(默认 128)个点。
  • 把这些新点与粗采样点合并后再渲染一次,输出 rgb------最终结果,细节更清楚。

代码(render_rays):

python 复制代码
# 1) 粗:均匀采样 + 前向
z_vals = uniform(near, far, N_coarse)
rgb_0, disp_0, acc_0, ..., weights = raw2outputs(network_fn(pts), z_vals, ...)

# 2) 用粗权重决定精细采样位置
z_samples = sample_pdf(z_vals_mid, weights, N_importance)

# 3) 细:合并点后重跑
z_vals = sort(cat([z_vals, z_samples]))
rgb, disp, acc, ... = raw2outputs(network_fn(new_pts), z_vals, ...)

损失同时监督两者:

python 复制代码
loss = L2(rgb, target) + L2(rgb_0, target)

监督粗网络是为了让它估计的权重更准(它决定细采样往哪儿放),监督细网络是为了最终渲染质量。N_importance=0 可关闭细网络。


10. 为什么做 3D 重建却输出 2D 图像

容易误解的地方:"重建"和"输出"是两件事。

是什么 维数
网络内部学到的表示 连续的神经辐射场 3D(+ 方向)
训练监督信号 多视角照片 2D
渲染输出 某个视角的图像 2D
可导出的几何 用 marching cubes 提取 mesh 3D
  • NeRF 确实在做三维重建 ,只是重建对象是隐式的 3D 场(不是显式网格/点云)。
  • 但它的任务本质是新视角合成,相机拍出来的画面本来就是 2D 的,所以渲染输出天生是 2D。
  • 这不是"只做了 2D",而是"用 2D 监督逼出 3D 表示,再渲染回 2D"。

为什么 2D 监督能学出 3D :靠多视角一致性。同一个 3D 点从不同角度看颜色应一致(密度也一致)。网络为了同时解释几十上百张不同视角的照片,被迫把"记忆"放到统一的 3D 结构中,而不是各记各的;配合可微体积渲染,梯度能从 2D 像素传回 3D 场。

想拿到显式 3D :见 extract_mesh.ipynb------在 3D 网格上查询密度,用 marching cubes 提等值面,导出 mesh/点云。

一句话:3D 是它"学到的东西",2D 是它"用来学的东西"和"给人看的结果"。


参考

相关推荐
ting94520001 小时前
深度拆解|1752vc Pitch Deck Analyzer 底层技术架构与核心算法原理
人工智能·算法·架构
2601_949950631 小时前
练题簿小程序的 4 个功能,把学习资料变成题库
人工智能·小程序·刷题·练习·小程序推荐
归秋1421 小时前
深度解读Work Agent长程任务拆解与执行的底层逻辑
大数据·人工智能
yukai080081 小时前
【203篇系列】055 十个月Agent变革回顾
人工智能
陈工大模型1 小时前
2026年9月AI可见度监测工具横评:从采样一致性与中立性出发的技术选型笔记
大数据·人工智能·笔记
海宇数据1 小时前
零信任架构实战:基于海宇身份证OCR构建自动化证照采集网关
人工智能·架构·自动化·ocr
一只桃子~2 小时前
Ai大模型数据标注与质检评测面试题
人工智能
枯木◊靠推文躺平版2 小时前
2026企业AI办公工具选型全指南
大数据·人工智能
地理探险家2 小时前
图片中的目标如何定位和计数?从数据检查到模型预测的完整实践
人工智能·深度学习·数据集