Faza 04 · lecția 13

Viziune 3D — nori de puncte și NeRF-uri

Scopul lecției: Viziunea 3D are două forme principale. Norii de puncte sunt ieșirea brută a senzorului. NeRF-urile sunt câmpul volumetric învățat. Ambele răspund la întrebarea „ce se află unde în spațiu”.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Viziune computerizată
Lectură
15 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Norii de puncte
  5. Arhitectura PointNet
  6. Câmpuri neuronale de radianță (NeRF-uri)
  7. Codarea pozițională în NeRF
  8. Redare volumetrică
  9. Ce a înlocuit NeRF-urile
  10. Seturi de date și repere de evaluare
  11. Construiți
  12. Pasul 1: Clasificator PointNet
  13. Pasul 2: Codarea pozițională
  14. Pasul 3: MLP NeRF mic
  15. Pasul 4: Redare volumetrică de-a lungul unei raze
  16. Utilizați
  17. Livrați
  18. Exerciții
  19. Termeni-cheie
  20. Lecturi suplimentare

Viziunea 3D are două forme principale. Norii de puncte sunt ieșirea brută a senzorului. NeRF-urile sunt câmpul volumetric învățat. Ambele răspund la întrebarea „ce se află unde în spațiu”.

Tip: Învățați + construiți Limbaje: Python Cerințe prealabile: Faza 4 Lecția 03 (CNN-uri), Faza 1 Lecția 12 (Operații pe tensori) Timp: ~45 de minute

Obiective de învățare

  • Deosebiți reprezentările 3D explicite (nor de puncte, plasă, voxel) de cele implicite (câmp de distanță cu semn, NeRF) și momentele în care este folosită fiecare
  • Înțelegeți trucul funcției simetrice din PointNet, care face o rețea neuronală invariantă la permutări pe un set neordonat de puncte
  • Urmăriți o trecere înainte NeRF: lansarea razelor, redarea volumetrică, codarea pozițională și capul MLP pentru densitate și culoare
  • Utilizați nerfstudio sau instant-ngp pentru reconstrucție 3D preantrenată dintr-un număr mic de imagini cu poziții cunoscute

Problema

O cameră produce o imagine 2D. Un LiDAR produce un set de puncte 3D fără ordine. Un flux de procesare structure-from-motion produce un nor rar de puncte-cheie 3D. Un NeRF reconstruiește o întreagă scenă 3D dintr-un număr mic de imagini cu poziții cunoscute. Toate acestea sunt „viziune”, însă niciuna nu seamănă cu tensorul dens dorit de un CNN.

Viziunea 3D contează deoarece aproape orice sarcină robotică cu valoare mare rulează în 3D: apucare, evitare a obstacolelor, navigație, ocluzie AR și capturare de conținut 3D. Un inginer de viziune care înțelege numai imaginile 2D nu poate participa la segmentul cu cea mai rapidă creștere din domeniu: conținut AR/VR, robotică, sisteme de conducere autonomă și reconstrucție 3D pe bază de NeRF pentru imobiliare sau construcții.

Cele două reprezentări domină din motive diferite. Norii de puncte sunt ceea ce oferă senzorii fără procesare suplimentară. NeRF-urile și succesoarele lor (3D Gaussian splatting, SDF-uri neuronale) sunt ceea ce obțineți când cereți unei rețele neuronale să învețe o scenă.

Conceptul

Norii de puncte

Un nor de puncte este un set neordonat de N puncte din R^3, fiecare putând avea opțional caracteristici precum culoarea, intensitatea sau normala.

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

Fără grilă, fără conectivitate. Două proprietăți îngreunează folosirea lor în rețele neuronale:

  • Invarianța la permutare — ieșirea nu trebuie să depindă de ordinea punctelor.
  • N variabil — un singur model trebuie să gestioneze nori de dimensiuni diferite.

PointNet (Qi et al., 2017) le-a rezolvat pe amândouă cu o singură idee: aplicați un MLP partajat fiecărui punct, apoi agregați cu o funcție simetrică (max pooling). Rezultatul este un vector cu dimensiune fixă, care nu depinde de ordine.

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

Acesta este întregul nucleu al PointNet. Variantele mai profunde (PointNet++, Point Transformer) adaugă eșantionare ierarhică și agregare locală, dar trucul funcției simetrice rămâne neschimbat.

Notă tehnică a traducerii: PointNet++ aplică PointNet recursiv pe partiții imbricate pentru a învăța caracteristici locale la mai multe scări. Point Transformer folosește straturi de autoatenție pentru nori de puncte. Prin urmare, nu este corect ca agregarea locală a acestor variante să fie descrisă drept același truc de max-pooling „neschimbat”.

Arhitectura PointNet

Диаграмма к уроку «Viziune 3D — nori de puncte și NeRF-uri»

„MLP partajat” înseamnă că același MLP rulează independent pe fiecare punct. Pentru eficiență, este implementat ca o convoluție 1x1 pe dimensiunea punctelor.

Câmpuri neuronale de radianță (NeRF-uri)

