3D 视觉:点云与 NeRF 本节摘要:3D 视觉有两种味道。点云是传感器的原始输出,NeRF 是学出来的体积场,两者都回答「空间里有什么、在哪里」。本节先讲显式表示(点云、网格、体素)与隐式表示(符号距离场、NeRF)的分野,用 PointNet 的对称函数技巧让网络对无序点集做到置换不变;再追踪一次 NeRF 的前向——光线投射、体积渲染、位置编码、密度+颜色 MLP 头;最后用 从少量带位姿图像做 3D 重建。读完本节,你能为给定任务选对 3D 表示,并理解为何 2026 年的生产系统几乎都换成了 3D 高斯泼溅。 对应原课程:Phase 4 · Lesson 13 · (原英文 )。
本节摘要: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)。
阅读完本节,你应当能够:
nerfstudio 或 instant-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), ]
没有网格、没有连接。两个性质让神经网络犯难:
PointNet(Qi 等,2017)用一个想法解决了两者:对每个点套同一个共享 MLP,再用对称函数(最大池化)聚合。结果是一个与顺序无关的定长向量。
f(P) = max_{p in P} MLP(p)
这就是 PointNet 的全部核心。更深的变体(PointNet++、Point Transformer)加了层级采样和局部聚合,但对称函数技巧不变。
「共享 MLP」指同一个 MLP 独立跑在每个点上。为效率实现为对点维度的 1×1 卷积。
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 权重里。
对 (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 训练慢(数小时)、渲染也慢(每图数秒)。其后谱系:
2026 年几乎每个真实 NeRF 产品其实都是 3D 高斯泼溅。心智模型仍是 NeRF。
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 个点上跑。
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 给出逐步升高的频率。
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 主干)相比是微型的,但足以演示架构。
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,含正确的归一化、居中和点采样。(x,y,z,dir) → (密度,颜色),渲染靠光线投射循环;无 3D 真值,场景存在权重里。C(r)=Σ T_i(1−exp(−σ_i·δ_i))c_i,沿光线积分。下一节讲视觉 Transformer——把 NLP 里的自注意力搬进图像,用 patch 化取代卷积,开启 ViT 时代。