3D 视觉:点云与 NeRF


文档摘要

3D 视觉:点云与 NeRF 本节摘要:3D 视觉有两种味道。点云是传感器的原始输出,NeRF 是学出来的体积场,两者都回答「空间里有什么、在哪里」。本节先讲显式表示(点云、网格、体素)与隐式表示(符号距离场、NeRF)的分野,用 PointNet 的对称函数技巧让网络对无序点集做到置换不变;再追踪一次 NeRF 的前向——光线投射、体积渲染、位置编码、密度+颜色 MLP 头;最后用 从少量带位姿图像做 3D 重建。读完本节,你能为给定任务选对 3D 表示,并理解为何 2026 年的生产系统几乎都换成了 3D 高斯泼溅。 对应原课程:Phase 4 · Lesson 13 · (原英文 )。

3D 视觉:点云与 NeRF

本节摘要:3D 视觉有两种味道。点云是传感器的原始输出,NeRF 是学出来的体积场,两者都回答「空间里有什么、在哪里」。本节先讲显式表示(点云、网格、体素)与隐式表示(符号距离场、NeRF)的分野,用 PointNet 的对称函数技巧让网络对无序点集做到置换不变;再追踪一次 NeRF 的前向——光线投射、体积渲染、位置编码、密度+颜色 MLP 头;最后用 nerfstudio 从少量带位姿图像做 3D 重建。读完本节,你能为给定任务选对 3D 表示,并理解为何 2026 年的生产系统几乎都换成了 3D 高斯泼溅。