NeRF-urile (Mildenhall et al., 2020) au luat întrebarea „putem reconstrui o scenă 3D din N fotografii?” și au răspuns cu o rețea neuronală care este scena. Rețeaua mapează (x, y, z, viewing_direction) la (density, colour). Redarea unei vederi noi este o buclă de lansare a razelor prin această rețea.

MLP NeRF:  (x, y, z, theta, phi) -> (sigma, r, g, b)

Pentru a reda pixelul (u, v) al unei vederi noi:
  1. Lansați o rază din cameră prin pixelul (u, v)
  2. Eșantionați puncte de-a lungul razei la distanțele t_1, t_2, ..., t_N
  3. Interogați MLP-ul în fiecare punct
  4. Compuneți culorile ponderate cu (1 - exp(-sigma * dt))
  5. Suma este culoarea pixelului redat

O funcție de pierdere compară pixelul redat cu pixelul de adevăr de referință din fotografiile de antrenare. Retropropagarea prin pasul de redare actualizează MLP-ul. Fără adevăr de referință 3D, fără geometrie explicită — scena este stocată în ponderile MLP-ului.

Notă tehnică a traducerii: NeRF-ul clasic se antrenează din imagini cu poziții de cameră cunoscute; în practică, parametrii intrinseci și pozițiile extrinseci trebuie calibrați sau estimați, de obicei prin structure-from-motion. În arhitectura originală, densitatea depinde numai de poziția spațială, iar radianța sau culoarea dependentă de vedere depinde de poziție și de direcția de observare; notația compactă din lecție ascunde această distincție.

Codarea pozițională în NeRF

Un MLP obișnuit pe (x, y, z) nu poate reprezenta detalii de frecvență înaltă deoarece MLP-urile au o părtinire spectrală către frecvențe joase. NeRF rezolvă aceasta prin codarea fiecărei coordonate într-un vector de caracteristici Fourier înainte de MLP:

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

Până la L=10 niveluri de frecvență. Este același truc folosit de Transformere pentru poziții și apare din nou în condiționarea temporală a difuziei (Lecția 10). Fără el, NeRF-urile arată neclar.

Notă tehnică a traducerii: Afirmația că un MLP obișnuit „nu poate reprezenta” frecvențe înalte este prea absolută: în configurațiile studiate, un MLP standard are o părtinire spectrală și le învață dificil, iar caracteristicile Fourier schimbă problema de învățare. Codarea NeRF și codarea pozițională sinusoidală din Transformer folosesc motivul sin/cos, dar au parametrizări și roluri diferite: prima codează coordonate spațiale continue, iar a doua poziții de tokenuri într-o secvență. Condiționarea temporală din difuzie este un tipar înrudit, nu aceeași reprezentare exactă.

Redare volumetrică

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 este transmisia — câtă lumină supraviețuiește până la punctul i. (1 - exp(-sigma_i * delta_i)) este opacitatea în punctul i. c_i este culoarea. Pixelul final este o sumă ponderată de-a lungul razei.

Ce a înlocuit NeRF-urile

NeRF-urile pure se antrenează lent (ore) și se redau lent (secunde per imagine). Evoluția ulterioară:

  • Instant-NGP (2022) — codarea cu grilă hash înlocuiește intrarea pozițională a MLP-ului; se antrenează în secunde.
  • Mip-NeRF 360 — gestionează scene neîncadrate și anti-aliasing.
  • 3D Gaussian Splatting (2023) — înlocuiește câmpul volumetric cu milioane de gaussiene 3D; se antrenează în minute și redă în timp real. Reprezintă opțiunea implicită curentă pentru producție.

Aproape fiecare produs NeRF real din 2026 este, de fapt, 3D Gaussian splatting. Modelul mental rămâne totuși NeRF-ul.

Notă tehnică a traducerii: Afirmațiile despre o opțiune implicită universală pentru producție, viteze de 100x și calitate comparabilă nu sunt garantate. Performanța, fidelitatea și alegerea reprezentării depind de scenă, captură, hardware, buget și cerințele aplicației; lucrarea 3D Gaussian Splatting raportează rezultate pentru protocolul său experimental.

Seturi de date și repere de evaluare

  • ShapeNet — clasificarea și segmentarea modelelor CAD 3D ca nori de puncte.
  • ScanNet — scanări interioare reale pentru segmentare.
  • KITTI — nori de puncte LiDAR exteriori pentru conducere autonomă.
  • NeRF Synthetic / Blended MVS — seturi de date cu imagini poziționate pentru sinteza de vederi.
  • Setul de date Mip-NeRF 360 — scene reale neîncadrate.

Construiți

Pasul 1: Clasificator 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()):,}")

Aproximativ 1,6M parametri. Rulează pe 1.024 de puncte per nor.

Notă tehnică a traducerii: Pentru num_classes=10, codul de mai sus are exact 807.626 parametri antrenabili, incluzând parametrii afini ai straturilor BatchNorm; afirmația „1,6M” este incorectă. Numărul se schimbă odată cu num_classes, iar buffer-ele BatchNorm nu sunt numărate de parameters().

