Phase 04 - Lesson 13
Visão 3D — Nuvens de Pontos e NeRFs
A visão 3D vem em dois sabores. Nuvens de pontos são a saída bruta do sensor. NeRFs são o campo volumétrico aprendido. Ambos respondem "o que está onde no espaço".
Tipo: Aprender + Construir Linguagens: Python Pré-requisitos: Fase 4 Lição 03 (CNNs), Fase 1 Lição 12 (Operações com Tensores) Tempo: ~45 minutos
Objetivos de Aprendizagem
- Distinguir representações 3D explícitas (nuvem de pontos, malha, voxel) e implícitas (campo de distância com sinal, NeRF) e quando cada uma é usada
- Entender o truque da função simétrica do PointNet que torna uma rede neural invariante a permutações sobre um conjunto não ordenado de pontos
- Acompanhar uma passagem direta de um NeRF: lançamento de raios, renderização volumétrica, codificação posicional, MLP com cabeça de densidade+cor
- Usar
nerfstudioouinstant-ngppara reconstrução 3D pré-treinada a partir de um pequeno conjunto de imagens com pose conhecida
O Problema
Uma câmera produz uma imagem 2D. Um LIDAR produz um conjunto de pontos 3D sem ordenação. Um pipeline de structure-from-motion produz uma nuvem esparsa de pontos-chave 3D. Um NeRF reconstrói uma cena 3D inteira a partir de um punhado de imagens com pose conhecida. Tudo isso é "visão", mas nada disso se parece com o tensor denso que uma CNN quer.
A visão 3D importa porque quase toda tarefa robótica de alto valor roda em 3D: agarrar, evitar obstáculos, navegação, oclusão em RA, captura de conteúdo 3D. Um engenheiro de visão que só entende imagens 2D fica trancado fora da fatia que mais cresce no campo (conteúdo RA/RV, robótica, pilhas de direção autônoma, reconstrução 3D baseada em NeRF para imóveis ou construção).
As duas representações dominam por motivos diferentes. Nuvens de pontos são o que os sensores entregam de graça. NeRFs e seus sucessores (3D Gaussian splatting, SDFs neurais) são o que você obtém quando pede a uma rede neural para aprender uma cena.
O Conceito
Nuvens de pontos
Uma nuvem de pontos é um conjunto não ordenado de N pontos em R^3, opcionalmente cada um com features (cor, intensidade, normal).
cloud = [
(x1, y1, z1, r1, g1, b1),
(x2, y2, z2, r2, g2, b2),
...
(xN, yN, zN, rN, gN, bN),
]
Sem grade, sem conectividade. Duas propriedades tornam isso difícil para redes neurais:
- Invariância a permutações — a saída não pode depender da ordem dos pontos.
- N variável — um único modelo precisa lidar com nuvens de tamanhos diferentes.
O PointNet (Qi et al., 2017) resolveu ambos com uma ideia: aplicar uma MLP compartilhada a cada ponto, depois agregar com uma função simétrica (max pool). O resultado é um vetor de tamanho fixo que não depende da ordem.
f(P) = max_{p in P} MLP(p)
Este é o núcleo inteiro do PointNet. Variantes mais profundas (PointNet++, Point Transformer) adicionam amostragem hierárquica e agregação local, mas o truque da função simétrica permanece inalterado.
A arquitetura do PointNet
flowchart LR
PTS["N pontos<br/>(x, y, z)"] --> MLP1["MLP compartilhada<br/>(64, 64)"]
MLP1 --> MLP2["MLP compartilhada<br/>(64, 128, 1024)"]
MLP2 --> MAX["max pool<br/>(simétrico)"]
MAX --> FEAT["feature global<br/>(1024,)"]
FEAT --> FC["classificador MLP"]
FC --> CLS["logits de classe"]
style MLP1 fill:#dbeafe,stroke:#2563eb
style MAX fill:#fef3c7,stroke:#d97706
style CLS fill:#dcfce7,stroke:#16a34a
"MLP compartilhada" significa que a mesma MLP roda em cada ponto independentemente. Implementada como uma convolução 1x1 sobre a dimensão de pontos por eficiência.
Campos de Radiância Neural (NeRFs)
Os NeRFs (Mildenhall et al., 2020) pegaram a pergunta "podemos reconstruir uma cena 3D a partir de N fotos?" e responderam com uma rede neural que é a cena. A rede mapeia (x, y, z, viewing_direction) para (density, colour). Renderizar uma nova vista é um laço de lançamento de raios sobre essa rede.
NeRF MLP: (x, y, z, theta, phi) -> (sigma, r, g, b)
To render a pixel (u, v) of a new view:
1. Cast a ray from the camera through pixel (u, v)
2. Sample points along the ray at distances t_1, t_2, ..., t_N
3. Query the MLP at each point
4. Composite the colours weighted by (1 - exp(-sigma * dt))
5. The sum is the rendered pixel colour
Uma loss compara o pixel renderizado ao pixel ground-truth nas fotos de treino. O backprop através da etapa de renderização atualiza a MLP. Sem ground truth 3D, sem geometria explícita — a cena é armazenada nos pesos da MLP.
Codificação posicional em NeRF
Uma MLP comum sobre (x, y, z) não consegue representar detalhes de alta frequência porque MLPs são espectralmente enviesadas para baixas frequências. O NeRF corrige isso codificando cada coordenada em um vetor de features de Fourier antes da MLP:
gamma(p) = (sin(2^0 pi p), cos(2^0 pi p), sin(2^1 pi p), cos(2^1 pi p), ...)
Até L=10 níveis de frequência. Este é o mesmo truque que os transformers usam para posições, e ele reaparece no condicionamento temporal de difusão (Lição 10). Sem ele, os NeRFs ficam borrados.
Renderização volumétrica
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 é a transmitância — quanta luz sobrevive até o ponto i. (1 - exp(-sigma_i * delta_i)) é a opacidade no ponto i. c_i é a cor. O pixel final é uma soma ponderada ao longo do raio.
O que substituiu os NeRFs
NeRFs puros são lentos para treinar (horas) e lentos para renderizar (segundos por imagem). A linhagem desde então:
- Instant-NGP (2022) — a codificação por grade de hash substitui a entrada de posição da MLP; treina em segundos.
- Mip-NeRF 360 — lida com cenas ilimitadas e antialiasing.
- 3D Gaussian Splatting (2023) — substitui o campo volumétrico por milhões de gaussianas 3D; treina em minutos, renderiza em tempo real. O padrão de produção atual.
Quase todo produto NeRF real em 2026 é na verdade 3D Gaussian splatting. O modelo mental ainda é o NeRF.
Conjuntos de dados e benchmarks
- ShapeNet — classificação e segmentação de modelos CAD 3D como nuvens de pontos.
- ScanNet — scans internos reais para segmentação.
- KITTI — nuvens de pontos LIDAR ao ar livre para direção autônoma.
- NeRF Synthetic / Blended MVS — conjuntos de imagens com pose para síntese de vistas.
- Mip-NeRF 360 dataset — cenas reais ilimitadas.
Construa
Passo 1: Classificador 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) — transposed for 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"output: {net(pts).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")
Cerca de 1,6M parâmetros. Roda em 1.024 pontos por nuvem.
Passo 2: Codificação posicional
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"input: {x.shape}")
print(f"encoded: {y.shape} # (5, 60)")
Multiplicar por 2^l * pi dá frequências progressivamente mais altas.
Passo 3: MLP Tiny NeRF
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}")
Minúsculo comparado ao NeRF original (que tem 2 troncos de MLP de profundidade 8). Suficiente para demonstrar a arquitetura.
Passo 4: Renderização volumétrica ao longo de um raio
def volumetric_render(sigma, rgb, t_vals):
"""
sigma: (..., N_samples)
rgb: (..., N_samples, 3)
t_vals: (N_samples,) distances along the ray
"""
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 colour: {rendered.tolist()}")
print(f"depth: {depth.item():.2f}")
Um raio, 64 amostras, compostas em um único pixel RGB e uma profundidade.
Use
Para trabalho real:
nerfstudio(Tancik et al.) — a biblioteca de referência atual para NeRF / Instant-NGP / Gaussian Splatting. Linha de comando mais um visualizador web.pytorch3d(Meta) — renderização diferenciável, utilitários de nuvem de pontos, operações de malha.open3d— processamento de nuvem de pontos, registro, visualização.
Para implantação, o 3D Gaussian splatting substituiu em grande parte os NeRFs puros porque renderiza 100x mais rápido. A qualidade da reconstrução é comparável.
Entregue
Esta lição produz:
outputs/prompt-3d-task-router.md— um prompt que roteia para a representação 3D certa (nuvem de pontos, malha, voxel, NeRF, Gaussian splat) com base na tarefa e nos dados de entrada.outputs/skill-point-cloud-loader.md— uma skill que escreve umDatasetdo PyTorch para arquivos .ply / .pcd / .xyz com normalização, centralização e amostragem de pontos corretas.
Exercícios
- (Fácil) Mostre que o PointNet é invariante a permutações: rode a mesma nuvem duas vezes, uma vez com os pontos embaralhados. Verifique que as saídas são idênticas até o ruído de ponto flutuante.
- (Médio) Implemente uma função mínima de geração de raios que, dadas as intrínsecas e a pose da câmera, produza origens e direções de raios para cada pixel de uma imagem H x W.
- (Difícil) Treine um TinyNeRF em um conjunto de dados sintético de vistas renderizadas de um cubo colorido (gerado via renderização diferenciável ou um ray tracer simples). Reporte a loss de renderização nas épocas 1, 10 e 100. Em qual época o modelo produz vistas reconhecíveis?
Termos-Chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| Nuvem de pontos | "Pontos 3D do LIDAR" | Conjunto não ordenado de (x, y, z) + features opcionais por ponto |
| PointNet | "Primeira rede neural em nuvens de pontos" | MLP compartilhada por ponto + pool simétrico (max); invariante a permutações por construção |
| NeRF | "MLP que é a cena" | Rede mapeando (x, y, z, dir) para (densidade, cor); renderizada por lançamento de raios |
| Codificação posicional | "Features de Fourier" | Codifica cada coordenada em sin/cos em múltiplas frequências para superar o viés de baixa frequência da MLP |
| Renderização volumétrica | "Integração de raio" | Compõe amostras ao longo de um raio em um único pixel usando transmitância e alfa |
| Instant-NGP | "NeRF de grade de hash" | Substitui a MLP de coordenadas do NeRF por uma grade de hash multirresolução; 100-1000x mais rápido |
| 3D Gaussian splatting | "Milhões de gaussianas" | Cena = coleção de gaussianas 3D; renderiza em tempo real, treina em minutos |
| SDF | "Campo de distância com sinal" | Função que retorna a distância com sinal até a superfície mais próxima; outra representação implícita |
Leitura Adicional
- PointNet (Qi et al., 2017) — o classificador invariante a permutações
- NeRF (Mildenhall et al., 2020) — o artigo que tornou a reconstrução 3D a partir de fotos um problema de rede neural
- Instant-NGP (Müller et al., 2022) — grades de hash, aceleração de 1000x
- 3D Gaussian Splatting (Kerbl et al., 2023) — a arquitetura que substituiu os NeRFs em produção