网易首页 > 网易号 > 正文 申请入驻

神经辐射场NeRF入门:3D视图合成的原理与PyTorch代码实现

0
分享至



NeRF(Neural Radiance Fields,神经辐射场)的核心思路是用一个全连接网络表示三维场景。输入是5D向量空间坐标(x, y, z)加上视角方向(θ, φ),输出则是该点的颜色和体积密度。训练的数据则是同一物体从不同角度拍摄的若干张照片。

通常情况下泛化能力是模型的追求目标,需要在大量不同样本上训练以避免过拟合。但NeRF恰恰相反,它只在单一场景的多个视角上训练,刻意让网络"过拟合"到这个特定场景,这与传统神经网络的训练逻辑完全相反。

这样NeRF把网络训练成了某个场景的"专家",这个专家只懂一件事,但懂得很透彻:给它任意一个新视角,它都能告诉你从那个方向看场景是什么样子,存储的不再是一堆图片,而是场景本身的隐式表示。



基本概念

把5D输入向量拆开来看:空间位置(x, y, z)和观察方向(θ, φ)。

颜色(也就是辐射度)同时依赖位置和观察方向,这很好理解,因为同一个点从不同角度看可能有不同的反光效果。但密度只跟位置有关与观察方向无关。这里的假设是材质本身不会因为你换个角度看就变透明或变不透明,这个约束大幅降低了模型复杂度。

用来表示这个映射关系的是一个多层感知机(MLP)而且没有卷积层,这个MLP被有意过拟合到特定场景。



渲染流程分三步:沿每条光线采样生成3D点,用网络预测每个点的颜色和密度,最后用体积渲染把这些颜色累积成二维图像。

训练时用梯度下降最小化渲染图像与真实图像之间的差距。不过直接训练效果不好原始5D输入需要经过位置编码转换才能让网络更好地捕捉高频细节。

传统体素表示需要显式存储整个场景占用空间巨大。NeRF则把场景信息压缩在网络参数里,最终模型可以比原始图片集小很多。这是NeRF的一个关键优势。

相关工作

NeRF出现之前,神经场景表示一直比不过体素、三角网格这些离散表示方法。

早期也有人用网络把位置坐标映射到距离函数或占用场,但只能处理ShapeNet这类合成3D数据。



arxiv:1912.07372 用3D占用场做隐式表示提出了可微渲染公式。arxiv:1906.01618的方法在每个3D点输出特征向量和颜色用循环神经网络沿光线移动来检测表面,但这些方法生成的表面往往过于平滑。

如果视角采样足够密集,光场插值技术就能生成新视角。但视角稀疏时必须用表示方法,体积方法能生成真实感强的图像但分辨率上不去。

场景表示机制



输入是位置x= (x, y, z) 和观察方向d= (θ, φ),输出是颜色 c = (r, g, b) 和密度 σ。整个5D映射用MLP来近似。



优化目标是网络权重 Θ。密度被假设为多视角一致的,颜色则同时取决于位置和观察方向。

网络结构上先用8个全连接层处理空间位置,输出密度σ和一个256维特征向量。这个特征再和观察方向拼接,再经过一个全连接层得到颜色。

体积渲染

光线参数化如下:



密度σ描述的是某点对光线的阻挡程度,可以理解为吸收概率。更严格地说它是光线在该点终止的微分概率。根据这个定义,光线从t传播到tₙ的透射概率可以表示为:



σ和T之间的关系可以画图来理解:



密度升高时透射率下降。一旦透射率降到零,后面的东西就完全被遮住了,也就是看不见了。

光线的期望颜色C(r)定义如下,沿光线从近到远积分:



问题在于c和σ都来自神经网络这个积分没有解析解。

实际计算时用数值积分,采用分层采样策略——把积分范围分成N个区间,每个区间均匀随机抽一个点。



分层采样保证MLP在整个优化过程中都能在连续位置上被评估。采样点通过求积公式计算C(t)这个公式选择上考虑了可微性。跟纯随机采样比方差更低。

Tᵢ是光线存活到第i个区间之前的概率。那光线在第i个区间内终止的概率呢?可以用密度来算:



σ越大这个概率越趋近于零,再往下推导:



光线颜色可以写成:



其中:



位置编码

直接拿5D坐标训练MLP,高频细节渲染不出来。因为深度网络天生偏好学习低频信号,解决办法是用高频函数把输入映射到更高维空间。



γ对每个坐标分别应用,是个确定性函数没有可学习参数。p归一化到[-1,+1]。L=4时的编码可视化:



L=4时的位置编码示意

编码用的是不同频率的正弦函数。Transformer里也用类似的位置编码但目的不同——Transformer是为了让模型感知token顺序,NeRF是为了注入高频信息。

分层采样

均匀采样的问题在于大量计算浪费在空旷区域。分层采样的思路是训练两个网络,一个粗糙一个精细。

先用粗糙网络采样评估一批点,再根据结果用逆变换采样在重要区域加密采样。精细网络用两组样本一起计算最终颜色。粗糙网络的颜色可以写成采样颜色的加权和。

实现

每个场景单独训练一个网络,只需要RGB图像作为训练数据。每次迭代从所有像素里采样一批光线,损失函数是粗糙和精细网络预测值与真值之间的均方误差。



接下来从零实现NeRF架构,在一个包含蓝色立方体和红色球体的简单数据集上训练。

数据集生成代码不在本文范围内——只涉及基础几何变换没有NeRF特有的概念。



数据集里的一些渲染图像。相机矩阵和坐标也存在了JSON文件里。



先导入必要的库:

import os, json, math
import numpy as np
from PIL import Image
import torch
import torch.nn as nn
import torch.nn.functional as F

位置编码函数:

def positional_encoding(x, L):
freqs = (2.0 ** torch.arange(L, device=x.device)) * math.pi # Define the frequencies
xb = x[..., None, :] * freqs[:, None] # Multiply by the frequencies
xb = xb.reshape(*x.shape[:-1], L * 3) # Flatten the (x,y,z) coordinates
return torch.cat([torch.sin(xb), torch.cos(xb)], dim=-1)

根据相机参数生成光线:

def get_rays(H, W, camera_angle_x, c2w, device):
# assume the pinhole camera model
fx = 0.5 * W / math.tan(0.5 * camera_angle_x) # calculate the focal lengths (assume fx=fy)
# principal point of the camera or the optical center of the image.
cx = (W - 1) * 0.5
cy = (H - 1) * 0.5
i, j = torch.meshgrid(torch.arange(W, device=device),
torch.arange(H, device=device), indexing="xy")
i, j = i.float(), j.float()
# convert pixels to normalized camera-plane coordinates
x = (i - cx) / fx
y = -(j - cy) / fx
z = -torch.ones_like(x)
# pack into 3D directions and normalize
dirs = torch.stack([x, y, z], dim=-1)
dirs = dirs / torch.norm(dirs, dim=-1, keepdim=True)
# rotate rays into world coordinates using pose matrix
R, t = c2w[:3, :3], c2w[:3, 3]
rd = dirs @ R.T
ro = t.expand_as(rd)
return ro, rd

NeRF网络结构:

class NeRF(nn.Module):
def __init__(self, L_pos=10, L_dir=4, hidden=256):
super().__init__()
# original vector is concatented with the fourier features
in_pos = 3 + 2 * L_pos * 3
in_dir = 3 + 2 * L_dir * 3
self.fc1 = nn.Linear(in_pos, hidden)
self.fc2 = nn.Linear(hidden, hidden)
self.fc3 = nn.Linear(hidden, hidden)
self.fc4 = nn.Linear(hidden, hidden)
self.fc5 = nn.Linear(hidden + in_pos, hidden)
self.fc6 = nn.Linear(hidden, hidden)
self.fc7 = nn.Linear(hidden, hidden)
self.fc8 = nn.Linear(hidden, hidden)
self.sigma = nn.Linear(hidden, 1)
self.feat = nn.Linear(hidden, hidden)
self.rgb1 = nn.Linear(hidden + in_dir, 128)
self.rgb2 = nn.Linear(128, 3)
self.L_pos, self.L_dir = L_pos, L_dir
def forward(self, x, d):
x_enc = torch.cat([x, positional_encoding(x, self.L_pos)], dim=-1)
d_enc = torch.cat([d, positional_encoding(d, self.L_dir)], dim=-1)
h = F.relu(self.fc1(x_enc))
h = F.relu(self.fc2(h))
h = F.relu(self.fc3(h))
h = F.relu(self.fc4(h))
h = torch.cat([h, x_enc], dim=-1) # skip connection
h = F.relu(self.fc5(h))
h = F.relu(self.fc6(h))
h = F.relu(self.fc7(h))
h = F.relu(self.fc8(h))
sigma = F.relu(self.sigma(h)) # density is calculated using positional information
feat = self.feat(h)
h = torch.cat([feat, d_enc], dim=-1) # add directional information for color
h = F.relu(self.rgb1(h))
rgb = torch.sigmoid(self.rgb2(h))
return rgb, sigma

