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

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®定义如下,沿光线从近到远积分:

问题在于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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1131345.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

综合项目实战——电子商城信息查询系统

目录 一、项目核心目标 二、技术选型解析 三、系统整体设计 3.1 架构设计 3.2 核心模块介绍 3.3 系统流程图 四、核心功能实现详解 4.1 用户注册与登录 4.1.1 注册功能实现 4.1.2 登录功能实现 4.2 商品搜索 4.2.1 关键词模糊查询 4.2.2 ID精确查询 4.2.3 动态页…

多台NAS管理新方案:节点小宝4.0服务聚合功能深度体验

作为拥有多台NAS设备的用户,一定在寻找能够统一管理这些设备的解决方案。最近有用户深度体验了节点小宝4.0的服务聚合功能,这给设备管理带来了全新的体验,以下是用户的一些自述:智能服务发现:让设备管理变得简单直观最…

端子与导轨布线工程实践:Altech 工控组件解析

在工业控制系统、自动化设备以及配电柜构建中,端子块与 DIN 导轨布线方案是实现安全、整洁、可靠连接的基础构件。Altech Corporation 是一家拥有多年工业控制元件供应经验的公司,其 DIN 导轨端子块、端子接线解决方案在国际工控市场中被广泛采纳&#x…

大数据场景下ZooKeeper的性能优化秘籍

大数据场景下ZooKeeper的性能优化秘籍 关键词:ZooKeeper、大数据、性能优化、分布式协调、会话管理 摘要:在大数据生态中,ZooKeeper作为Hadoop、Kafka、HBase等系统的"分布式协调管家",常因高并发、海量节点、复杂会话管…

FPGA教程系列-流水线思想初识

FPGA教程系列-流水线思想初识 流水线设计是一种典型的面积换性能的设计。一方面通过对长功能路径的合理划分,在同一时间内同时并行多个该功能请求,大大提高了某个功能的吞吐率;另一方面由于长功能路径被切割成短路径,可以达到更高…

AI原生应用语音合成:助力智能政务语音服务

AI原生应用语音合成:助力智能政务语音服务 关键词:AI原生应用、语音合成、智能政务、TTS技术、自然语言处理、人机交互、政务服务升级 摘要:本文从智能政务的实际需求出发,深度解析AI原生语音合成技术的核心原理与政务场景的适配逻辑。通过“技术原理-场景落地-实战案例”的…

LangChainV1.0[08]-LCEL:LangChain Expression Language

Chain翻译成中文就是“链”,我们将大模型、相关工具等作为组件,链就是负责将这些组件按照某一种逻辑,顺序组合成一个流水线的方式。比如我们要构建一个简单的问答链,就需要把大模型组件和标准输出组件用链串联起来。 1.简单链 fro…

托盘输送机程序那些事儿

托盘输送机程序 硬件配置:PLC:1500SP F-1PN HMI:KTP700 Basic PN 和上位WCS通讯是通过S7读写DB背景数据块的方式实现 程序提供两个版本,V1是源自北起院,看起来比较难懂,各种状态字;V2源自外企&a…

ImageMagick 高效图像处理与自动化指南

在处理海量数字图像时,依靠图形化界面进行逐一操作不仅低效,且极易产生人为失误。ImageMagick 并非一款为绘图设计的交互软件,而是一个专门通过命令行执行复杂图像处理任务的二进制工具集。它被广泛应用于后端开发、自动化运维以及高性能图像…

风速weibull分布随机风速生成Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。 🍎 往期回顾关注个人主页:Matlab科研工作室 👇 关注我领取海量matlab电子书和数学建模资料 &#x1…

Amphenol LTW 防水线缆 IP67/IP68 结构解析

在工业自动化、户外设备、LED 照明以及传感器系统中,防水线缆组件是保障系统稳定运行的重要基础件。其中,Amphenol LTW 作为专注于防水连接技术的品牌,其防水线缆在 IP67、IP68 等等级应用中具有较高的工程参考价值。 本文从工程应用角度出发…

Linux 网络编程:epoll 实现聊天室

这是 epoll 进阶实战的经典案例 —— 基于epoll 边缘触发(ET) 非阻塞 IO实现高并发聊天室,同时解决 10000 并发连接时的系统限制问题,是理解 epoll 在实际项目中落地的核心实践!一、核心需求与设计思路1. 功能目标支持…

Python 虚拟环境的配置与管理指南

虚拟环境的核心原理 虚拟环境并非重新安装了一套完整的 Python,而是在项目目录下创建了一个包含 Python 解释器副本和独立包管理工具的轻量级目录。激活环境后,系统会将该目录的路径推送到环境变量的最前端,使得终端在调用 Python 指令时优先…

TensorFlow学习系列01 | 实现mnist手写数字识别

🍨 本文为🔗365天深度学习训练营中的学习记录博客🍖 原作者:K同学啊 一、前置知识 1、知识总结 概念 作用 归一化 统一数据范围,加速训练 卷积层 提取图像局部特征 池化层 压缩数据,增强鲁棒性 全…

强烈安利8个AI论文网站,自考学生轻松搞定毕业论文!

强烈安利8个AI论文网站,自考学生轻松搞定毕业论文! 自考论文的“救星”:AI 工具如何改变你的写作方式 对于自考学生来说,撰写毕业论文往往是一项既耗时又充满挑战的任务。从选题到结构搭建,再到内容撰写和查重降重&…

热电联产在综合能源系统中的选址定容研究Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。 🍎 往期回顾关注个人主页:Matlab科研工作室 👇 关注我领取海量matlab电子书和数学建模资料 &#x1…

广汽与华为签署全面合作框架协议,深化鸿蒙生态与AI融合

华为ai 2026年1月5日,广汽集团与华为终端在广州签署全面合作框架协议,标志着双方战略合作进一步升级至生态融合新阶段。华为常务董事、终端BG董事长余承东一行到访广汽集团番禺总部,与广汽集团董事长冯兴亚等高层就智能汽车技术演进、产业协同…

别再说Redis是单线程了,这才是它真正的线程模型

“Redis是单线程的。” 这句话你可能听过无数遍。面试官问你Redis为什么快,你脱口而出:"因为它是单线程的,避免了线程切换开销。"面试官满意地点点头,你也觉得自己答对了。 但这个答案,只对了一半。 从Redis 6.0开始,Redis就不再是纯粹的单线程了。它引入了…

为什么WiFi已连接却有感叹号?4种方法修复

在使用电脑连接Wi-Fi时,你是否遇到过无线网络图标旁边出现黄色感叹号的情况?这通常表示网络连接存在问题,导致无法正常上网。下面就为大家分享几种解决方法,帮助你快速恢复网络连接。 方法1:检查路由器与宽带是否正常 …

【开题答辩全过程】以 基于SSM的固定资产管理系统设计与实现为例,包含答辩的问题和答案

个人简介一名14年经验的资深毕设内行人,语言擅长Java、php、微信小程序、Python、Golang、安卓Android等开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。感谢大家的…