Pasul 2: Codarea pozițională

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

Înmulțirea cu 2^l * pi produce frecvențe progresiv mai înalte.

Pasul 3: MLP NeRF mic

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

Este mic comparativ cu NeRF-ul original (care are 2 trunchiuri MLP de adâncime 8). Este suficient pentru a demonstra arhitectura.

Notă tehnică a traducerii: NeRF-ul original folosește două rețele de același tip, una grosieră și una fină, pentru eșantionare ierarhică; fiecare are un trunchi MLP de opt straturi pentru poziție, urmat de o ramură condiționată de direcție pentru culoare. Prin urmare, formularea „două trunchiuri MLP cu adâncimea 8” nu descrie cu precizie arhitectura unei singure rețele.

Pasul 4: Redare volumetrică de-a lungul unei raze

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

O rază, 64 de eșantioane, compunere într-un singur pixel RGB și o adâncime.

Utilizați

Pentru lucru real:

  • nerfstudio (Tancik et al.) — biblioteca de referință actuală pentru NeRF / Instant-NGP / Gaussian Splatting. Include linie de comandă și un vizualizator web.
  • pytorch3d (Meta) — redare diferențiabilă, utilitare pentru nori de puncte și operații pe plase.
  • open3d — procesarea, alinierea și vizualizarea norilor de puncte.

Pentru implementare, 3D Gaussian splatting a înlocuit în mare măsură NeRF-urile pure deoarece redă de 100x mai rapid. Calitatea reconstrucției este comparabilă.

Livrați

Această lecție produce:

  • outputs/prompt-3d-task-router.md — un prompt care rutează către reprezentarea 3D corectă (nor de puncte, plasă, voxel, NeRF, gaussian splat) pe baza sarcinii și a datelor de intrare.
  • outputs/skill-point-cloud-loader.md — o aptitudine care scrie un PyTorch Dataset pentru fișiere .ply / .pcd / .xyz cu normalizare, centrare și eșantionare a punctelor corecte.

Exerciții

  1. (Ușor) Arătați că PointNet este invariant la permutări: rulați același nor de două ori, o dată cu punctele amestecate. Verificați că ieșirile sunt identice până la zgomotul de virgulă mobilă.

Notă tehnică a traducerii: Înainte de comparație apelați net.eval(). În modul de antrenare, Dropout eșantionează măști aleatoare, astfel încât două treceri pot produce ieșiri diferite chiar pentru același nor; torch.no_grad() elimină doar urmărirea gradientului și nu schimbă comportamentul Dropout.

  1. (Mediu) Implementați o funcție minimală de generare a razelor care, având parametrii intrinseci și poziția camerei, produce originile și direcțiile razelor pentru fiecare pixel al unei imagini H x W.
  2. (Dificil) Antrenați un TinyNeRF pe un set de date sintetic cu vederi redate ale unui cub colorat (generat prin redare diferențiabilă sau un ray tracer simplu). Raportați pierderea de redare la epocile 1, 10 și 100. La ce epocă produce modelul vederi recognoscibile?

Termeni-cheie

Termen Cum este numit în practică Ce înseamnă de fapt
Nor de puncte „Puncte 3D de la LiDAR” Set neordonat de (x, y, z) + caracteristici opționale per punct
PointNet „Prima rețea neuronală pentru nori de puncte” MLP partajat per punct + max pool simetric; invariantă la permutări prin construcție
NeRF „MLP-ul care este scena” Rețea care mapează (x, y, z, dir) la (densitate, culoare); redată prin lansarea razelor
Codare pozițională „Caracteristici Fourier” Codează fiecare coordonată în sin/cos la frecvențe multiple pentru a depăși părtinirea MLP către frecvențe joase
Redare volumetrică „Integrare de-a lungul razei” Compune eșantioane de-a lungul unei raze într-un singur pixel folosind transmisia și alpha
Instant-NGP „NeRF cu grilă hash” Înlocuiește MLP-ul de coordonate NeRF cu o grilă hash multirezoluție; de 100–1000x mai rapid
3D Gaussian splatting „Milioane de gaussiene” Scena = colecție de gaussiene 3D; redă în timp real și se antrenează în minute
SDF „Câmp de distanță cu semn” Funcție care întoarce distanța cu semn până la cea mai apropiată suprafață; o altă reprezentare implicită

Notă tehnică a traducerii: Instant-NGP nu înlocuiește MLP-ul NeRF cu grila hash; grila hash multirezoluție înlocuiește codarea pozițională clasică, iar un MLP mic rămâne în model. Vitezele, redarea în timp real și duratele de antrenare sunt rezultate dependente de implementare, scenă și hardware, nu proprietăți universale.

Lecturi suplimentare

Sursă: Originalul în limba engleză

Navigare: ← Lecția 04.12 — Înțelegerea videoclipurilor — modelare temporală · Faza 4 — Viziune computerizată · Lecția 04.14 — Transformere vizuale (ViT) → · Catalog complet