Фаза 04 · урок 13

3D-зрение — облака точек и NeRF

Цель урока: Камера создаёт 2D-изображение. LIDAR создаёт набор 3D-точек без порядка. Конвейер structure-from-motion создаёт разреженное облако 3D-ключевых точек. NeRF реконструирует целую 3D-сцену по нескольким изображениям с известными позами. Всё…

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Компьютерное зрение
Чтение
11 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Облака точек
  5. Архитектура PointNet
  6. Поля нейронного излучения (NeRF)
  7. Позиционное кодирование в NeRF
  8. Объёмный рендеринг
  9. Чем заменили NeRF
  10. Наборы данных и бенчмарки
  11. Соберите
  12. Шаг 1: классификатор PointNet
  13. Шаг 2: позиционное кодирование
  14. Шаг 3: небольшой NeRF MLP
  15. Шаг 4: объёмный рендеринг вдоль луча
  16. Используйте
  17. Поставьте
  18. Упражнения
  19. Ключевые термины
  20. Дополнительные материалы

3D-зрение бывает двух видов. Облака точек — необработанный выход датчика. NeRF — обученное объёмное поле. Оба отвечают на вопрос «что и где находится в пространстве».

Тип: Изучите + соберите Языки: Python Предварительные требования: Фаза 4, урок 03 (CNN), фаза 1, урок 12 (Операции с тензорами) Время: ~45 минут

Цели обучения

  • Различать явные (облако точек, сетка, воксель) и неявные (поле знакового расстояния, NeRF) 3D-представления и случаи их применения
  • Понимать приём PointNet с симметричной функцией, который делает нейронную сеть перестановочно-инвариантной относительно неупорядоченного набора точек
  • Прослеживать прямой проход NeRF: трассировка лучей, объёмный рендеринг, позиционное кодирование, MLP-головка плотности и цвета
  • Использовать nerfstudio или instant-ngp для предобученной 3D-реконструкции по небольшому набору изображений с известными позами

Проблема

Камера создаёт 2D-изображение. LIDAR создаёт набор 3D-точек без порядка. Конвейер structure-from-motion создаёт разреженное облако 3D-ключевых точек. NeRF реконструирует целую 3D-сцену по нескольким изображениям с известными позами. Всё это относится к «зрению», но ничто из этого не похоже на плотный тензор, который ожидает CNN.

3D-зрение важно, потому что почти каждая ценная задача робота выполняется в 3D: захват предметов, обход препятствий, навигация, перекрытие объектов в AR, захват 3D-контента. Инженер по компьютерному зрению, который понимает только 2D-изображения, отрезан от наиболее быстро растущей части области (AR/VR-контент, робототехника, стеки автономного вождения, 3D-реконструкция на основе NeRF для недвижимости или строительства).

Два представления доминируют по разным причинам. Облака точек — это то, что датчики дают вам бесплатно. NeRF и их последователи (3D Gaussian splatting, нейронные SDF) — это то, что получается, когда вы просите нейронную сеть изучить сцену.

Концепция

Облака точек

Облако точек — это неупорядоченный набор из N точек в R^3, каждая из которых при необходимости имеет признаки (цвет, интенсивность, нормаль).

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

Нет ни сетки, ни связности. Для нейронных сетей это усложняют два свойства:

  • Инвариантность к перестановкам — выход не должен зависеть от порядка точек.
  • Переменное N — одна модель должна обрабатывать облака разных размеров.

PointNet (Qi et al., 2017) решил обе задачи одной идеей: применить общий MLP к каждой точке, затем агрегировать симметричной функцией (max pool). В результате получается вектор фиксированного размера, не зависящий от порядка.

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

Это вся основа PointNet. Более глубокие варианты (PointNet++, Point Transformer) добавляют иерархическую выборку и локальную агрегацию, но приём с симметричной функцией остаётся неизменным.

Архитектура PointNet

Диаграмма к уроку «3D-зрение — облака точек и NeRF»

«Общий MLP» означает, что один и тот же MLP независимо запускается на каждой точке. Для эффективности он реализуется как свёртка 1x1 по измерению точек.

Поля нейронного излучения (NeRF)

NeRF (Mildenhall et al., 2020) взяли вопрос «можем ли мы реконструировать 3D-сцену по N фотографиям?» и ответили нейронной сетью, которая и является сценой. Сеть отображает (x, y, z, viewing_direction) в (density, colour). Рендеринг нового вида — это цикл трассировки луча по этой сети.

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