渲染函数,这个是整个流程的核心:

def render_rays(model, ro, rd, near=2.0, far=6.0, N=64):
# sample along the ray
t = torch.linspace(near, far, N, device=ro.device)
pts = ro[:, None, :] + rd[:, None, :] * t[None, :, None] # r = o + td
# attach view directions to each sample
# each point knows where the ray comes from
dirs = rd[:, None, :].expand_as(pts)
# query NeRF at each point and reshape
rgb, sigma = model(pts.reshape(-1,3), dirs.reshape(-1,3))
rgb = rgb.reshape(ro.shape[0], N, 3)
sigma = sigma.reshape(ro.shape[0], N)
# compute the distance between the samples
delta = t[1:] - t[:-1]
delta = torch.cat([delta, torch.tensor([1e10], device=ro.device)])
# convert density into opacity
alpha = 1 - torch.exp(-sigma * delta)
# compute transmittance along the ray
T = torch.cumprod(torch.cat([torch.ones((ro.shape[0],1), device=ro.device),
1 - alpha + 1e-10], dim=-1), dim=-1)[:, :-1]
weights = T * alpha
return (weights[...,None] * rgb).sum(dim=1) # accumulate the colors

训练循环:

device = "cuda" if torch.cuda.is_available() else "cpu"
images, c2ws, H, W, fov = load_dataset("nerf_synth_cube_sphere")
images, c2ws = images.to(device), c2ws.to(device)
model = NeRF().to(device)
opt = torch.optim.Adam(model.parameters(), lr=5e-4)
loss_hist, psnr_hist, iters = [], [], []
for it in range(1, 5001):
idx = torch.randint(0, images.shape[0], (1,)).item()
ro, rd = get_rays(H, W, fov, c2ws[idx], device)
gt = images[idx].reshape(-1,3)
sel = torch.randint(0, ro.numel()//3, (2048,), device=device)
pred = render_rays(model, ro.reshape(-1,3)[sel], rd.reshape(-1,3)[sel])
# for simplicity, we will only implement the coarse sampling.
loss = F.mse_loss(pred, gt[sel])
opt.zero_grad()
loss.backward()
opt.step()
if it % 200 == 0:
psnr = -10 * torch.log10(loss).item()
loss_hist.append(loss.item())
psnr_hist.append(psnr)
iters.append(it)
print(f"Iter {it} | Loss {loss.item():.6f} | PSNR {psnr:.2f} dB")
torch.save(model.state_dict(), "nerf_cube_sphere_coarse.pth")
# ---- Plots ----
plt.figure()
plt.plot(iters, loss_hist, color='red', lw=5)
plt.title("Training Loss")
plt.show()
plt.figure()
plt.plot(iters, psnr_hist, color='black', lw=5)
plt.title("Training PSNR")
plt.show()

迭代次数与PSNR、损失值的变化曲线:



模型训练完成下一步是生成新视角。

look_at函数用于从指定相机位置构建位姿矩阵:

def look_at(eye):
eye = torch.tensor(eye, dtype=torch.float32) # where the camera is
target = torch.tensor([0.0, 0.0, 0.0])
up = torch.tensor([0,1,0], dtype=torch.float32) # which direction is "up" in the world
f = (target - eye); f /= torch.norm(f) # forward direction of the camera
r = torch.cross(f, up); r /= torch.norm(r) # right direction. use cross product between f and up
u = torch.cross(r, f) # true camera up direction
c2w = torch.eye(4)
c2w[:3,0], c2w[:3,1], c2w[:3,2], c2w[:3,3] = r, u, -f, eye
return c2w

推理代码:

device = "cuda" if torch.cuda.is_available() else "cpu"
with open("nerf_synth_cube_sphere/transforms.json") as f:
meta = json.load(f)
H, W, fov = meta["h"], meta["w"], meta["camera_angle_x"]
model = NeRF().to(device)
model.load_state_dict(torch.load("nerf_cube_sphere_coarse.pth", map_location=device))
model.eval()
os.makedirs("novel_views", exist_ok=True)
for i in range(120):
angle = 2 * math.pi * i / 120
eye = [4 * math.cos(angle), 1.0, 4 * math.sin(angle)]
c2w = look_at(eye).to(device)
with torch.no_grad():
ro, rd = get_rays(H, W, fov, c2w, device)
rgb = render_rays(model, ro.reshape(-1,3), rd.reshape(-1,3))
img = rgb.reshape(H, W, 3).clamp(0,1).cpu().numpy()
Image.fromarray((img*255).astype(np.uint8)).save(f"novel_views/view_{i:03d}.png")
print("Rendered view", i)

新视角渲染结果(训练集中没有这些角度):



图中的伪影——椒盐噪声、条纹、浮动的亮点——来自空旷区域的密度估计误差。只用粗糙模型、不做精细采样的情况下这些问题会更明显。另外场景里大片空白区域也是个麻烦,模型不得不花大量计算去探索这些没什么内容的地方。

再看看深度图:



立方体的平面捕捉得相当准确没有幽灵表面。空旷区域有些斑点噪声说明虽然空白区域整体学得还行,但稀疏性还是带来了一些小误差。

参考文献

Mildenhall, B., Srinivasan, P. P., Gharbi, M., Tancik, M., Barron, J. T., Simonyan, K., Abbeel, P., & Malik, J. (2020). NeRF: Representing scenes as neural radiance fields for view synthesis.

https://avoid.overfit.cn/post/4a1b21ea7d754b81b875928c95a45856

作者:Kavishka Abeywardana

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
闫学晶一家三口现身机场!疑似遇到麻烦,老公不停打电话面露难色

闫学晶一家三口现身机场!疑似遇到麻烦,老公不停打电话面露难色

阿纂看事
2026-01-15 15:56:31
原来他就是聂卫平长子,移民日本改国籍娶日本妻,拒绝让儿子姓聂

原来他就是聂卫平长子,移民日本改国籍娶日本妻,拒绝让儿子姓聂

一娱三分地
2026-01-15 16:10:35
女子因厨师长一句不干就滚,在店门口躺了四天,店家还立一块牌子

女子因厨师长一句不干就滚,在店门口躺了四天,店家还立一块牌子

社会日日鲜
2026-01-15 06:52:20
乌克兰继续在改变世界格局

乌克兰继续在改变世界格局

难得君
2026-01-13 19:15:30
三位离退休“老干部”分别在美国、日本和澳大利亚去世...

三位离退休“老干部”分别在美国、日本和澳大利亚去世...

深度报
2026-01-15 22:44:55
湖南省委常委、常务副省长张迎春任新疆维吾尔自治区党委常委

湖南省委常委、常务副省长张迎春任新疆维吾尔自治区党委常委

澎湃新闻
2026-01-16 14:08:26
聂卫平全家福曝光太催泪,3个子女近况各不同,最后露面暴瘦!

聂卫平全家福曝光太催泪,3个子女近况各不同,最后露面暴瘦!

古希腊掌管松饼的神
2026-01-15 13:55:45
聂卫平住院照曝光:吸氧坐轮椅,穿病号服仍不忘下棋,女儿想念他

聂卫平住院照曝光:吸氧坐轮椅,穿病号服仍不忘下棋,女儿想念他

观察鉴娱
2026-01-16 09:47:21
宗馥莉除名娃哈哈大反转,一切都结束了!

宗馥莉除名娃哈哈大反转,一切都结束了!

财经三分钟pro
2026-01-15 16:52:43
安徽一殡仪馆处理逝者遗物时,竟在被子里发现41万存款单……

安徽一殡仪馆处理逝者遗物时,竟在被子里发现41万存款单……

环球网资讯
2026-01-15 15:33:07
马斯克警告中国在AI计算和电力上大幅领先美国:中国电力产能2026年达到美国的3倍

马斯克警告中国在AI计算和电力上大幅领先美国:中国电力产能2026年达到美国的3倍

知识圈
2026-01-15 16:49:27
北京一女子在超市6次盗窃车厘子,被警方刑拘;其在单位做法务、收入高,一斤车厘子价格才30元

北京一女子在超市6次盗窃车厘子,被警方刑拘;其在单位做法务、收入高,一斤车厘子价格才30元

大风新闻
2026-01-15 11:59:04
太突然!李湘遭全平台封号,近期行程被扒令人费解,原因疑曝光

太突然!李湘遭全平台封号,近期行程被扒令人费解,原因疑曝光

古希腊掌管月桂的神
2026-01-16 10:41:50
40岁董方卓拒执教U23国足:除了高血压我能得到啥 我还想多活几年

40岁董方卓拒执教U23国足:除了高血压我能得到啥 我还想多活几年

风过乡
2026-01-16 12:53:25
四川10.91%的HIV携带者感染耐药毒株 成都、德阳与周边城市传播关联紧密

四川10.91%的HIV携带者感染耐药毒株 成都、德阳与周边城市传播关联紧密

小星球探索
2026-01-15 15:04:31
台媒曝大S离世一年,汪小菲与徐家重启谈判,抚养费之争迎来转机

台媒曝大S离世一年,汪小菲与徐家重启谈判,抚养费之争迎来转机

李健政观察
2026-01-16 09:37:46
航母打击群开往中东,特朗普:希望“速战速决”!伊朗进入最高战备状态,约2000枚导弹可覆盖美以基地!多国航班绕飞伊领空

航母打击群开往中东,特朗普:希望“速战速决”!伊朗进入最高战备状态,约2000枚导弹可覆盖美以基地!多国航班绕飞伊领空

每日经济新闻
2026-01-15 19:49:32
中雪+小雪!降温10℃!明起,河北大范围雨雪来袭

中雪+小雪!降温10℃!明起,河北大范围雨雪来袭

鲁中晨报
2026-01-16 13:51:03
经济下行,2026 年、2027 年、2028 年这三年,六大忠告要记牢

经济下行,2026 年、2027 年、2028 年这三年,六大忠告要记牢

互联网思维
2026-01-15 23:32:28
“大牛股”复牌后又涨停,上交所出手:暂停相关投资者账户交易!公司最新公告

“大牛股”复牌后又涨停,上交所出手:暂停相关投资者账户交易!公司最新公告

每日经济新闻
2026-01-14 19:41:12
2026-01-16 14:48:49
deephub incentive-icons
deephub
CV NLP和数据挖掘知识
1891文章数 1443关注度
往期回顾 全部

科技要闻

被网友"催"着走,小米紧急"抄"了特斯拉

头条要闻

上海网红餐厅服务员辱骂顾客:吃到一万四再让我服务

头条要闻

上海网红餐厅服务员辱骂顾客:吃到一万四再让我服务

体育要闻

聂卫平:黑白棋盘上的凡人棋圣

娱乐要闻

黄慧颐手撕保剑锋 曾黎意外卷入风波

财经要闻

深圳有白银商家爆雷 维权群超350人

汽车要闻

经典之上再造经典 BJ40探险家上市 13.49万元起

态度原创

时尚
教育
亲子
本地
游戏

年度最扎心电影,看得中年男女坐立难安

教育要闻

师范生,正在集体失业?这个视频,建议报志愿前看三遍!

亲子要闻

任家萱停工2年备孕还没怀,45岁执着生二胎,近照已是路人模样

本地新闻

云游内蒙|黄沙与碧波撞色,乌海天生会“混搭”

《辐射:伦敦》mod开发者称贝塞斯达应该卖了《辐射》IP

无障碍浏览 进入关怀版