Phase 04 - Lesson 13
Visión 3D — Nubes de Puntos y NeRFs
La visión 3D viene en dos sabores. Las nubes de puntos son la salida cruda del sensor. Los NeRFs son el campo volumétrico aprendido. Ambos responden "qué hay dónde en el espacio".
Tipo: Aprender + Construir Lenguajes: Python Prerrequisitos: Fase 4 Lección 03 (CNNs), Fase 1 Lección 12 (Operaciones con Tensores) Tiempo: ~45 minutos
Objetivos de Aprendizaje
- Distinguir representaciones 3D explícitas (nube de puntos, malla, vóxel) e implícitas (campo de distancia con signo, NeRF) y cuándo se usa cada una
- Entender el truco de la función simétrica de PointNet que hace que una red neuronal sea invariante a permutaciones sobre un conjunto no ordenado de puntos
- Seguir una pasada hacia adelante de un NeRF: lanzamiento de rayos, renderizado volumétrico, codificación posicional, MLP con cabeza de densidad+color
- Usar
nerfstudiooinstant-ngppara reconstrucción 3D preentrenada a partir de un pequeño conjunto de imágenes con pose conocida
El Problema
Una cámara produce una imagen 2D. Un LIDAR produce un conjunto de puntos 3D sin orden. Un pipeline de structure-from-motion produce una nube dispersa de puntos clave 3D. Un NeRF reconstruye una escena 3D completa a partir de un puñado de imágenes con pose conocida. Todo esto es "visión", pero nada de esto se parece al tensor denso que una CNN quiere.
La visión 3D importa porque casi toda tarea robótica de alto valor opera en 3D: agarre, evasión de obstáculos, navegación, oclusión en RA, captura de contenido 3D. Un ingeniero de visión que solo entiende imágenes 2D queda excluido de la porción de mayor crecimiento del campo (contenido RA/RV, robótica, pilas de conducción autónoma, reconstrucción 3D basada en NeRF para bienes raíces o construcción).
Las dos representaciones dominan por razones distintas. Las nubes de puntos son lo que los sensores te entregan gratis. Los NeRFs y sus sucesores (3D Gaussian splatting, SDFs neuronales) son lo que obtienes cuando le pides a una red neuronal que aprenda una escena.
El Concepto
Nubes de puntos
Una nube de puntos es un conjunto no ordenado de N puntos en R^3, opcionalmente cada uno con características (color, intensidad, normal).
cloud = [
(x1, y1, z1, r1, g1, b1),
(x2, y2, z2, r2, g2, b2),
...
(xN, yN, zN, rN, gN, bN),
]
Sin grilla, sin conectividad. Dos propiedades hacen esto difícil para las redes neuronales:
- Invarianza a permutaciones — la salida no debe depender del orden de los puntos.
- N variable — un solo modelo debe manejar nubes de distintos tamaños.
PointNet (Qi et al., 2017) resolvió ambas con una idea: aplicar una MLP compartida a cada punto, luego agregar con una función simétrica (max pool). El resultado es un vector de tamaño fijo que no depende del orden.
f(P) = max_{p in P} MLP(p)
Este es el núcleo completo de PointNet. Las variantes más profundas (PointNet++, Point Transformer) agregan muestreo jerárquico y agregación local, pero el truco de la función simétrica permanece sin cambios.
La arquitectura de PointNet
flowchart LR
PTS["N puntos<br/>(x, y, z)"] --> MLP1["MLP compartida<br/>(64, 64)"]
MLP1 --> MLP2["MLP compartida<br/>(64, 128, 1024)"]
MLP2 --> MAX["max pool<br/>(simétrico)"]
MAX --> FEAT["característica global<br/>(1024,)"]
FEAT --> FC["clasificador MLP"]
FC --> CLS["logits de clase"]
style MLP1 fill:#dbeafe,stroke:#2563eb
style MAX fill:#fef3c7,stroke:#d97706
style CLS fill:#dcfce7,stroke:#16a34a
"MLP compartida" significa que la misma MLP corre en cada punto de forma independiente. Implementada como una convolución 1x1 sobre la dimensión de puntos por eficiencia.
Campos de Radiancia Neuronal (NeRFs)
Los NeRFs (Mildenhall et al., 2020) tomaron la pregunta "¿podemos reconstruir una escena 3D a partir de N fotos?" y respondieron con una red neuronal que es la escena. La red mapea (x, y, z, viewing_direction) a (density, colour). Renderizar una nueva vista es un bucle de lanzamiento de rayos sobre esta red.
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
Una pérdida compara el píxel renderizado con el píxel ground-truth en las fotos de entrenamiento. El backprop a través del paso de renderizado actualiza la MLP. Sin ground truth 3D, sin geometría explícita — la escena se almacena en los pesos de la MLP.
Codificación posicional en NeRF
Una MLP común sobre (x, y, z) no puede representar detalles de alta frecuencia porque las MLPs están espectralmente sesgadas hacia las bajas frecuencias. NeRF lo arregla codificando cada coordenada en un vector de características de Fourier antes de la MLP:
gamma(p) = (sin(2^0 pi p), cos(2^0 pi p), sin(2^1 pi p), cos(2^1 pi p), ...)
Hasta L=10 niveles de frecuencia. Este es el mismo truco que los transformers usan para las posiciones, y reaparece en el condicionamiento temporal de difusión (Lección 10). Sin él, los NeRFs se ven borrosos.
Renderizado volumétrico
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 es la transmitancia — cuánta luz sobrevive hasta el punto i. (1 - exp(-sigma_i * delta_i)) es la opacidad en el punto i. c_i es el color. El píxel final es una suma ponderada a lo largo del rayo.
Qué reemplazó a los NeRFs
Los NeRFs puros son lentos de entrenar (horas) y lentos de renderizar (segundos por imagen). El linaje desde entonces:
- Instant-NGP (2022) — la codificación por grilla de hash reemplaza la entrada de posición de la MLP; entrena en segundos.
- Mip-NeRF 360 — maneja escenas sin límites y antialiasing.
- 3D Gaussian Splatting (2023) — reemplaza el campo volumétrico con millones de gaussianas 3D; entrena en minutos, renderiza en tiempo real. El estándar de producción actual.
Casi todo producto NeRF real en 2026 es en realidad 3D Gaussian splatting. El modelo mental sigue siendo el NeRF.
Conjuntos de datos y benchmarks
- ShapeNet — clasificación y segmentación de modelos CAD 3D como nubes de puntos.
- ScanNet — escaneos interiores reales para segmentación.
- KITTI — nubes de puntos LIDAR exteriores para conducción autónoma.
- NeRF Synthetic / Blended MVS — conjuntos de imágenes con pose para síntesis de vistas.
- Mip-NeRF 360 dataset — escenas reales sin límites.
Constrúyelo
Paso 1: Clasificador 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()):,}")
Alrededor de 1,6M parámetros. Corre sobre 1.024 puntos por nube.
Paso 2: Codificación 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 da frecuencias progresivamente más altas.
Paso 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}")
Diminuto comparado con el NeRF original (que tiene 2 troncos de MLP de profundidad 8). Suficiente para demostrar la arquitectura.
Paso 4: Renderizado volumétrico a lo largo de un rayo
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}")
Un rayo, 64 muestras, compuestas en un solo píxel RGB y una profundidad.
Úsalo
Para trabajo real:
nerfstudio(Tancik et al.) — la biblioteca de referencia actual para NeRF / Instant-NGP / Gaussian Splatting. Línea de comandos más un visor web.pytorch3d(Meta) — renderizado diferenciable, utilidades de nube de puntos, operaciones de malla.open3d— procesamiento de nube de puntos, registro, visualización.
Para despliegue, el 3D Gaussian splatting reemplazó en gran medida a los NeRFs puros porque renderiza 100x más rápido. La calidad de la reconstrucción es comparable.
Entrégalo
Esta lección produce:
outputs/prompt-3d-task-router.md— un prompt que enruta a la representación 3D correcta (nube de puntos, malla, vóxel, NeRF, Gaussian splat) según la tarea y los datos de entrada.outputs/skill-point-cloud-loader.md— una skill que escribe unDatasetde PyTorch para archivos .ply / .pcd / .xyz con normalización, centrado y muestreo de puntos correctos.
Ejercicios
- (Fácil) Muestra que PointNet es invariante a permutaciones: pasa la misma nube dos veces, una vez con los puntos barajados. Verifica que las salidas sean idénticas salvo por el ruido de punto flotante.
- (Medio) Implementa una función mínima de generación de rayos que, dadas las intrínsecas y la pose de la cámara, produzca orígenes y direcciones de rayos para cada píxel de una imagen H x W.
- (Difícil) Entrena un TinyNeRF sobre un conjunto de datos sintético de vistas renderizadas de un cubo de colores (generado vía renderizado diferenciable o un ray tracer simple). Reporta la pérdida de renderizado en las épocas 1, 10 y 100. ¿En qué época el modelo produce vistas reconocibles?
Términos Clave
| Término | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| Nube de puntos | "Puntos 3D del LIDAR" | Conjunto no ordenado de (x, y, z) + características opcionales por punto |
| PointNet | "Primera red neuronal en nubes de puntos" | MLP compartida por punto + pool simétrico (max); invariante a permutaciones por construcción |
| NeRF | "MLP que es la escena" | Red que mapea (x, y, z, dir) a (densidad, color); renderizada por lanzamiento de rayos |
| Codificación posicional | "Características de Fourier" | Codifica cada coordenada en sin/cos en múltiples frecuencias para superar el sesgo de baja frecuencia de la MLP |
| Renderizado volumétrico | "Integración de rayo" | Compone muestras a lo largo de un rayo en un solo píxel usando transmitancia y alfa |
| Instant-NGP | "NeRF de grilla de hash" | Reemplaza la MLP de coordenadas del NeRF con una grilla de hash multirresolución; 100-1000x más rápido |
| 3D Gaussian splatting | "Millones de gaussianas" | Escena = colección de gaussianas 3D; renderiza en tiempo real, entrena en minutos |
| SDF | "Campo de distancia con signo" | Función que devuelve la distancia con signo a la superficie más cercana; otra representación implícita |
Lectura Adicional
- PointNet (Qi et al., 2017) — el clasificador invariante a permutaciones
- NeRF (Mildenhall et al., 2020) — el artículo que convirtió la reconstrucción 3D a partir de fotos en un problema de red neuronal
- Instant-NGP (Müller et al., 2022) — grillas de hash, aceleración de 1000x
- 3D Gaussian Splatting (Kerbl et al., 2023) — la arquitectura que reemplazó a los NeRFs en producción