Функция потерь сравнивает отрендеренный пиксель с пикселем ground truth на обучающих фотографиях. Обратное распространение через шаг рендеринга обновляет MLP. Нет ни 3D-ground truth, ни явной геометрии — сцена хранится в весах MLP.

Позиционное кодирование в NeRF

Обычный MLP на (x, y, z) не может представлять высокочастотные детали, поскольку 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 частотных уровней. Это тот же приём, который трансформеры используют для позиций, и он снова появляется в условии времени диффузии (урок 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) — hash-grid-кодирование заменяет позиционный вход MLP; обучается за секунды.
  • Mip-NeRF 360 — обрабатывает неограниченные сцены и сглаживание.
  • 3D Gaussian Splatting (2023) — заменяет объёмное поле миллионами 3D-гауссиан; обучается за минуты, рендерит в реальном времени. Текущий стандарт для production.

Почти каждый реальный NeRF-продукт в 2026 году на самом деле использует 3D Gaussian splatting. Но ментальная модель всё ещё остаётся 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) — 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()):,}")

Около 1,6 млн параметров. Работает с 1 024 точками на облако.

Шаг 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"input:  {x.shape}")
print(f"encoded: {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 (у которого есть 2 MLP-ствола глубины 8). Этого достаточно, чтобы продемонстрировать архитектуру.

Шаг 4: объёмный рендеринг вдоль луча

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}")

Один луч, 64 выборки, компонуются в один RGB-пиксель и значение глубины.

Используйте

Для реальной работы:

  • nerfstudio (Tancik et al.) — текущая эталонная библиотека для NeRF / Instant-NGP / Gaussian Splatting. Командная строка плюс web viewer.
  • pytorch3d (Meta) — дифференцируемый рендеринг, утилиты для облаков точек, операции с сетками.
  • open3d — обработка облаков точек, регистрация, визуализация.

Для развёртывания 3D Gaussian splatting в значительной степени заменил чистые NeRF, потому что рендерит в 100 раз быстрее. Качество реконструкции сопоставимо.

Поставьте

Этот урок создаёт:

  • outputs/prompt-3d-task-router.md — промпт, который направляет задачу к правильному 3D-представлению (облако точек, сетка, воксель, NeRF, Gaussian splat) на основе задачи и входных данных.
  • outputs/skill-point-cloud-loader.md — навык, который пишет PyTorch Dataset для файлов .ply / .pcd / .xyz с корректными нормализацией, центрированием и выборкой точек.

Упражнения

  1. (Легко) Покажите, что PointNet инвариантен к перестановкам: дважды пропустите через него одно и то же облако, один раз с перемешанными точками. Убедитесь, что выходы идентичны с точностью до шума плавающей точки.
  2. (Средне) Реализуйте минимальную функцию генерации лучей, которая при заданных внутренних параметрах и позе камеры создаёт начала и направления лучей для каждого пикселя изображения H x W.
  3. (Сложно) Обучите TinyNeRF на синтетическом наборе отрендеренных видов цветного куба (сгенерированном дифференцируемым рендерингом или простым трассировщиком лучей). Сообщите потерю рендеринга на эпохах 1, 10 и 100. На какой эпохе модель создаёт узнаваемые виды?

Ключевые термины

Термин Как обычно говорят Что это на самом деле означает
Облако точек «3D-точки от LIDAR» Неупорядоченный набор (x, y, z) + необязательные признаки для каждой точки
PointNet «Первая нейронная сеть для облаков точек» Общий MLP на точку + симметричный (max) pool; перестановочно-инвариантна по построению
NeRF «MLP, которая является сценой» Сеть, отображающая (x, y, z, dir) в (плотность, цвет); рендерится трассировкой лучей
Позиционное кодирование «Признаки Фурье» Кодирует каждую координату в sin/cos на нескольких частотах, чтобы преодолеть низкочастотное смещение MLP
Объёмный рендеринг «Интегрирование по лучу» Компонуирует выборки вдоль луча в один пиксель с помощью пропускания и alpha
Instant-NGP «Hash-grid NeRF» Заменяет координатный MLP NeRF многомасштабной hash grid; в 100–1000 раз быстрее
3D Gaussian splatting «Миллионы гауссиан» Сцена = набор 3D-гауссиан; рендерится в реальном времени, обучается за минуты
SDF «Поле знакового расстояния» Функция, возвращающая знаковое расстояние до ближайшей поверхности; ещё одно неявное представление

Дополнительные материалы


Источник: 3D Vision — Point Clouds & NeRFs 04.12 — Понимание видео — временное моделирование · Фаза 04 — Компьютерное зрение · 04.14 — Трансформеры для зрения (ViT) · Полный каталог