all lessons / computer_vision_3d / 11 · deep learning on 3D lesson 11 / 13

Deep learning on 3D and scene understanding

Lessons 09–10 fit one scene by optimization — a NeRF or a cloud of Gaussians is tuned until its renderings match a specific set of photos. That machinery reconstructs beautifully, but it does not recognize anything: point a NeRF at a new street and it has learned nothing transferable, no notion of "car" or "pedestrian," no ability to generalize to a scene it never optimized. To learn across scenes — to build a detector or a segmenter — we need networks that consume 3D directly. But the raw 3D from lesson 03, the point cloud, is an unordered, irregular, sparse set, and the grid convolution that powers image networks (CV lesson 06) simply does not apply to it. This lesson builds the 3D backbones that fix that, and the task heads that turn features into boxes, labels, and occupancy.

The plan
Four moves. (1) See why the grid CNN fails on 3D — a convolution needs a regular lattice and a fixed neighbor ordering, and a point cloud has neither — and name the three families that cope: point-based, voxel/sparse, and projection/BEV. (2) Build the point-based line: PointNet's permutation-invariant shared-MLP-then-symmetric-pool trick, and PointNet++'s hierarchical fix for local structure. (3) Build the voxel/sparse line: why a dense 3D CNN is O(N³) (lesson 01's curse) and how sparse convolution escapes it (lesson 01's sparsity), plus the fast projection/BEV shortcut. (4) Assemble the task heads — 3D detection (7-DOF boxes), segmentation, and the occupancy-prediction turn — the direct 3D analog of CV detection (lesson 09) and segmentation. The governing tension throughout: the representation you feed the network sets what convolution — if any — you are allowed to use.

1 · Why grid CNNs fail on 3D, and the three families

The 2D convolution of CV lesson 06 rests on two properties of an image that we never had to think about. First, the pixels sit on a regular grid: every pixel has neighbors at fixed offsets (±1, 0), (0, ±1), and a 3×3 kernel is just "multiply these nine specific neighbors by these nine specific weights." Second, that grid gives a canonical ordering: pixel (i,j) is a well-defined slot, so weight-sharing across positions is meaningful. Translation equivariance — the whole reason CNNs work — is a statement about sliding a kernel across that lattice.

A point cloud (lesson 03) has neither property. It is a set: { p1, …, pn } with pi ∈ ℝ³, and three facts about it break the CNN outright:

There are, broadly, only three ways to get a deep network onto this data, and every 3D architecture is a variation on one of them:

Family A
Point-based
Consume the raw set directly. Design the network to be permutation-invariant by construction. PointNet, PointNet++.
Family B
Voxel / sparse conv
Snap points onto a 3D grid so a convolution can run — then compute only where there is content. Minkowski / submanifold sparse conv.
Family C
Projection / BEV
Flatten 3D to a 2D image (range view or bird's-eye view) and run a fast, ordinary 2D CNN. Fast, loses some geometry.

Read those as three answers to one question — how do I recover a grid and an ordering, or avoid needing them? Family A avoids the grid entirely by engineering invariance into the architecture. Family B rebuilds the grid but refuses to pay for the empty parts. Family C throws away one dimension to reuse the mature 2D machinery wholesale. The rest of the lesson builds each, in that order, then hangs task heads off them.

2 · Point-based — PointNet and PointNet++

Start with Family A, because its core idea is the cleanest and most quoted result in 3D deep learning. We want a function f that eats a set of points and returns a single global feature vector (a descriptor of the whole shape), and the one hard requirement is permutation invariance: reorder the input points and the output must not change, because a set has no order to begin with.

The PointNet trick. How do you build a function of a set that ignores order? Process each point independently with the same function, then combine the results with an operation that is itself order-blind. Concretely, apply a shared MLP h to every point (the same weights for all of them — this is where permutation symmetry enters, because "the same function on each element" cannot depend on which element came first), then aggregate the per-point features with a symmetric function — PointNet uses element-wise max-pooling:

f(\{p1, …, pn\}) = γ\Big( \operatorname*{MAX}i=1..n\; h(pi) \Big) .

Read the pieces. h lifts each 3-vector to a high-dimensional per-point feature (say 1024-D) with a small MLP shared across all points. The element-wise \operatorname{MAX} over the n points collapses them into one 1024-D vector: for each of the 1024 feature channels, keep the largest value across all points. Because \max\{a, b, c\} = \max\{c, a, b\} — max does not care about order — the pooled vector is provably invariant to any permutation of the inputs. Finally γ is another MLP mapping that global descriptor to a class label (or whatever the task needs). Sum or average would work as symmetric functions too; max is used because it tends to latch onto the most salient points and empirically generalizes best.

A beautiful consequence: for each channel, exactly one input point supplied the winning value. Those winners are the critical points — a sparse subset that alone determines the descriptor. Perturb any other point and the output is unchanged; PointNet has, in effect, learned a data-dependent skeleton of the shape. (The widget below makes both the invariance and the critical points visible.)

The T-Net. One more piece for robustness to pose: PointNet prepends a small learned network (the T-Net) that regresses an alignment transform — a 3×3 matrix — and applies it to the input points before the shared MLP, so that clouds arriving in slightly different orientations are canonicalized first. (It is a learned, data-driven cousin of the rigid alignment from lesson 02, though PointNet does not constrain it to SO(3).)

The limitation that forced a sequel
A single global max-pool has no notion of local neighborhoods. Every point talks to the global aggregate and never to its neighbors, so PointNet captures the coarse global shape but misses fine, local geometry — the very structure that distinguishes a chair leg from a table leg. This is the same lesson image CNNs taught in reverse: a network with no local receptive field, no hierarchy, sees only the gist.

PointNet++ — go hierarchical. The fix is to reintroduce locality the way a CNN does — by building a hierarchy of growing receptive fields — but adapted to an unordered, irregular set. One set-abstraction layer does three things:

  1. Sample a subset of centroids with farthest point sampling (FPS): greedily pick the point farthest from all already-picked points, repeating until you have the desired count. FPS spreads centroids evenly over the surface regardless of density — far better coverage than random sampling, which over-picks dense regions.
  2. Group each centroid's local neighborhood by a ball query (all points within radius r) or kNN, producing a small local patch per centroid.
  3. Encode each patch with a mini-PointNet (shared MLP + max-pool over just that patch) into one feature vector attached to the centroid.

The output is a smaller set of points, each now carrying a feature that summarizes its local region. Stack these layers and the receptive field grows: local edges → parts → whole object — precisely the local-to-global feature hierarchy of a CNN (CV lesson 06), just expressed as repeated sample→group→pool on a set instead of stride→convolve→pool on a grid. Multi-scale grouping (several radii per centroid) further hardens it against the varying density that plagues LiDAR.

3 · Voxel-based and sparse convolution

Family B takes the opposite tack: if the trouble is that a point cloud has no grid, give it one. Voxelize — partition space into a 3D lattice and mark each cell by the points that fall in it — and now an ordinary 3D convolution applies, the honest volumetric analog of the 2D CNN. This is conceptually the most direct port of image networks to 3D.

The catch is exactly the curse of the third dimension from lesson 01. A dense grid at resolution N per side has voxels, and a 3D convolution costs O(N³) multiply-adds per channel. A 512³ grid is 134 million cells; a dense 3D CNN over it does not fit on a GPU. And it is almost all waste: the points sit on surfaces, so the vast majority of voxels are empty — you would be convolving over air.

Sparse convolution is the escape, and it is lesson 01's sparsity trick made into an operator. Store and compute only at occupied voxels. A hash map holds the active sites; the convolution gathers each active site's active neighbors, applies the kernel, and scatters the result — touching zero empty cells. Submanifold sparse convolution goes further and keeps the active set fixed (an output site is active only if the corresponding input site was), which prevents the "dilation" that would otherwise fill space with active voxels layer by layer and destroy the sparsity you were exploiting. Libraries like MinkowskiEngine and SpConv make this practical, and it is what makes high-resolution 3D CNNs tractable — the dominant backbone for LiDAR point clouds and dense indoor scans.

Projection / BEV — the fast shortcut. Family C sidesteps 3D convolution altogether. Flatten the cloud to a 2D image and run a mature, fast 2D CNN on it:

Projection trades away some geometry (collapsing a dimension loses fine vertical structure and can merge distinct objects) for a large speed win. Point-voxel hybrids such as PVCNN keep the best of both: a point-based branch preserves fine detail at continuous coordinates while a low-resolution voxel branch does cheap neighborhood aggregation, fused each layer. A compact comparison:

FamilyHandles unordered input?Memory / computeStrength
Point-based (PointNet++)yes — invariant by construction∝ #pointsno quantization; native to raw clouds
Dense voxel + 3D CNNyes — after voxelizationO(N³) — often infeasiblesimplest port of image CNNs
Sparse conv (Minkowski)yes — after voxelization∝ occupied voxelshigh-res 3D; LiDAR & indoor scans
Projection / BEVyes — after projection∝ 2D grid (cheap)fast; deployed in AV stacks
Point-voxel hybrid (PVCNN)yespoint detail + voxel speeddetail without the O(N³) cost

4 · Task heads — the 3D analog of CV detection and segmentation

A backbone produces features; a head turns them into a prediction. The tasks mirror 2D vision (CV lesson 09) one dimension up, with the extra geometry making both the outputs and the pitfalls richer.

3D object detection. Instead of a 2D box (x, y, w, h), predict a 7-DOF oriented box: center (x, y, z), size (l, w, h), and heading \text{yaw} (rotation about the vertical axis — objects on the ground rarely pitch or roll, so one angle suffices). The families of section 3 each spawn a detector:

Camera-only 3D detection. Detecting 3D boxes from images alone (no LiDAR) is attractive — cameras are cheap — but ill-posed, because a single image lost depth (lesson 01's many-to-one projection). The modern recipes recover it:

Metrics. The 2D detection toolkit carries over with a dimension added: 3D IoU (volumetric intersection-over-union of two oriented boxes), 3D NMS to suppress duplicates, and mAP averaged over classes and thresholds. The nuScenes NDS (nuScenes Detection Score) additionally rolls in errors in translation, scale, orientation, velocity, and attribute — because in 3D a box can have the right IoU yet the wrong heading or size, which a pure IoU metric would let slide.

Segmentation. Assign a label per point rather than per box: semantic (road / building / vegetation), instance (this car vs. that car), or panoptic (both at once). Sparse-conv U-Nets dominate indoor and LiDAR semantic segmentation; KPConv defines a genuine convolution directly on points via learnable kernel points in continuous space (no voxel grid), a point-native answer to "what is a convolution on an irregular set?"

Occupancy prediction. A pointed current trend in autonomous driving: instead of emitting boxes, predict a dense voxel grid of occupancy + semantics for the whole scene — "is this cell occupied, and by what?" The motivation is a failure mode of boxes. A 7-DOF box assumes every obstacle is a known, roughly box-shaped category; but the road throws up open-set, oddly-shaped hazards — a fallen ladder, debris, an overhanging branch, a strangely articulated vehicle — that no box class fits and that a detector may miss entirely. An occupancy grid is category-agnostic: it can flag "something solid is here, do not drive into it" without ever naming it, which is exactly the robustness a safety-critical stack wants.

The fusion trap — a teaser for lesson 13
Real AV stacks fuse camera + LiDAR, and the moment you do, coordinates and units become a minefield. A LiDAR point is metric (meters) in the sensor's frame; projecting it into an image needs the extrinsic SE(3) from LiDAR→camera (lesson 02) and the camera intrinsics K — get the calibration or the frame convention wrong (the Rᵀt inverse bug from lesson 02) and your points land in the wrong pixels. Worse, the sensors fire at different rates, so a moving car sits at different places in the LiDAR sweep and the camera frame unless you time-sync and motion-compensate. Fusing well is largely a discipline of coordinate frames, calibration, and timestamps — the subject of lesson 13.

Widget — permutation invariance: why PointNet max-pools

Below are eight fixed 2D points, each labeled by its current position in the input list. For every point we compute a simple per-point feature h(pi) = [\,x,\ y,\ x·x,\ \sin(3y)\,] — the stand-in for PointNet's shared MLP. From those we build two global descriptors: the PointNet one, an element-wise \operatorname{MAX} over all points (a symmetric aggregation), and a naive one that weights each point by its list index, Σi\, i·h(pi) (order-dependent by design). Hit shuffle to permute the list order and relabel the points. Watch: the max-pooled vector is unchanged — it is a function of the set — while the index-weighted vector changes every time. The starred points are the critical points: for each feature channel, the one point that supplied the winning max.

Permutation invariance: max-pool is a set function, index-weighting is not
Coordinates are mapped to [−1, 1] for the features. The MAX descriptor (labeled invariant) depends only on which points are present, not their order, so shuffling leaves it identical — the whole reason PointNet pools with max. The index-weighted descriptor (labeled order-dependent) reshuffles with the list. A star marks each channel's critical point (the argmax); those alone fix the invariant descriptor.
MAX-pool descriptor
label
invariant
Index-weighted Σ i·h
label
order-dependent
MAX changed on shuffle?
Naive changed on shuffle?
Show the core JS
// per-point feature: h(p) = [x, y, x*x, sin(3y)]  (x,y normalised to [-1,1])
function h(p){ return [p.x, p.y, p.x*p.x, Math.sin(3*p.y)]; }

// (1) PointNet-style: element-wise MAX over all points — a SYMMETRIC aggregation.
//     max{a,b,c} = max of the SET, independent of order → permutation-invariant.
var mx = [-Infinity,-Infinity,-Infinity,-Infinity], argmax = [0,0,0,0];
for (var i=0;i<pts.length;i++){
  var f = h(pts[i]);
  for (var d=0;d<4;d++) if (f[d] > mx[d]){ mx[d]=f[d]; argmax[d]=i; }  // track critical point
}

// (2) NAIVE order-dependent: weight each point by its LIST INDEX, then sum.
//     depends on the order the points arrive in → NOT a set function.
var naive = [0,0,0,0];
for (var j=0;j<pts.length;j++){
  var g = h(pts[j]);
  for (var d2=0;d2<4;d2++) naive[d2] += j * g[d2];   // the index j is the whole problem
}

// shuffle = permute the list order (Fisher–Yates). mx is UNCHANGED (max ignores order);
// naive CHANGES because the index weights land on different points.

Where this points next

We can now run networks on 3D — invariant to the unordered set (PointNet++), tractable on high-resolution volumes (sparse conv), fast on flattened views (BEV) — and read out boxes, labels, and occupancy that generalize across scenes. But look back at camera-only detection: every one of those methods (LSS, BEVFormer, pseudo-LiDAR) leaned on a depth it had to predict from images. We invoked it as a black box. Lesson 12 opens it: learned depth — how a network estimates a depth map from a single image (monocular, resolving the scale ambiguity with data priors) or from several (deep multi-view stereo, which learns the matching that classical MVS did by hand). That is the bridge from "3D from sensors" to "3D from ordinary pixels," and it feeds directly back into the pseudo-LiDAR and BEV detectors we just built.

Takeaway
Lessons 09–10 optimize one scene; to learn across scenes you need networks on 3D directly, but a point cloud is an unordered, irregular, sparse set so the grid CNN doesn't apply. Three families cope: point-based (PointNet — permutation invariance via a shared MLP h then a symmetric \operatorname{MAX} pool, so f=γ(\operatorname{MAX}_i\, h(p_i)) is provably order-independent; PointNet++ adds local structure via FPS-sample → ball-group → mini-PointNet hierarchy); voxel/sparse (a dense 3D CNN is O(N³) — lesson 01's curse — so sparse/submanifold convolution computes only at occupied voxels, lesson 01's sparsity escape, dominant for LiDAR and indoor scans); and projection/BEV (flatten to a range or bird's-eye image and run a fast 2D CNN). Task heads are the 3D analog of 2D vision: 7-DOF detection (PointPillars, CenterPoint on LiDAR; LSS/BEVFormer/pseudo-LiDAR from cameras), scored by 3D IoU/mAP and nuScenes NDS; point segmentation (sparse-conv U-Nets, KPConv); and category-agnostic occupancy prediction for open-set obstacles a box would miss. Governing rule: the representation you feed the net decides which convolution — if any — you may use.

Interview prompts