对应原课程:Phase 4 · Lesson 13 · 3d-vision-nerf(原英文 phases/04-computer-vision/13-3d-vision-nerf/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 区分显式表示(点云、网格、体素)与隐式表示(符号距离场、NeRF),说出各自适用场景。
  2. 理解 PointNet 的对称函数技巧,使网络对无序点集置换不变。
  3. 追踪一次 NeRF 前向:光线投射、体积渲染、位置编码、密度+颜色 MLP
  4. nerfstudioinstant-ngp 从少量带位姿图像做预训练 3D 重建。

一、问题与直觉

相机产出 2D 图像,LIDAR 产出一组无序的 3D 点,运动恢复结构流水线产出稀疏的 3D 关键点云,NeRF 从少数带位姿的图像重建整个 3D 场景。这些都是「视觉」,但没有一个长得像 CNN 想要的稠密张量。

3D 视觉之所以重要,是因为几乎所有高价值机器人任务都跑在 3D 里:抓取、避障、导航、AR 遮挡、3D 内容采集。只会 2D 图像的视觉工程师,会被锁在这个领域增长最快的切片之外(AR/VR 内容、机器人、自动驾驶栈、面向房地产或施工的 NeRF 3D 重建)。

两种表示各因不同原因主导。点云是传感器白送的;NeRF 及其后继者(3D 高斯泼溅、神经 SDF)是你让神经网络去学一个场景时得到的。

点云

点云是 R³ 中 N 个点的无序集合,每个点可选带特征(颜色、强度、法向量)。

cloud = [ (x1, y1, z1, r1, g1, b1), (x2, y2, z2, r2, g2, b2), ... (xN, yN, zN, rN, gN, bN), ]

没有网格、没有连接。两个性质让神经网络犯难:

  • 置换不变性——输出不能依赖点的顺序。
  • N 可变——同一个模型要处理不同大小的点云。

PointNet(Qi 等,2017)用一个想法解决了两者:对每个点套同一个共享 MLP,再用对称函数(最大池化)聚合。结果是一个与顺序无关的定长向量。

f(P) = max_{p in P} MLP(p)

这就是 PointNet 的全部核心。更深的变体(PointNet++、Point Transformer)加了层级采样和局部聚合,但对称函数技巧不变。

PointNet 架构

「共享 MLP」指同一个 MLP 独立跑在每个点上。为效率实现为对点维度的 1×1 卷积。

神经辐射场(NeRF)

NeRF(Mildenhall 等,2020)把「能否从 N 张照片重建 3D 场景?」这个问题,用一个本身就是场景的神经网络来回答。网络把 (x, y, z, 视方向) 映到 (密度, 颜色)。渲染新视角就是在这个网络上做光线投射循环。

NeRF MLP: (x, y, z, theta, phi) -> (sigma, r, g, b) 渲染新视角中像素 (u, v): 1. 从相机穿过像素 (u, v) 投射一条光线 2. 沿光线在距离 t_1, t_2, ..., t_N 处采样点 3. 在每个点查询 MLP 4. 用 (1 - exp(-sigma * dt)) 作权重合成颜色 5. 求和得到渲染像素颜色

损失比较渲染像素与训练照片里的真值像素。通过渲染步反传更新 MLP。没有 3D 真值、没有显式几何——场景存在 MLP 权重里。

NeRF 的位置编码

(x, y, z) 的朴素 MLP 无法表示高频细节,因为 MLP 在频谱上偏向低频。NeRF 的修法是把每个坐标先编码成傅里叶特征向量,再喂给 MLP:

gamma(p) = (sin(2^0 · pi · p), cos(2^0 · pi · p), sin(2^1 · pi · p), cos(2^1 · pi · p), ...)

最多 L=10 个频率层级。这跟 Transformer 的位置编码是同一招,第 10 节扩散的时间条件也再用了一次。没有它,NeRF 看起来就模糊。

体积渲染

C(r) = sum_i T_i * (1 - exp(-sigma_i * delta_i)) * c_i T_i = exp(- sum_{j<i} sigma_j * delta_j) delta_i = t_{i+1} - t_i

T_i 是透射率——光存活到点 i 的比例。(1 - exp(-sigma_i · delta_i)) 是点 i 处的不透明度。c_i 是颜色。最终像素是沿光线的加权和。

取代 NeRF 的东西

纯 NeRF 训练慢(数小时)、渲染也慢(每图数秒)。其后谱系:

  • Instant-NGP(2022)——哈希网格编码替代 MLP 的位置输入,秒级训练。
  • Mip-NeRF 360——处理无界场景和抗锯齿。
  • 3D 高斯泼溅(2023)——用数百万个 3D 高斯替代体积场,分钟级训练、实时渲染,当前生产默认。

2026 年几乎每个真实 NeRF 产品其实都是 3D 高斯泼溅。心智模型仍是 NeRF。

数据集与基准

  • ShapeNet——3D CAD 模型作为点云的分类与分割。
  • ScanNet——真实室内扫描,用于分割。
  • KITTI——自动驾驶的户外 LIDAR 点云。
  • NeRF Synthetic / Blended MVS——视角合成的带位姿图像数据集。
  • Mip-NeRF 360 数据集——无界真实场景。

二、从零实现

步骤 1:PointNet 分类器

import torch import torch.nn as nn class PointNet(nn.Module): def __init__(self, num_classes=10): super().__init__() self.mlp1 = nn.Sequential( nn.Conv1d(3, 64, 1), nn.BatchNorm1d(64), nn.ReLU(inplace=True), nn.Conv1d(64, 64, 1), nn.BatchNorm1d(64), nn.ReLU(inplace=True), ) self.mlp2 = nn.Sequential( nn.Conv1d(64, 128, 1), nn.BatchNorm1d(128), nn.ReLU(inplace=True), nn.Conv1d(128, 1024, 1), nn.BatchNorm1d(1024), nn.ReLU(inplace=True), ) self.head = nn.Sequential( nn.Linear(1024, 512), nn.BatchNorm1d(512), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(512, 256), nn.BatchNorm1d(256), nn.ReLU(inplace=True), nn.Dropout(0.3), nn.Linear(256, num_classes), ) def forward(self, x): # x: (N, 3, num_points) — 为 Conv1d 转置过 x = self.mlp1(x) x = self.mlp2(x) x = torch.max(x, dim=-1)[0] # (N, 1024) return self.head(x) pts = torch.randn(4, 3, 1024) net = PointNet(num_classes=10) print(f"输出: {net(pts).shape}") print(f"参数量: {sum(p.numel() for p in net.parameters()):,}")

约 160 万参数,每点云 1024 个点上跑。

步骤 2:位置编码

def positional_encoding(x, L=10): """ x: (..., D) -> (..., D * 2 * L) """ freqs = 2.0 ** torch.arange(L, dtype=x.dtype, device=x.device) args = x.unsqueeze(-1) * freqs * 3.141592653589793 sinc = torch.cat([args.sin(), args.cos()], dim=-1) return sinc.reshape(*x.shape[:-1], -1) x = torch.randn(5, 3) y = positional_encoding(x, L=10) print(f"输入: {x.shape}") print(f"编码后: {y.shape} # (5, 60)")

乘以 2^l · pi 给出逐步升高的频率。

步骤 3:微型 NeRF MLP

class TinyNeRF(nn.Module): def __init__(self, L_pos=10, L_dir=4, hidden=128): super().__init__() self.L_pos = L_pos self.L_dir = L_dir pos_dim = 3 * 2 * L_pos dir_dim = 3 * 2 * L_dir self.trunk = nn.Sequential( nn.Linear(pos_dim, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, hidden), nn.ReLU(inplace=True), ) self.sigma = nn.Linear(hidden, 1) self.color = nn.Sequential( nn.Linear(hidden + dir_dim, hidden // 2), nn.ReLU(inplace=True), nn.Linear(hidden // 2, 3), nn.Sigmoid(), ) def forward(self, x, d): x_enc = positional_encoding(x, self.L_pos) d_enc = positional_encoding(d, self.L_dir) h = self.trunk(x_enc) sigma = torch.relu(self.sigma(h)).squeeze(-1) rgb = self.color(torch.cat([h, d_enc], dim=-1)) return sigma, rgb nerf = TinyNeRF() x = torch.randn(128, 3) d = torch.randn(128, 3) s, c = nerf(x, d) print(f"sigma: {s.shape} rgb: {c.shape}")

跟原版 NeRF(两个深度 8 的 MLP 主干)相比是微型的,但足以演示架构。

步骤 4:沿光线的体积渲染

def volumetric_render(sigma, rgb, t_vals): """ sigma: (..., N_samples) rgb: (..., N_samples, 3) t_vals: (N_samples,) 沿光线距离 """ delta = torch.cat([t_vals[1:] - t_vals[:-1], torch.full_like(t_vals[:1], 1e10)]) alpha = 1.0 - torch.exp(-sigma * delta) trans = torch.cumprod(torch.cat([torch.ones_like(alpha[..., :1]), 1.0 - alpha + 1e-10], dim=-1), dim=-1)[..., :-1] weights = alpha * trans rendered = (weights.unsqueeze(-1) * rgb).sum(dim=-2) depth = (weights * t_vals).sum(dim=-1) return rendered, depth, weights N = 64 t_vals = torch.linspace(2.0, 6.0, N) sigma = torch.rand(N) * 0.5 rgb = torch.rand(N, 3) rendered, depth, weights = volumetric_render(sigma, rgb, t_vals) print(f"渲染颜色: {rendered.tolist()}") print(f"深度: {depth.item():.2f}")

一条光线、64 个采样点,合成出一个 RGB 像素和深度。

三、框架对比

真活儿用:

  • nerfstudio(Tancik 等)——NeRF / Instant-NGP / 高斯泼溅的当前参考库。命令行加网页查看器。
  • pytorch3d(Meta)——可微渲染、点云工具、网格操作。
  • open3d——点云处理、配准、可视化。

部署上,3D 高斯泼溅因渲染快 100 倍已基本取代纯 NeRF,重建质量相当。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-3d-task-router.md:一个提示词——按任务和输入数据,路由到正确的 3D 表示(点云、网格、体素、NeRF、高斯泼溅)。
  • skill-point-cloud-loader.md:一个技能——为 .ply / .pcd / .xyz 文件写一个 PyTorch Dataset,含正确的归一化、居中和点采样。

五、练习

  1. (简单) 证明 PointNet 置换不变:把同一个点云跑两遍,一次打乱点的顺序,验证输出在浮点噪声范围内一致。
  2. (中等) 实现一个最小的射线生成函数,给定相机内参和位姿,为 H×W 图像的每个像素产出射线原点和方向。
  3. (困难) 在彩色立方体渲染视角的合成数据集(经可微渲染或简单光线追踪生成)上训 TinyNeRF。报告 epoch 1、10、100 处的渲染损失。模型在哪个 epoch 能产出可辨认的视角?

本节要点回顾

  1. 两种 3D 表示:点云是传感器白送的无序点集,NeRF 是学出来的体积场,都回答「什么在哪里」。
  2. PointNet 的对称函数技巧——共享 MLP + 最大池化,天然置换不变;后续变体不变这个核心。
  3. NeRF = 网络就是场景——(x,y,z,dir) → (密度,颜色),渲染靠光线投射循环;无 3D 真值,场景存在权重里。
  4. 位置编码必须——傅里叶特征克服 MLP 的低频偏置;没有它 NeRF 模糊。
  5. 体积渲染 = 透射率加权合成——C(r)=Σ T_i(1−exp(−σ_i·δ_i))c_i,沿光线积分。
  6. NeRF 已被取代——Instant-NGP(哈希网格,秒级)、Mip-NeRF 360(无界)、3D 高斯泼溅(实时,分钟训)。
  7. 2026 年生产默认是 3D 高斯泼溅——渲染快 100 倍,质量相当;心智模型仍是 NeRF。
  8. 工具链:nerfstudio(重建)、pytorch3d(可微渲染)、open3d(点云处理)。

下一节讲视觉 Transformer——把 NLP 里的自注意力搬进图像,用 patch 化取代卷积,开启 ViT 时代。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U