Charles R. Qi, Hao Su, Kaichun Mo, Leonidas J. Guibas (Stanford) — arXiv:1612.00593 · and Qi, Yi, Su, Guibas — arXiv:1706.02413

PointNet: Learning on Unordered Sets

A lidar sweep is not a picture. It is a set — shuffle the rows and nothing physical changed, but every convolutional network you own just saw a different object. Two papers fix this with an idea that fits in one line of code.

Prerequisites: matrix–vector multiply + what ReLU does. Permutation invariance, symmetric functions, farthest point sampling and hierarchical grouping are all derived from zero.
10
Chapters
4
Interactive Sims
89.2%
ModelNet40 (2016)
3.5M
Parameters

Chapter 0: A Set, Not a Grid

You are standing next to a delivery robot in a loading bay. On its roof a lidar spins ten times a second, and each revolution throws back about a hundred thousand returns: distance and angle, converted to (x, y, z). Your job is to answer one question from that data — is the thing four metres ahead a person, a pallet, or a doorframe?

You have spent five years building convolutional networks. So you do the obvious thing. You take one sweep, keep the 1024 returns that fall inside the box of interest, stack them into an array, and feed it to a network.

X ∈ R1024 × 3  —  1024 rows, each a point, each row three floats

The network trains. It even works, sort of, on the data you recorded on Tuesday. Then on Wednesday the driver's firmware update changes the order in which returns are flushed from the buffer — not the returns themselves, just the order — and accuracy falls off a cliff.

Nothing about the loading bay changed. Nothing about the geometry changed. The same 1024 points came back, in a different sequence, and the model no longer recognises the scene.

Here is the whole problem in one sentence. An image is a function on a grid: pixel (i, j) has a canonical address, and if you permute the pixels you destroy the picture. A point cloud is a set: the points have no addresses, only positions, and if you permute the rows you have destroyed exactly nothing. Your array has an order. The world it describes does not. Every layer you write must be blind to the difference — and a matrix multiply is not blind to it by default.

Why the flattened MLP fails, mechanically

Let us make the failure concrete rather than philosophical, because the mechanics matter for everything that follows.

Suppose you flatten the array to a vector of length 3N and put a fully connected layer on it. The first layer is a matrix W with 3N columns. Point i contributes to the pre-activation through exactly three columns: 3i, 3i+1, 3i+2. Those columns hold their own weights, learned independently of every other point's columns.

Take a two-point cloud and one output neuron, so the arithmetic fits on a line. Let the weights on the four relevant slots (two points, two coordinates each) be:

w = [ 1.0 , −2.0  |  0.5 , 3.0 ]    first pair is point 1, second pair is point 2

Feed the cloud { (2, 1), (0, 4) } in that order. The pre-activation is

z = 1.0(2) + (−2.0)(1) + 0.5(0) + 3.0(4) = 2 − 2 + 0 + 12 = 12

Now feed the identical set in the other order, { (0, 4), (2, 1) }:

z = 1.0(0) + (−2.0)(4) + 0.5(2) + 3.0(1) = 0 − 8 + 1 + 3 = −4

Twelve versus minus four, from the same two points. The network is not confused, it is not undertrained, it has not seen too little data — it is computing a different function of the same set, because the function it computes is a function of the array, and the array carries an order the set does not have.

What is actually in that tensor

Before we design a layer, it is worth being concrete about what a row of the array physically is, because the abstraction "a point in R3" hides several decisions.

A spinning lidar fires a laser at a known azimuth and elevation, waits, and measures the time until the reflection returns. Multiply by the speed of light, divide by two, and you have a range. Convert the (range, azimuth, elevation) triple to Cartesian and you have your (x, y, z). Alongside it the sensor usually gives you three more things, and whether you feed them to the network is a modelling choice:

ChannelWhat it isShould the network see it?
x, y, zCartesian position in the sensor frameAlways. This is the geometry.
intensityReturned energy — a function of surface material, angle and rangeOften. It separates road paint from asphalt. It is also sensor-specific and does not transfer.
ring / beam indexWhich of the 32 or 64 lasers produced this returnCareful. It is very predictive and it is a fact about the sensor, not the world. Models lean on it and then fail on a different sensor.
timestampWhen within the sweep this return happenedNeeded for motion compensation on a moving platform; usually consumed before the network.

PointNet uses (x, y, z) and nothing else for object classification, and the papers are explicit that "additional dimensions may be added by computing normals and other local or global features". That is worth noting because it is where PointNet++ gets its extra 1.2 points on ModelNet40 — from surface normals appended to each row, taking the input from n×3 to n×6. For the scene-segmentation experiments the row grows to nine numbers: XYZ, RGB, and the point's normalised position within the room.

The other thing hidden by "a point in R3" is that these points are samples of a surface, not a filling of a volume. That distinction will produce a piece of arithmetic in Chapter 5 that explains one of PointNet++'s central design decisions, so file it now: a point cloud of an object is a 2D thing living in a 3D coordinate system, and its statistics behave accordingly.

How many orders are there?

An honest engineer's next thought is: fine, augment. Feed every ordering during training and the network will learn to ignore order.

Count first. A cloud of N points has N! orderings. For the modest N = 1024 that PointNet actually uses on ModelNet40:

log10(1024!) ≈ 2639.7  →  1024! ≈ 102640

There are roughly 1080 atoms in the observable universe. You are not going to sample 102640 orderings, and you are not going to sample a meaningful fraction of them, and no schedule of data augmentation covers a set that large. Augmentation is not a fix; it is a hope.

The invariance you need is a hard constraint, not a soft preference. Anything you can express as "the network should learn to ignore X" you can usually express better as "the network is structurally incapable of seeing X". Convolutional weight sharing is exactly this move for translation. PointNet is exactly this move for permutation. The reason it works is that it removes the degree of freedom rather than penalising it.

Two escapes the field took first, and their bills

By late 2016 the standard answer to "deep learning on 3D data" was to turn the point set back into something a CNN already understood. Two ways, both expensive.

Escape 1: voxelize. Overlay a regular 3D grid and mark each cell occupied or empty. Now you have a tensor and you can run 3D convolutions. This was VoxNet and 3DShapeNets territory. The bill is arithmetic:

32 × 32 × 32 = 32,768 cells  ·  1024 points fill at most 1024 of them

So at least 96.9% of your tensor is the number zero, and you are paying for every one of those zeros in memory and in FLOPs. And 323 is coarse: normalise a 3-metre doorframe into that grid and each cell is roughly 9 cm on a side, which is the difference between a hand and a fist. Push to 2563 to get 1 cm cells and you are storing 16,777,216 cells for the same 1024 points — a 99.994% empty tensor, and 3D convolution cost grows with the cube of resolution.

Escape 2: render it. Put virtual cameras around the object, render images, run a 2D CNN on each, pool across views. This is MVCNN, and it was genuinely the strongest 3D classifier of its day — 90.1% average-class accuracy on ModelNet40, using 80 rendered views. It costs 60.0M parameters and 62,057M FLOPs per sample. And it fundamentally cannot label individual points: a render is a projection, and a projection has thrown away the correspondence you need for segmentation.

RepresentationWhat the CNN seesParamsFLOPs / sampleWhat it costs you
Voxel grid (Subvolume)Dense 3D tensor, mostly zeros16.6M3,633MQuantization; cubic scaling; sparsity wasted
Multi-view (MVCNN)80 rendered 2D images60.0M62,057MCannot segment points; render pipeline; view choice
Raw points (PointNet)The set itself3.5M440M— you have to invent the layer

Those are the paper's own Table 6 numbers. PointNet is 141× cheaper than MVCNN and 8× cheaper than the volumetric network in FLOPs, and 17× smaller than MVCNN in parameters — while matching the volumetric network's 89.2% overall accuracy. The cost of the detour through a grid was not a rounding error. It was the entire budget.

Two more escapes, and the paper's arguments against them

Voxels and renders are the escapes the field took. There are two more that any engineer proposes within five minutes, and the paper dismantles both. They are worth working through because the reasoning sharpens what "invariance" means.

Escape 3: sort the points into a canonical order. If the problem is that the array has an arbitrary order, impose a non-arbitrary one — sort by x, then y, then z. Now the same set always produces the same array, and your MLP is legal.

This fails, and the failure is not a corner case. Sorting requires a map from R3 to a position on a 1D line. Nudge a point by a micrometre and it can jump past its neighbour, so its index changes by one, so every subsequent point's index shifts and the flattened vector is rewritten wholesale. A tiny change in the input produces an enormous change in the representation.

The paper's version of this argument is a proof by contradiction, and it generalises past coordinate sorting: if a stable ordering existed, it would define a bijection from a high-dimensional space to the real line that preserves spatial proximity as the dimension collapses — and no such map exists in general. Two points near each other in R3 must sometimes land far apart on the line, because the line simply does not have enough room. Empirically, the paper's Figure 5 confirms it: an MLP on sorted points performs poorly, "though slightly better than directly processing an unsorted input."

Escape 4: treat the set as a sequence and use an RNN. Feed the points one at a time to an LSTM, train with randomly permuted sequences, and let the network learn to be order-blind.

This is more defensible and still loses, for two reasons the paper names. First, it does not have the invariance, it approximates it, and Vinyals et al.'s Order Matters showed that for sequence models "order does matter and cannot be totally omitted" — the approximation is systematically imperfect. Second, scale: an RNN is "relatively good" at ignoring order for sequences of a few dozen elements, and point clouds have thousands. You are asking a recurrent state to remember an unordered set of 1024 items and produce the same summary for every one of 102640 presentations. It also costs O(n) sequential steps, which destroys the parallelism that makes the per-point approach fast.

Notice the pattern in all four escapes. Voxels and renders change the data to fit the model. Sorting and RNNs keep the data and hope the model copes. PointNet does neither — it changes the function class so that the symmetry is an identity rather than a target. That is nearly always the strongest available move, and the reason it is available here is that permutation invariance has a clean algebraic characterisation. Not every symmetry does.

The three properties any point-set layer must respect

The paper states them plainly in Section 4.1, and it is worth holding all three in your head, because PointNet solves the first cleanly, half-solves the third, and openly ignores the second — which is the entire reason PointNet++ exists.

Property 1 — Unordered
A network consuming N points must be invariant to all N! feeding orders. PointNet: solved exactly, by construction.
Property 2 — Interaction among points
The points live in a metric space. Neighbours form meaningful subsets; local structure matters. PointNet: ignored. This is the flaw of Chapter 4.
Property 3 — Invariance under transformation
Rotating and translating the whole cloud should not change the label. PointNet: partially, via a learned alignment module. See Chapter 3 for how partial.

What we are about to build

Everything in this lesson descends from a single design decision that the authors compress into one equation. It is worth reading now, before it means anything, so that you can watch it acquire meaning:

f( { x1, …, xn } ) ≈ γ ( MAXi=1..n { h(xi) } )

h is a small network applied to each point on its own. MAX is elementwise maximum across all the points. γ is another small network applied to the result. That is PointNet. The rest of the architecture — batch norm, the two alignment modules, the segmentation branch — is scaffolding around those three symbols.

Chapter 1 derives why that particular shape is forced on you. Chapter 2 shows the strange and useful consequence: the embedding of a shape depends on a sparse skeleton of at most K points, which is simultaneously why the model is robust to missing data and why it cannot see fine detail. Chapters 4 through 6 are the sequel, in which the same authors admit the flaw and rebuild the model as a hierarchy.

The claim, stated now so you can hold it to account. A per-point network followed by a single max-pool is not a heuristic that happens to be order-blind. It is a universal approximator of continuous set functions (Theorem 1), it provably depends on at most K points where K is the pooled dimension (Theorem 2), and on ModelNet40 in 2016 it matched the best volumetric network at 89.2% overall accuracy with 3.5M parameters and 440M FLOPs — on raw points, with no grid anywhere in the pipeline.
Why is training-time permutation augmentation not a real solution to the ordering problem?

Chapter 1: Symmetry by Max

We need a function of a set. Let us derive its shape rather than guess it.

Write the thing we want as f({x1, …, xn}), and demand that swapping any two arguments leaves the value alone. A function with that property has a name in mathematics: it is symmetric. You already know several. Sum is symmetric: 2 + 5 + 9 does not care about order. Product is symmetric. Minimum and maximum are symmetric. Average is symmetric.

So one legal design is: apply some symmetric function to the raw points and be done. But max over raw coordinates of a chair gives you the bounding-box corner, which is a terrible chair descriptor. Sum over raw coordinates gives you n times the centroid, which is barely better. Raw symmetric functions throw away everything.

The move: transform first, then symmetrise

Here is the idea, and it is the only genuinely creative step in the paper. Do not apply the symmetric function to the coordinates. Apply it to learned features of the coordinates.

f( { x1, …, xn } ) ≈ γ ( g ( h(x1), …, h(xn) ) )

where h: RN → RK is applied to every point independently, with shared weights, and g is symmetric. Now check the invariance, because it should be immediate and it is:

Permute the input. Each xi still gets the same hh has no idea which slot a point arrived in, because it only ever sees one point. So the multiset {h(x1), …, h(xn)} is unchanged; only its listing order changed. And g is symmetric, so it ignores listing order. Therefore f is unchanged. There is no training involved in that guarantee. It is algebra.

Note carefully what shared weights buy here. In a CNN, weight sharing across spatial positions buys translation equivariance. In PointNet, weight sharing across points buys permutation equivariance for the per-point stage — permute the input, and the stack of features permutes identically. Equivariance plus a symmetric pool equals invariance. That two-step (equivariant layers, then an invariant readout) is the same skeleton you will find in graph neural networks, DeepSets, and transformers without positional encoding.

Which symmetric function? The paper actually measured

Sum, average, max and an attention-weighted sum are all symmetric and all legal. The paper ran the control experiment on ModelNet40 and found max pooling wins "by a large winning margin" over average pooling and over an attention-based weighted sum of the style used in Order Matters. Two intuitions for why:

Max does not dilute. If one point in ten thousand is the tip of an aeroplane's nose, average pooling divides its evidence by ten thousand. Max pooling passes it through untouched. Detection of a rare local pattern survives; under averaging it drowns.

Max is stable under resampling. Sample the same surface twice with different random seeds and the average changes with the sample density; the maximum barely moves, because it is a property of the surface's extremes under h, not of how many samples you drew.

The cost of max — and it is a real cost that Chapter 2 is entirely about — is that the gradient flows to exactly one point per output dimension. Everything else gets zero. We will make that concrete in a moment.

A tiny PointNet, worked entirely by hand

Everything above is easier to believe once you have pushed numbers through it. Let us build the smallest possible PointNet and run it.

Drop to two dimensions so the arithmetic fits on a page — nothing in the argument depends on the dimension. Our "cloud" is four points:

A = (0.2, 0.9)   B = (0.8, 0.1)   C = (0.5, 0.5)   D = (0.1, 0.2)

Our per-point network h is a single linear layer with a ReLU, mapping R2 → R3. In real PointNet this is a five-layer MLP with widths 64, 64, 64, 128, 1024; here it is one layer with width 3, so that K = 3 and we can write everything down.

h(x) = ReLU(Wx + b),   W = [[2, −1], [−1, 2], [1, 1]],   b = (0, 0, −0.6)

Run point A = (0.2, 0.9) through it, one row at a time:

row 1: 2(0.2) + (−1)(0.9) + 0 = 0.4 − 0.9 = −0.5  → ReLU → 0
row 2: (−1)(0.2) + 2(0.9) + 0 = −0.2 + 1.8 = 1.6  → 1.6
row 3: 1(0.2) + 1(0.9) − 0.6 = 1.1 − 0.6 = 0.5  → 0.5

So h(A) = (0, 1.6, 0.5). Doing the same for the other three:

PointPre-activation Wx + bh(x) after ReLU
A = (0.2, 0.9)(−0.5,  1.6,  0.5)(0,  1.6,  0.5)
B = (0.8, 0.1)( 1.5, −0.6,  0.3)(1.5,  0,  0.3)
C = (0.5, 0.5)( 0.5,  0.5,  0.4)(0.5,  0.5,  0.4)
D = (0.1, 0.2)( 0.0,  0.3, −0.3)(0,  0.3,  0)

Now pool. Take the maximum down each column, across points:

u1 = max(0, 1.5, 0.5, 0) = 1.5  (from B)
u2 = max(1.6, 0, 0.5, 0.3) = 1.6  (from A)
u3 = max(0.5, 0.3, 0.4, 0) = 0.5  (from A)
u = (1.5, 1.6, 0.5)  —  the global shape descriptor

Now do the thing the whole chapter is about. Feed the points in the order D, C, B, A. Every row of the table above is unchanged — h never knew the order. The columns get maximised over the same four numbers in a different sequence, and maximum does not care. u = (1.5, 1.6, 0.5), exactly. Try B, D, A, C. Same. Try all 24 orderings. Same.

Compare that to Chapter 0's flattened MLP, which turned 12 into −4 on a two-point cloud. Same data, same shuffle, and the difference is not that PointNet was trained better. It is that PointNet's function does not have a slot for order to sit in.

See it hold, and see it break

The simulation runs exactly the four points above. Shuffle them and watch the pooled bars sit perfectly still. Then switch the aggregator to concat — which is what a flattened MLP effectively does, reading features slot by slot — and watch the same shuffle wreck it.

Permutation invariance, live

Top row: the four points in their current feeding order. Middle: h(x) for each, three numbers per point — note these move with the point, never with the slot. Bottom: the aggregated descriptor. Under max and sum the descriptor is a set function. Under concat it is not.

aggregator:

Three things worth noticing while you play. First, the middle band never reorders its contents — each point carries its own feature wherever it lands, which is what "shared weights, applied independently" means in pictures. Second, under max the winning point for each output dimension is marked; for these four points, B wins dimension 1 and A wins dimensions 2 and 3, and C and D win nothing at all. Hold that thought — it is Chapter 2. Third, sum is also perfectly invariant; it is not wrong, it just measures worse, for the dilution reason above.

Where the gradient goes

Forward, max pooling is obvious. Backward, it has a property you should know about before you train anything, and the toy makes it visible.

The derivative of max(a1, …, an) with respect to ai is 1 if ai is the maximum and 0 otherwise. So when the loss sends a gradient back through uj, that gradient is delivered entirely to the single point that won dimension j and nowhere else.

In our toy, with K = 3 and four points: B receives the gradient from dimension 1, A receives the gradients from dimensions 2 and 3, and C and D receive exactly zero. On this step they might as well not exist — their coordinates get no update signal at all.

∂uj / ∂hj(xi) = 1 if i = argmax, else 0  →   at most K of the n points get gradient

Two consequences that matter in practice. First, with n = 1024 points and K = 1024 pooled dimensions, at most 1024 of 1024 points could get gradient — but in practice one point wins many dimensions, so the number of points touched per step is far smaller. The learning signal is sparse. Second, this is not a problem the way sparse gradients usually are, because h's weights are shared: every point contributes to the weight gradient through whichever dimensions it won, and across a batch of clouds and thousands of steps, every region of the input space gets visited. Weight sharing rescues what winner-takes-all would otherwise starve.

It also explains why the network converges to the behaviour Chapter 2 describes. If a point only receives gradient when it wins a dimension, the optimiser has a strong incentive to make the winners informative — to place the decision boundaries of h so that the points which win are the ones that distinguish shapes. That is the mechanism behind the "skeleton" visualisation, and it was not designed in.

Concept and realization: PointNet in twenty lines

Everything above is one small module. Here it is with the shapes annotated, so you can see that the invariance argument and the code are the same object.

python# PointNet classification, vanilla (no T-Nets). B = batch, N = points, K = pooled width.
class PointNet(nn.Module):
    def __init__(self, n_classes=40, K=1024):
        # Conv1d with kernel_size=1 IS a shared per-point MLP. The "convolution"
        # never mixes neighbouring columns, because the kernel is one wide — it
        # is just an efficient way to apply the same Linear to every point.
        self.h = nn.Sequential(
            nn.Conv1d(3,   64,  1), nn.BatchNorm1d(64),  nn.ReLU(),
            nn.Conv1d(64,  64,  1), nn.BatchNorm1d(64),  nn.ReLU(),
            nn.Conv1d(64,  64,  1), nn.BatchNorm1d(64),  nn.ReLU(),
            nn.Conv1d(64,  128, 1), nn.BatchNorm1d(128), nn.ReLU(),
            nn.Conv1d(128, K,   1), nn.BatchNorm1d(K),   nn.ReLU())
        self.gamma = nn.Sequential(
            nn.Linear(K, 512), nn.BatchNorm1d(512), nn.ReLU(), nn.Dropout(0.3),
            nn.Linear(512, 256), nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(0.3),
            nn.Linear(256, n_classes))

    def forward(self, x):            # x: (B, 3, N)  — channels first
        f = self.h(x)               # (B, K, N)   every column computed in isolation
        u = f.max(dim=2)[0]         # (B, K)      <-- THE ENTIRE INVARIANCE IS THIS LINE
        return self.gamma(u)        # (B, n_classes)

Three things in that listing are load-bearing and easy to miss.

Conv1d with kernel size 1 is not a convolution in any interesting sense. It applies the same linear map to every column independently — which is exactly "shared MLP applied to each point". It is written as a convolution purely because that is how the framework lays out an efficient batched implementation. If you replace it with a real kernel of width 3, you have silently made the model order-dependent, because column i would then depend on columns i−1 and i+1, which are arbitrary neighbours in the array and not neighbours in space. That is a real bug people ship.

max(dim=2) is the whole paper. Change it to mean(dim=2) and you have average pooling, which is still invariant and measurably worse. Change it to flatten() and you have Chapter 0's broken model. One line separates the three.

BatchNorm1d over a (B, C, N) tensor normalises over batch and points. That is the right choice here and worth understanding: the statistics of channel c are estimated across all points of all clouds in the batch, which is precisely the pooled population that the shared weights operate on. Normalising per-point instead would be incoherent — there is nothing to average over. It does mean the model's behaviour is mildly batch-dependent at training time, which is one more reason inference uses running statistics.

Theorem 1: this is not a restriction

A reasonable objection at this point: yes, it is invariant — but have we given up expressive power to buy that invariance? Surely a function that must be blind to order is a weaker function.

The paper's Theorem 1 says no, in a specific and strong sense. Let X be the sets of exactly n points inside the unit cube [0,1]m, and let f be any set function on X that is continuous with respect to Hausdorff distance — meaning that nudging the points a little nudges f a little. (Hausdorff distance between two sets is, roughly, the worst distance from any point in one set to the nearest point of the other. It is the natural "how different are these two clouds" metric when the clouds have no correspondence.)

∀ ε > 0,  ∃ h and γ :  | f(S) − γ( MAXx∈S { h(x) } ) | < ε   for every S ∈ X

So per-point MLP plus max plus MLP can approximate any continuous set function arbitrarily well, given enough neurons at the pooling layer. The invariance is free: you gave up nothing in exchange for it.

The proof sketch is worth carrying around because it also tells you the worst case. In the limit, the network can simply learn to voxelize: partition the cube into small cells, let dimension j of h be an indicator that fires when a point is in cell j, and the max over points becomes exactly the occupancy grid. So PointNet is at least as expressive as the voxel approach it replaces — that is the floor. The paper's own comment is the interesting part: "in practice, however, the network learns a much smarter strategy to probe the space."

Read the theorem as a statement about K, the pooled width. "Given enough neurons at the max pooling layer" is not a throwaway. If the worst-case strategy is one dimension per voxel, then K is your resolution budget. And the paper measures exactly this: taking K from 64 up to 1024 buys 2–4% accuracy on ModelNet40. It also measures the other axis: accuracy grows with the number of input points but saturates around 1K points, which is why every experiment in both papers uses 1024.

From the toy to the real thing: shapes and parameter counts

Now scale the toy up to the network in Figure 2 of the paper, tracking tensors. Take the classification branch, n = 1024 points, k = 40 classes:

StageTensor shapeWhat happens
input1024 × 3xyz, normalised into the unit sphere
input T-Net3 × 3learned affine align (Chapter 3)
shared MLP(64, 64)1024 × 64same weights on every row; BN + ReLU
feature T-Net64 × 64learned feature-space align (Chapter 3)
shared MLP(64, 128, 1024)1024 × 1024still per-point, still independent
max pool1024the only place points talk to each other
FC(512, 256, 40)40γ, with dropout on the last MLP

Look hard at the max-pool row. Up to that line, the 1024 points have never interacted — each is a 1024-dim feature computed in total ignorance of the others. Then in one operation, all 1,048,576 numbers collapse to 1024. That single line is the entire communication budget of PointNet, and it is why Chapter 4 has to exist.

Count the parameters of the shared MLP by hand; a linear layer ab has ab + b parameters:

3→64: 256 · 64→64: 4,160 · 64→64: 4,160 · 64→128: 8,320 · 128→1024: 132,096
total shared MLP = 148,992
FC head 1024→512: 524,800 · 512→256: 131,328 · 256→40: 10,280 → 666,408
148,992 + 666,408 = 815,400 ≈ 0.8M

That is exactly the "PointNet (vanilla) 0.8M" row of the paper's Table 6, reproduced from first principles. Note where the parameters are: 82% of them are in the classifier head, on a 1024-dim vector. The part that touches the points costs 0.149M. Per-point processing is nearly free; it is the global vector that is expensive. Keep that in mind in Chapter 3 when we meet a module that costs 1.85M on its own.

Why does max pooling beat average pooling as the symmetric aggregator, in the paper's own control experiment?

Chapter 2: The Critical Points

Go back to the toy table. The pooled descriptor was u = (1.5, 1.6, 0.5), and dimension by dimension it came from B, A, A.

Now ask a question that should make you slightly uneasy: what happens if we delete point D?

h(D) = (0, 0.3, 0). It is not the maximum in any column. Remove it and recompute: u1 = max(0, 1.5, 0.5) = 1.5, u2 = max(1.6, 0, 0.5) = 1.6, u3 = max(0.5, 0.3, 0.4) = 0.5. Identical. Not approximately identical — bitwise identical.

Delete C as well. h(C) = (0.5, 0.5, 0.4), also nowhere a maximum. Recompute over {A, B} alone: (1.5, 1.6, 0.5). Identical again.

Half the cloud was decorative. The global descriptor of this four-point shape is entirely determined by two points. Not "mostly determined" — determined, exactly, with the other two contributing literally nothing to the forward pass and receiving literally nothing on the backward pass.

Theorem 2, stated and unpacked

This is not an artefact of a toy. It is the paper's Theorem 2, and it is the most interesting result in either paper.

Define u(S) = MAXx∈S {h(x)}, the pooled vector, and f = γ ∘ u. Then:

(a)  ∀S, ∃ CS, NS ⊆ X  such that  f(T) = f(S)  whenever  CS ⊆ T ⊆ NS
(b)  |CS| ≤ K

In words. There are two special sets attached to every shape.

CS, the critical point set: the points that actually win at least one dimension of the max. Keep these and you keep the descriptor, no matter what else you throw away. In our toy, CS = {A, B}.

NS, the upper-bound shape: every point in space whose feature vector is componentwise no larger than u. Add any of them and the max does not move, because none of them can win anything. Any cloud T sandwiched between the two — contains all of CS, contained in NS — produces exactly the same global feature.

And part (b) is the sharp bit: CS has at most K elements, where K is the pooled width. One dimension of the max can only be won by one point. With K = 1024, a 100,000-point lidar sweep is summarised by at most 1024 points. Usually far fewer, because one point tends to win many dimensions at once.

Computing NS for the toy, by hand

The upper-bound shape sounds abstract until you write it down. For our h it is three inequalities. A point (x, y) fails to change u if and only if each of its three pre-activations is at most the corresponding uj — note ReLU(z) ≤ u is the same as z ≤ u whenever u ≥ 0, which it is:

2x − y ≤ 1.5     −x + 2y ≤ 1.6     x + y − 0.6 ≤ 0.5 (i.e. x + y ≤ 1.1)

Three half-planes. Their intersection is NS — a convex polygon containing the original four points. Test a candidate outlier E = (0.4, 0.6):

2(0.4) − 0.6 = 0.2 ≤ 1.5 ✓   −0.4 + 1.2 = 0.8 ≤ 1.6 ✓   0.4 + 0.6 = 1.0 ≤ 1.1 ✓

All three hold, so E is inside NS. Add it to the cloud and the descriptor is unchanged: h(E) = (0.2, 0.8, 0.4), and every entry loses its column. A spurious return — a dust mote, a rain droplet, a multipath ghost — that lands anywhere inside that polygon is invisible to the model.

Now test F = (1.0, 0.0), further out:

2(1.0) − 0.0 = 2.0 > 1.5 ✗  →   u1 becomes 2.0, and the descriptor moves

So outliers are not harmless in general — they are harmless exactly when they fall inside the upper-bound shape, and damaging exactly when they push past one of its faces. That is a precise, testable statement about a model's failure mode, derived from three inequalities, and it is the sort of thing you almost never get from a deep network.

What the robustness numbers actually mean

The paper measures the consequences on ModelNet40, and the numbers are the theorem in experimental clothing.

CorruptionEffect on accuracyWhy, from Theorem 2
Delete 50% of points (farthest-point sampled)−2.4%Deletion only matters if it removes a member of CS, and |CS| ≤ K is a small fraction of 1024
Delete 50% of points (random)−3.8%Random deletion is likelier than FPS to knock out a critical point
20% of points replaced by outliersstill >80% accurateOutliers inside NS are inert; the model was trained with a density channel so it can also learn to discount them
VoxNet, same 50% deletion86.3% → 46.0%An occupancy grid has no critical set — every emptied cell is a changed input feature
PointNet, same 50% deletion−3.7%Same experiment, same split, same 1024 input points

Sit with the VoxNet comparison. Forty points of accuracy versus under four, on identical corruption. That is not a tuning difference. It is the difference between a representation whose output depends on all of the input and one whose output provably depends on at most K of it.

How many critical points, really?

Theorem 2 gives an upper bound of K. The interesting question is what the number actually is, and the answer follows from a simple counting argument you can do yourself.

There are K dimensions and each is won by exactly one point, so the number of wins is exactly K. The number of winners is at most K and equals K only if no point ever wins twice. But points do not win at random — a point that is extremal in one direction is usually extremal in nearby directions too. The wing tip of an aeroplane wins every dimension whose h is sensitive to "far out along this axis", and there are many such dimensions.

So the effective critical count is K divided by the average number of wins per winner, and that divisor grows with how correlated the point functions are. The simulation above makes this visible: at K = 4 you see at most four critical points and usually four; push K to 16 and the count rises but by less than a factor of four, because the same lobe tips keep winning adjacent directions.

This is also why the paper's ablation on K shows diminishing returns rather than a linear improvement: raising K from 64 to 1024 is a 16× increase in probes and buys 2–4% accuracy. Each extra probe overlaps more with existing ones. And it is the mathematical version of the visual intuition — a shape's silhouette is described well by a few dozen well-chosen extremal points, and the hundredth one adds very little.

Gradient sparsity is the same fact from the other side

Chapter 1 established that gradient flows only to the argmax of each dimension. Theorem 2 says the forward pass depends only on those same points. These are one statement, read forwards and backwards.

The practical consequence bites during training, not inference. Early in training, before h has learned anything useful, the winners are essentially arbitrary — whichever points happen to sit far out along the random directions the initialisation produced. Those arbitrary points get all the gradient. As h reshapes, the winners migrate, and the set of points receiving learning signal migrates with them.

This is why the model benefits from the jitter augmentation the paper applies — Gaussian noise at σ = 0.02 on every coordinate, every epoch. Jitter continuously reshuffles which point is marginally extremal in each direction, so the network cannot latch onto specific sampled points and must learn a function of the underlying surface. Without it you would be fitting the sampling, not the shape.

A diagnostic worth stealing. If you are debugging a PointNet-family model, log the size of the critical set per cloud. It costs one argmax you already computed. A collapsing critical set — two or three points winning every dimension — means the point functions have become correlated and the model has effectively lost its resolution, and you will see it well before accuracy tells you. A critical set that saturates at K means every probe is finding a distinct winner, which usually means K is too small for the shape complexity.

The visualisation the paper is famous for

When the authors rendered CS for real shapes, the critical points did not look random. They looked like the skeleton of the object: wing tips and tail fins on an aeroplane, chair legs and the seat's four corners, the rim and handle of a mug. The network had, without being asked, learned to summarise a shape by its extremal and distinctive structure — which is what a human sketching the object from memory would also do.

Two consequences the paper draws out. First, CS and NS generalise: rendered for objects from categories never seen in training — a teapot, a bunny, a hand, a human body — the critical sets still trace the skeleton, so the per-point functions h learned something about geometry rather than about ModelNet's furniture. Second, because two similar shapes activate overlapping dimensions, you can build a crude shape correspondence by matching points that win the same dimension in both clouds — a free by-product of the architecture.

Critical points and the upper-bound shape

A 2D "shape" of 90 points, and a bank of K point functions hj(x) = ReLU(wj·x + bj). Orange points are critical — each wins at least one dimension of the max. The shaded region is NS, the upper-bound shape: add points anywhere inside it and the descriptor does not move. The readout is the L2 distance between the current descriptor and the original one, so 0.000 means bitwise identical output.

K =

Push the buttons in order and read the number. Deleting half the non-critical points leaves the descriptor at distance 0.000. Adding a hundred points inside the shaded polygon leaves it at 0.000. One point past the boundary and it jumps. Then raise K from 4 to 16 and watch the critical set grow and the shaded region tighten — more probes means a finer summary means less slack for corruption to hide in, which is Theorem 2(b) and the "K = 64 to 1024 buys 2–4%" measurement, both visible at once.

Reading the bottleneck ablation properly

The paper reports that raising the pooled width K from 64 to 1024 buys 2–4% accuracy, and that accuracy grows with the number of input points but saturates around 1K. Those two curves say something specific when you put them next to Theorem 2.

K is the number of probes. It is your descriptive vocabulary — how many independent questions the network is allowed to ask about the shape before the answers get concatenated into a vector. Doubling K doubles the questions and, because the questions overlap, delivers less than double the information. Hence the diminishing curve.

n, the number of input points, is the resolution of the surface being asked about. And here is why it saturates: once n is large enough that every one of the K probes has a well-determined winner — that is, once the surface is sampled finely enough that adding more points does not change any extremum — extra points contribute nothing at all. They are all non-critical by construction. The paper measures the saturation point at roughly 1000 for ModelNet40 objects, and the number is not universal: it depends on K and on how convoluted the shapes are.

A budget rule this suggests. If your accuracy improves when you raise K, your descriptor is vocabulary-limited — buy more probes. If it improves when you raise n, your input is resolution-limited — sample more points. If neither helps, you have hit the ceiling of what a global summary can express, and the fix is architectural, not a hyperparameter. That third case is precisely where PointNet stopped and PointNet++ started.

The by-product nobody planned: shape correspondence

Here is a consequence of Theorem 2 that the paper puts in the supplementary and that deserves better placement.

Take two chairs. Each has a critical set, and each critical point is associated with the specific dimensions of the max that it won. Now match points across the two shapes by pairing those that activate the same dimensions. Because dimension j corresponds to one learned point function hj — "the thing that responds to this kind of local extremity" — the point that wins dimension j on chair one and the point that wins dimension j on chair two are, by construction, playing the same structural role.

The paper's Figures 13 and 14 show this producing sensible correspondences between pairs of chairs and pairs of tables: back-left leg to back-left leg, seat corner to seat corner. Nothing in the training objective asked for correspondence. It falls out because the max's argmax is an index, and indices are comparable across clouds.

This matters beyond the pretty picture. Correspondence is the input to registration, to shape morphing, to transferring annotations from one model to another. Getting a rough version of it for free, out of a classifier, is the kind of thing that tells you the representation is doing something structurally right rather than fitting a benchmark.

The same result is also the flaw

Everything above is a virtue when you are worrying about missing returns and sensor noise. Turn it around and it is the central weakness.

If a shape is summarised by at most K extremal points, then two shapes that agree on those points are indistinguishable, no matter how they differ elsewhere. Fine texture, small parts, the difference between a smooth surface and a lightly ridged one — all of it lives in the non-critical points, and the non-critical points do not reach the descriptor. The paper is candid about the symptom on ModelNet40: the gap to MVCNN "is due to the loss of fine geometry details that can be captured by rendered images."

And there is a second, deeper version of the problem, which Chapter 4 is about. Because pooling is global, the descriptor knows that a wing-tip response fired somewhere in the cloud, but not where, and not what was near it. Nothing in the architecture ever asks "what does the neighbourhood around this point look like?" — and neighbourhoods are where geometry lives.

A lidar sweep of a parked car picks up 200 spurious returns from airborne dust. Under Theorem 2, when are those returns harmless to the global descriptor?

Chapter 3: T-Net — Alignment and Its Limits

Property 3 from Chapter 0 is still unpaid. Rotate the chair thirty degrees about the vertical axis and it is still a chair, but every coordinate in the array has changed, so every pre-activation of h has changed, so the descriptor has changed. Permutation is handled. Rigid motion is not.

The classical fix is canonicalisation: before doing anything, rotate the object into a standard pose. Run PCA on the point coordinates, take the principal axes, rotate so that they align with the coordinate frame. This works until it does not — a symmetric object has ambiguous principal axes, a partial scan has axes determined by which half you happened to see, and a flat object has two nearly equal eigenvalues so the axis choice flips under noise.

Learn the alignment instead

PointNet's answer is a small network that predicts the alignment from the data. The authors call it a T-Net, and they are explicit that they are borrowing the spatial transformer idea from Jaderberg et al. and getting it much cheaper. In images, applying a predicted transform means resampling a grid with interpolation, which introduces aliasing and needs a custom layer. In point clouds, applying a transform is a matrix multiply on the coordinates. No resampling. No alias. No new layer type.

X' = X · T,    X ∈ Rn×3,  T = T-Net(X) ∈ R3×3

And the T-Net itself is a mini-PointNet, which is a pleasing recursion. Its architecture, from the supplementary material: shared MLP with widths 64, 128, 1024 on each point; max pool across points; two fully connected layers of width 512 and 256; a final layer emitting the nine numbers of a 3×3 matrix, initialised to the identity. Batch norm and ReLU everywhere except the last layer.

Initialising the output at the identity matters. At step zero the module is a no-op, so training starts from plain PointNet and the alignment is learned as a correction rather than fought for from noise.

Watch it undo a rotation, numerically

Make the mechanism concrete on two points. Suppose the canonical pose of a small object has points p = (1, 0) and q = (0, 1), and the scan arrives rotated by 30 degrees. The rotation matrix is

R = [[cos30, −sin30], [sin30, cos30]] = [[0.866, −0.500], [0.500, 0.866]]

so the observed points are Rp = (0.866, 0.500) and Rq = (−0.500, 0.866). Any per-point function h now receives entirely different inputs than it did in the canonical pose, and the pooled descriptor moves.

What the T-Net must learn is to output T = R−1 = RT (rotations are orthogonal, so the inverse is the transpose) for this cloud — and something different for every other pose. Apply it:

RT(Rp) = (0.866)(0.866) + (0.500)(0.500) , (−0.500)(0.866) + (0.866)(0.500)
= 0.750 + 0.250 , −0.433 + 0.433 = (1, 0) = p

Canonical pose recovered exactly. Now look at what that requires of the module. The T-Net must produce a different output for every input pose, and the mapping from "cloud as seen" to "the rotation that undoes it" is exactly the pose-estimation problem you were trying to avoid solving. The T-Net does not sidestep it — it learns an approximation of it, and the quality of that approximation is the quality of your rotation invariance.

And here is the recursive charm of it: the T-Net is a mini-PointNet, so it inherits Theorem 2. Its output also depends on at most K critical points of the cloud. The predicted alignment of a chair is being decided by a handful of extremal points — which is defensible (the extremal points do carry most of the pose information) and also fragile in exactly the situation you would predict: when the critical points are near-symmetric and the true pose is ambiguous.

The second T-Net, and why it needs a regulariser

If aligning coordinates helps, why not align features? After the first shared MLP the points carry 64-dim vectors; insert a second T-Net that predicts a 64×64 matrix and multiply. Same idea, one level up.

Except it does not work out of the box, and the reason is dimensional. A 3×3 matrix has 9 free numbers. A 64×64 matrix has 4,096. That is a much larger, much less constrained thing to regress, and the optimisation becomes unstable. So the paper adds a term to the loss that pushes the predicted matrix toward orthogonality:

Lreg = ‖ I − A ATF2

with weight 0.001 alongside the softmax classification loss. The justification is one sentence in the paper and worth expanding: an orthogonal transformation does not lose information. Orthogonal means the rows are unit-length and mutually perpendicular, so the map is a rotation or reflection — it can reorient the feature space but cannot squash it. If the matrix were allowed to be, say, rank 20, the module would silently destroy 44 dimensions of feature before pooling ever saw them.

Work it out on a 2×2. Suppose the mini-network predicts

A = [[1, 0.5], [0, 1]]

Then A AT = [[1² + 0.5²,  1(0) + 0.5(1)], [0(1) + 1(0.5),  0² + 1²]] = [[1.25, 0.5], [0.5, 1]]. Subtract from the identity:

I − A AT = [[−0.25, −0.5], [−0.5, 0]]

The squared Frobenius norm is the sum of squared entries: 0.0625 + 0.25 + 0.25 + 0 = 0.5625. Times the weight 0.001 gives a penalty of 0.00056 added to the loss — small, deliberately, because this is a nudge and not a constraint. Now try a genuine rotation, A = [[cosθ, −sinθ], [sinθ, cosθ]]: A AT = I exactly, for any θ, so Lreg = 0. The penalty is zero on the whole rotation group and grows with shear and scaling. That is precisely the shape you want.

The ablation, and the part everyone skips

Table 5 of the paper is four lines and a lesson.

Transform usedModelNet40 overall accuracyChange vs none
none87.1
input (3×3) only87.9+0.8
feature (64×64) only86.9−0.2
feature (64×64) + regulariser87.4+0.3
both + regulariser89.2+2.1

Line three is the one people skip: the feature transform, added on its own without the orthogonality penalty, makes the model worse. Adding capacity hurt. The regulariser converts it from −0.2 to +0.3 — and only in combination with the input transform does the whole thing produce +2.1.

The general lesson, which outlives PointNet. A learned module that outputs a structured object — a matrix, a permutation, a warp — usually needs a term in the loss that keeps the output on the manifold of sensible such objects. Otherwise the optimiser will happily find a degenerate one that reduces training loss by destroying information. "It has more parameters, so it must be at least as good" is false whenever the new parameters can throw information away.

What the alignment costs

Now count, because this is where the parameter budget actually goes. Recall a linear layer ab costs ab + b.

Input T-Net (3-dim input, 9-dim output): 3→64 is 256; 64→128 is 8,320; 128→1024 is 132,096; then 1024→512 is 524,800; 512→256 is 131,328; 256→9 is 2,313. Total 799,113.

Feature T-Net (64-dim input, 4096-dim output): 64→64 is 4,160; 64→128 is 8,320; 128→1024 is 132,096; 1024→512 is 524,800; 512→256 is 131,328; and the last layer, 256→4096, is 256(4096) + 4096 = 1,052,672 on its own. Total 1,853,376.

0.815M (vanilla) + 0.799M (T-Net 1) + 1.853M (T-Net 2) = 3.468M ≈ 3.5M

Which reproduces the "PointNet 3.5M" row of Table 6 exactly. Read the split again: the two alignment modules are 77% of the entire network, and a single output layer — the one that emits 4,096 numbers — is 30% of the whole model by itself. You are paying 2.65M parameters, more than three times the base network, to buy 2.1 points of accuracy.

That is a defensible trade in a 2016 benchmark paper. It is a much less obvious trade in a robot that has a 2 ms budget per sweep, and it is the first thing most practitioners drop. Table 6 tells you what dropping it buys: vanilla PointNet is 0.8M params and 148M FLOPs against the full model's 3.5M and 440M — a third of the compute for 87.1% instead of 89.2%.

Why identity initialisation, precisely

"Initialise the output at the identity" sounds like a nicety. It is closer to a necessity, and the reason is a chicken-and-egg problem in the gradient.

Suppose the final layer of the T-Net were initialised the usual way, with small random weights around zero. Then at step one the predicted matrix T is approximately the zero matrix, so X·T ≈ 0, so every point in every cloud collapses to the origin. The main network then sees an identical, information-free input for every example, produces an identical output, and the gradient with respect to T carries no signal about which alignment would have been better — because no alignment was tried.

The standard implementation adds a constant identity to the regressed matrix:

python# The T-Net's last layer regresses a *correction*, not the matrix itself.
def forward(self, x):              # x: (B, 3, N)
    f = self.h(x).max(dim=2)[0]     # (B, 1024)  a mini-PointNet — same recipe
    d = self.fc(f).view(-1, 3, 3)     # (B, 3, 3)  initialised near zero
    I = torch.eye(3, device=x.device)
    return d + I                   # (B, 3, 3)  starts AT the identity, learns a delta

Now step one is a no-op, training begins from exactly the vanilla model's behaviour, and every gradient step asks the meaningful question — which small departure from doing nothing reduces the loss? This "regress a residual from the identity" pattern shows up everywhere a network predicts a transformation, and the failure mode it avoids is always the same: a randomly initialised transform destroys the signal it was supposed to reorient.

Note also what the mini-PointNet inside the T-Net implies. The predicted alignment is itself a permutation-invariant function of the cloud — it has to be, or the alignment would depend on the row order and you would have reintroduced the bug you were fixing. Every module in this architecture that touches the whole cloud is either symmetric or broken.

The alternatives, and when each is right

ApproachHow it gets pose invarianceGuaranteeCost
Rotation augmentation onlyShow the model every pose during trainingNone — statisticalFree, but needs more data and capacity
PCA canonicalisationRotate onto principal axes analyticallyExact when eigenvalues are distinctFree; fails on symmetric or flat objects and on partial scans
T-Net (PointNet)Predict an affine matrix from the dataNone — learned approximation2.65M parameters for +2.1%
Local reference framePer-neighbourhood frame from the local covarianceExact given a stable frameCheap; frame flips are the failure mode
Rotation-invariant featuresFeed distances and angles instead of coordinatesExact, by constructionDiscards absolute orientation information you may need
Equivariant networksLayers that commute with rotation by designExact, by constructionRestricted layer types; more expensive per FLOP

Read the guarantee column. The two rows with exact guarantees achieve them the same way PointNet achieves permutation invariance — by removing the degree of freedom from the function class rather than training it away. The T-Net sits in the awkward middle: a learned approximation with a real parameter cost and no theorem. It won its ablation in 2016 and it is the piece of PointNet that aged least well.

The honest limits of learned alignment

T-Net is a good engineering answer, and it is not the same thing as rotation invariance. Four caveats worth carrying:

It is canonicalisation, not equivariance. The network predicts a pose correction and hopes it is right. There is no theorem saying the same shape at two orientations gets mapped to the same canonical frame. In practice it is approximately true for orientations resembling the training distribution and degrades outside it — and PointNet's ModelNet40 augmentation only rotates about the up-axis, so the model is never asked to handle arbitrary SO(3) at training time.

The predicted matrix is affine, not rigid. Nine free numbers can shear and scale, not just rotate. For the 3×3 case the paper accepts this without a regulariser; the module is free to apply a mild affine warp if that happens to reduce loss.

It costs more than the network it corrects. See the arithmetic above.

It does not address Property 2 at all. Aligning the whole cloud does nothing about local structure. You can canonicalise a chair perfectly and the model still has no mechanism for asking what a chair leg looks like up close. Which brings us to the sequel.

Where the field went instead. Later work largely stopped predicting a global pose and started building rotation invariance into the features — using point-pair descriptors (distances and angles, which are rotation invariant by construction), local reference frames per neighbourhood, or steerable and equivariant convolutions. PointNet++'s own move, as we will see, is subtler and cheaper: it subtracts the centroid inside every local group, which buys exact translation invariance per neighbourhood for free and removes much of the pressure the input T-Net was under.
The feature T-Net without its orthogonality regulariser scored 86.9% — worse than using no transform at all (87.1%). What is the mechanism?

Chapter 4: The Flaw, and Set Abstraction

Six months after PointNet, the same four authors published a paper whose second sentence is a critique of their own: "However, by design PointNet does not capture local structures induced by the metric space points live in, limiting its ability to recognize fine-grained patterns and generalizability to complex scenes."

Let us be precise about what is missing, because "no local structure" is a slogan until you can point at the line of code.

Where the locality went

Return to the tensor table of Chapter 1. From the input to the max pool, every point is processed alone. Point 17's 1024-dim feature is a function of point 17's three coordinates and nothing else. Then one max pool collapses everything. So the network has exactly two scales: the individual point, and the entire cloud. There is nothing in between.

Compare a CNN, which is nothing but in-between scales. A 3×3 kernel at layer 1 sees a 3×3 pixel patch. At layer 2, after a stride-2 pool, its 3×3 kernel sees an effective 7×7 of the original. By layer 5 it sees most of the image. That growing receptive field is the whole reason CNNs generalise: low layers learn edges that are reusable everywhere, high layers compose them into objects.

PointNet has a receptive field of one point, then jumps to the whole cloud. Whatever a "chair leg" is — four roughly parallel thin structures with a particular local curvature signature — PointNet has no layer whose input is a chair leg. It has layers whose input is a single point, and one layer whose input is the entire chair. The mid-level vocabulary that makes vision work does not exist.

The practical symptom shows up worst in scenes. On a single ModelNet40 object, normalised into a unit sphere, "global" is a reasonable scale. On a scanned room, the global pool covers a 1.5-metre cube containing a table, part of a chair, some floor and a wall — and one max over that is a very blunt instrument. The paper's Figure 6 is exactly this: PointNet "captures the overall layout of the room correctly but fails to discover the furniture."

The fix, in one sentence

Apply PointNet recursively on a nested partition of the point set.

That is genuinely the whole idea. If PointNet is a good way to summarise a set of points into a vector, then use it to summarise small neighbourhoods into vectors, treat those vectors as new points at their neighbourhood centres, and repeat. You have built a hierarchy with growing receptive fields, out of the module you already have.

The unit of that hierarchy is called a set abstraction level, and it has three layers.

1 — Sampling layer
Choose N' centroids from the N input points, by farthest point sampling. These become the "pixel locations" of the next level.
2 — Grouping layer
Around each centroid, gather the points within radius r by ball query. Output is N' groups of up to K points each.
3 — PointNet layer
Subtract the centroid, run a shared mini-PointNet on each group, max-pool within the group. Each group becomes one feature vector.
input  N × (d + C)  →  output  N' × (d + C')

where d is the coordinate dimension (3) and C, C' are feature widths. The level takes a big set of thin points and returns a smaller set of fat points. Stack three of these and you have PointNet++.

Sampling: why farthest point, not random

Farthest point sampling (FPS) is greedy and one line to state: start from a point, then repeatedly add whichever remaining point is farthest from the set you have already chosen.

xij = argmaxx ∈ S  mint < j  d(x, xit)

Run it by hand. Six points in the plane:

p1=(0,0)  p2=(1,0)  p3=(0,1)  p4=(1,1)  p5=(0.5,0.5)  p6=(0.1,0.05)

Step 1. Seed with p1. Distances from p1: p2 = 1, p3 = 1, p4 = √2 = 1.414, p5 = √0.5 = 0.707, p6 = √(0.01+0.0025) = 0.112. The largest is p4. Pick p4.

Step 2. Recompute each remaining point's distance to the nearest chosen point. p2: min(1, 1) = 1. p3: min(1, 1) = 1. p5: min(0.707, 0.707) = 0.707. p6: min(0.112, 1.309) = 0.112. The largest is 1, a tie between p2 and p3. Pick p2 (ties broken by index).

Step 3. p3: min(1, 1, 1.414) = 1. p5: 0.707. p6: 0.112. Pick p3.

The FPS order is p1, p4, p2, p3, p5, p6 — the four corners first, then the middle, then the near-duplicate last. Now do it randomly and you may well draw p1, p6, p5: two of your three centroids are 0.112 apart and a whole corner of the shape is unrepresented.

The quantity FPS is actually greedily minimising is the covering radius: after m picks, the largest distance from any point to its nearest centroid. In the run above, that number goes 1.414 → 1.0 → 1.0 → 0.707. Every input point is guaranteed to be within the covering radius of some centroid, and that is exactly the guarantee you need before you go asking about neighbourhoods.

The line the paper puts under this is the important one. "In contrast to CNNs that scan the vector space agnostic of data distribution, our sampling strategy generates receptive fields in a data dependent manner." A convolution puts a kernel centre at every pixel whether or not anything is there. FPS puts centroids where the data is, and spreads them as evenly as the data allows. This is the point-cloud analogue of "stride", but it adapts to the surface instead of to the array.

FPS costs O(N N') — each of N' picks scans all N remaining distances. For 1024 → 512 that is about half a million distance comparisons, which is why it is written as a CUDA kernel and why it shows up in profiles. And because the first seed is arbitrary, the whole thing is randomised. Does that matter? The authors measured it: over ten different seeds on ModelNet40, the standard deviation of the global feature norm is 0.021 (a 2.1% deviation) and the standard deviation of test accuracy is 0.0017 — 0.17%. Stable enough to ignore.

Grouping: ball query, and why not kNN

You have centroids. Now decide who belongs to whom. Two obvious options:

k-nearest neighbours: take the K closest points to each centroid. Guarantees a fixed group size. The scale of the group varies with density — in a dense region the 32 nearest points span 2 cm, in a sparse region they span 40 cm.

Ball query: take all points within radius r, capped at K. Guarantees a fixed physical scale. The group size varies with density — and can be zero.

PointNet++ chooses ball query, and the reasoning is exactly about weight sharing. The mini-PointNet applied to every group has shared weights. Shared weights only make sense if the groups are comparable objects. Under ball query, "a group" always means "a 20 cm ball of surface", everywhere in the cloud, so a learned detector for "20 cm of curved edge" is meaningful. Under kNN, "a group" means a different physical thing in every part of the cloud, and the shared detector has no consistent quantity to detect. In the paper's words, ball query "guarantees a fixed region scale thus making local region feature more generalizable across space, which is preferred for tasks requiring local pattern recognition."

They measured it too, on uniformly-sampled ModelNet40 with 1024 points:

Neighbourhood ruleAccuracyNote
kNN, k = 1689.3too tight
kNN, k = 6490.3larger neighbourhood helps
radius r = 0.189.1too small — see Chapter 5
radius r = 0.290.7the default

Two readings. First, on uniform data the gap is small — 90.7 versus 90.3 — and the authors say so honestly, adding "we speculate in very non-uniform point set, kNN based query will result in worse generalization ability." Second, look at the r = 0.1 row. A smaller neighbourhood hurt, which contradicts the CNN folklore that smaller kernels are better. Chapter 5 explains why, with arithmetic.

Note also the cap. Ball query returns a variable number of points, but GPUs want rectangular tensors, so implementations pad or subsample to a fixed K per group. This is harmless in a way that is worth appreciating: the mini-PointNet ends in a max pool over the group, and duplicating a point in a max pool changes nothing.

The PointNet layer, and the one line that does the real work

Each group arrives as a tensor of shape N' × K × (d + C). Before anything else, the coordinates are re-expressed relative to the centroid:

xi(j) ← xi(j) − x̂(j)   for i = 1..K, j = 1..d

This tiny line is doing three jobs at once, and it is worth naming all three.

It makes the group translation invariant, exactly. Move the whole cloud a metre to the left and every centroid moves with it, so every relative coordinate is unchanged, so every group feature is unchanged. No learned module, no regulariser, no approximation — it is a subtraction. Compare the T-Net of Chapter 3, which spends 0.8M parameters to approximate a global alignment.

It makes weight sharing meaningful. Every group now lives in its own little box centred at the origin with radius r. A shared MLP sees comparable inputs everywhere in the cloud, which is the whole point of sharing.

It encodes the geometry rather than the position. "Point at (3.4, 0.2, 1.1)" is a fact about where the sensor was. "Point at (+0.03, −0.01, +0.02) from my centroid, alongside 31 others" is a fact about shape. The second one generalises.

Then a shared mini-PointNet — MLP on each of the K points, max pool over the K — turns each group into one C'-dim vector, and the level outputs N' × (d + C'): the centroid coordinates, in the original frame, plus the new feature.

Ball query in practice: the cap, the padding, and a bias nobody mentions

Ball query returns "all points within r". GPUs want rectangles. The reconciliation has three details that will bite you if you do not know them.

The cap. Implementations set an upper limit K on group size — typically 32 or 64. Once K neighbours are found, the search stops. In a dense region the ball genuinely contains more than K points and you keep a subset.

The padding. If the ball contains fewer than K points, the remaining slots are filled by repeating the first found neighbour (in the reference implementation, index 0 — usually the centroid itself). This looks like a hack and is almost harmless, for a specific reason: the group ends in a max pool, and duplicating an entry in a max pool changes nothing. Had the aggregator been a mean, padding would have systematically biased every sparse group toward its first neighbour, and the whole density story of Chapter 5 would be worse than it is. The choice of max in 2016 quietly paid off in 2017.

The bias. The reference kernel takes the first K points it encounters within the radius, in input array order — not the K nearest. So in a dense region, which subset of the ball you keep depends on the ordering of the input array. That is a mild permutation dependence smuggled back in through the implementation, and it is the reason two runs on the same cloud with rows shuffled can differ by a hair. It does not matter much in practice — the group is max-pooled and the kept subset is a random sample of a well-populated ball — but it is worth knowing that the model's exact invariance stops at the layer boundary.

Concept and realization: one set abstraction level

python# One set abstraction level: sample -> group -> mini-PointNet.
# xyz:   (B, N, 3)        coordinates in the ORIGINAL frame
# feat:  (B, N, C)        per-point features from the previous level (or None)
def set_abstraction(xyz, feat, n_centroids, radius, k_cap, mlp):
    idx      = farthest_point_sample(xyz, n_centroids)   # (B, N')   O(N*N') greedy
    new_xyz  = gather(xyz, idx)                          # (B, N', 3)

    grp_idx  = ball_query(radius, k_cap, xyz, new_xyz)   # (B, N', K) padded with idx 0
    grp_xyz  = gather(xyz, grp_idx)                      # (B, N', K, 3)

    # THE line: express each neighbour relative to its centroid.
    # Exact translation invariance, zero parameters, one subtraction.
    grp_xyz  = grp_xyz - new_xyz.unsqueeze(2)            # (B, N', K, 3)

    if feat is not None:                              # carry features up the hierarchy
        grp  = cat([grp_xyz, gather(feat, grp_idx)], dim=-1)  # (B, N', K, 3+C)
    else:
        grp  = grp_xyz                                   # (B, N', K, 3)

    grp      = mlp(grp)                                  # (B, N', K, C')  shared over N' AND K
    new_feat = grp.max(dim=2)[0]                       # (B, N', C')    a PointNet per group
    return new_xyz, new_feat                             # (B, N', 3), (B, N', C')

Read the last three lines and compare them with the PointNet listing in Chapter 1. mlp then max(dim=2) — the pooling axis is now the within-group axis K instead of the whole-cloud axis N, and that is the entire difference. PointNet++ is PointNet with the max moved from global to local, wrapped in a sampler and a grouper, and repeated.

Note also which coordinates are returned. new_xyz is in the original frame, unshifted — the centroid subtraction applies only to the grouped neighbours, inside the level. Each level therefore hands the next one a legitimate, smaller point cloud, in world coordinates, with fatter features. That is what makes the recursion type-check.

And note the memory. At level 1 with N' = 512, K = 32, C' = 128, the intermediate grouped tensor for a batch of 16 is 16 × 512 × 32 × 128 = 33.5 million activations before the pool collapses it to 16 × 512 × 128 = 1.05 million. The grouping step inflates the tensor by a factor of K and then throws it away. That factor of 32 is where PointNet++'s memory goes, and it is why the sensible knob when you run out of memory is K, not the MLP width.

The actual classification hierarchy, with arithmetic

Here is the network from the supplementary material, in the paper's own notation, where SA(N', r, [widths]) is a set abstraction level:

SA(512, 0.2, [64, 64, 128]) → SA(128, 0.4, [128, 128, 256]) → SA([256, 512, 1024])
→ FC(512, 0.5) → FC(256, 0.5) → FC(K)

Walk the shapes for a 1024-point cloud, and keep an eye on the physical scale, since the cloud is normalised into a unit ball:

StageTensorPointsRadiusEffective receptive field
input1024 × 31024a single point
SA 1 grouping512 × K × 3512 groups0.20.2 — a small surface patch
SA 1 output512 × (3 + 128)512each "point" now summarises a 0.2 ball
SA 2 grouping128 × K × (3 + 128)128 groups0.40.4 + 0.2 = 0.6 — radii compose
SA 2 output128 × (3 + 256)128a part, not a patch
SA 3 (global)10241the whole shape — this is plain PointNet
FC head40classification

Notice the last set abstraction level has no radius and no centroid count. SA([256, 512, 1024]) is a global set abstraction — it takes all 128 remaining points as one group and pools them to a single vector. Which is to say: PointNet++ ends in PointNet. The whole architecture is two levels of local abstraction feeding the original 2016 model.

And notice the receptive-field column. A level-2 group has radius 0.4 in the coordinates of level-1 centroids, but each of those already summarises a ball of radius 0.2. So the level-2 feature depends on original points up to roughly 0.6 away — radii compose exactly the way strided convolutions compose. The 1024→512→128→1 point counts are the analogue of pooling strides, and 0.2→0.4 is the analogue of a growing kernel.

The recursion, stated once more. PointNet is a function from a set of points to a vector. PointNet++ observes that a vector attached to a location is a point, in the extended sense of "coordinates plus features". So the output of a PointNet layer can be the input of another one. Every level of the hierarchy is the same module, applied to progressively coarser, progressively richer points — which is exactly what a CNN does, with FPS replacing stride and ball query replacing the kernel footprint.
FPS and ball query, step by step

A deliberately non-uniform 2D cloud — dense on the left, sparse on the right, exactly the perspective effect a real scanner produces. Step FPS one centroid at a time and watch the covering radius fall. Then compare it with random sampling at the same budget, and sweep the ball radius to see group occupancy change.

sampler:
ball radius r = 0.09

Three things to watch. The covering radius readout drops fast under FPS and erratically under random sampling — that is the coverage guarantee, visible. The empty groups counter is what happens when the radius is smaller than the local spacing; those centroids produce a feature computed from nothing but padding. And the group size spread readout shows the density problem directly: at a single radius, groups on the dense side hit the cap while groups on the sparse side hold two or three points. That spread is what Chapter 5 exists to fix.

Why does the PointNet layer subtract the centroid coordinate from every point in a group before running the shared MLP?

Chapter 5: Density, and Multi-Scale Grouping

Every experiment so far has quietly assumed something false: that points are spread evenly over the surface. On a CAD model sampled uniformly by face area, they are. On a real scan, they are emphatically not.

The paper opens with a photograph of a Structure Sensor scan for exactly this reason. Density in a real capture varies because of perspective (a surface at 2 m gets far more returns per square centimetre than the same surface at 20 m), incidence angle (a wall face-on catches a dense grid; the same wall at a glancing angle catches a smear), radial pattern (a spinning lidar has fixed angular spacing, so vertical point spacing grows linearly with range), and motion (a moving platform stretches and compresses the sweep).

The trap this sets is subtle and expensive. Train on uniformly sampled CAD models, learn beautiful fine-grained local detectors at radius 0.1, deploy on a scan whose far half is ten times sparser, and those detectors receive neighbourhoods with one or two points in them. The features they produce are not degraded, they are meaningless — and nothing in the loss ever warned you, because the training distribution never contained a sparse region.

How many points are in a ball? Compute it.

This is the arithmetic that makes the whole chapter click, and it is two lines.

Points sampled from a shape do not fill a volume; they lie on a surface. So the expected number in a ball of radius r scales with the ball's cross-sectional area, not its volume. For a cloud of N points spread evenly over a surface of total area A, and a small patch that is locally flat:

E[ count in ball of radius r ] ≈ N · πr2 / A

Take the paper's own normalisation: the cloud is scaled into a unit sphere. Use the unit sphere's own surface, A = 4π, as a stand-in. The π cancels beautifully:

E[count] ≈ N · πr2 / (4π) = N r2 / 4

Now plug in N = 1024, the standard input size, and the exact radii PointNet++ uses:

radius rN = 1024 (training density)N = 256 (test-time dropout)verdict at N = 256
0.11024(0.01)/4 = 2.56256(0.01)/4 = 0.64usually empty
0.21024(0.04)/4 = 10.24256(0.04)/4 = 2.56too few to see a shape
0.41024(0.16)/4 = 40.96256(0.16)/4 = 10.24still informative
0.81024(0.64)/4 = 163.84256(0.64)/4 = 40.96coarse but robust

Read the first row twice. At training density, a radius-0.1 ball holds about two and a half points — already marginal. Drop the cloud to a quarter of its density and the same ball holds less than one point on average. A "local pattern detector" whose input is, most of the time, nothing at all.

This is the mechanism behind the r = 0.1 → 89.1% versus r = 0.2 → 90.7% row of Chapter 4's table, and it is why the paper writes that its experiments "give counter evidence" to the CNN rule that smaller kernels are better. In an image, a 3×3 kernel always sees exactly nine values. In a point cloud, a small radius sees however many points happened to land there — and the answer is often zero.

The asymmetry to internalise. Kernel size in a CNN is a pure modelling choice; it never interacts with the data's density, because the data has no density — the grid is the density. Radius in a point network is a modelling choice entangled with the sensor. Choose it too small and the layer starves; too large and it blurs. And the right answer is different in different parts of the same sweep.

Multi-scale grouping: refuse to choose

If no single radius is right everywhere, do not pick one. At each centroid, run the grouping and the mini-PointNet at several radii in parallel, and concatenate the results.

Here is the actual MSG classification network from the supplementary:

SA(512, [0.1, 0.2, 0.4], [[32,32,64], [64,64,128], [64,96,128]])
→ SA(128, [0.2, 0.4, 0.8], [[64,64,128], [128,128,256], [128,128,256]])
→ SA([256, 512, 1024]) → FC(512, 0.5) → FC(256, 0.5) → FC(K)

Level 1 groups the same 512 centroids three times, at radii 0.1, 0.2 and 0.4, with three separate mini-PointNets producing 64, 128 and 128 channels. Concatenated, each centroid carries 64 + 128 + 128 = 320 features instead of the single-scale version's 128. Level 2 does the same at 0.2 / 0.4 / 0.8, producing 128 + 256 + 256 = 640.

Note the width allocation: the smallest radius gets the fewest channels (64) and the larger radii get more. That is a sensible prior — a two-point neighbourhood cannot support 128 independent descriptors, and the authors are not pretending otherwise.

Why the small radius gets the fewest channels. This looks like a detail and it is a statement about information. A radius-0.1 ball at training density holds about 2.56 points; after centroid subtraction those points span 3 × 2.56 ≈ 8 numbers of geometric content. Asking a network to extract 128 independent descriptors from roughly eight numbers is asking for 120 dimensions of noise. Sixty-four is still generous. Meanwhile the radius-0.4 ball holds about 41 points, or 123 numbers, and can genuinely support a wider description. The channel allocation tracks the information available at each scale, and if you change the radii you should change the widths with them.

What MSG costs, counted. The expensive object in a set abstraction level is the grouped tensor, of size N' × K × C. Single-scale grouping at level 1 builds one such tensor. MSG builds three, and the largest radius has the most neighbours to gather. Using the occupancy formula, the three balls hold about 2.56, 10.24 and 40.96 points, so if the cap K allows it the gathered volume is roughly 21× the smallest branch alone and about 5× the single-scale r = 0.2 branch. The measured cost is a clean 2× overall (163.2 ms versus 82.4 ms), because the caps truncate the large balls and because the levels above are unchanged. Still: MSG is not free, and it is most expensive exactly where you have the most centroids — the first level.

Random input dropout: teaching the model that density varies

Concatenating scales is only half the job. If you train exclusively on 1024 uniformly sampled points, the network never encounters a situation where the small radius is useless, so it never learns to discount it. At test time on a sparse scan it will happily trust a feature computed from noise.

So the paper adds an augmentation with a slightly unusual shape. For each training cloud, draw a dropout ratio

θ ~ Uniform[0, p],   p = 0.95

then drop each point independently with probability θ. Two sources of variation, deliberately: θ varies the sparsity from cloud to cloud, and the per-point coin flip varies the uniformity within a cloud. The cap at 0.95 rather than 1.0 exists only to avoid producing an empty set. At test time nothing is dropped.

The effect is that the network sees, across a training run, clouds ranging from 1024 points down to about 50, and it must produce a good answer for all of them from the same weights. The only way to do that is to learn a combination rule — effectively, to weight the small-radius branch down when it looks starved and up when it looks rich. The paper calls this learning "to adaptively weight patterns detected at different scales."

This is the cleanest example in either paper of an augmentation that is not about generalisation in the usual sense. Random input dropout is not there to prevent overfitting. It is there to create the phenomenon the architecture was designed to handle. Without it, MSG has three branches and no reason to prefer any of them; with it, the branches acquire a division of labour. Architecture and augmentation are one design, not two.

The result: MSG+DP loses less than 1% accuracy going from 1024 test points down to 256. Ablated single-scale grouping (SSG) "fails to generalize to sparse sampling density", and SSG+DP fixes most of it. Interestingly, plain PointNet is also fairly robust here — because its only scale is global, and a global summary degrades gracefully — but it starts from a lower ceiling.

The density regimes you actually meet

The occupancy formula is more useful once you attach real situations to its regimes. Here is the same arithmetic, translated.

RegimeWhere it comes fromOccupancy at r = 0.2What breaks, and what to do
Uniform, denseCAD models sampled by face area; close-range structured light≈ 10Nothing. This is the regime both papers train in, and SSG is enough.
Smooth gradientPerspective — near surfaces dense, far surfaces sparse40 near, 2 farSmall-radius features are fine near and meaningless far. MSG, or an adaptive radius keyed to range.
AnisotropicSpinning lidar — dense along a ring, sparse between ringsvaries with directionA ball is the wrong neighbourhood shape. Ellipsoidal or cylindrical queries, or explicit ring-aware grouping.
HolesSpecular surfaces, black paint, glass, self-occlusion0 over a regionGroups are empty, not sparse. Coarser scales are the only source of signal — exactly what MRG's second vector provides.
DuplicatedAccumulating multiple sweeps into a map without voxel downsamplinginflated, redundantMax pooling absorbs it harmlessly; a mean-based aggregator would be biased. FPS also degrades — it wastes centroids on the overlapping region.

The third row is the one production systems trip over. Both papers' experiments use object scans and room scans with roughly isotropic local sampling; a spinning lidar does not have that property at all. Points along a laser ring are 2–5 cm apart at 20 m while adjacent rings are 30 cm apart, so a spherical ball query at any radius either grabs a one-dimensional string of points from a single ring or reaches across three rings and includes a great deal of range. Neither is the neighbourhood you wanted. This anisotropy, more than anything in the papers, is why outdoor-lidar perception drifted toward sparse voxel convolutions, where the neighbourhood is defined by the grid rather than by a metric ball.

What multi-scale grouping does not fix

MSG makes the model robust to density variation. It does not create information that the sensor did not capture. Three limits worth stating plainly:

It cannot resolve detail below the sampling. If the far half of your scan has 2 cm point spacing, no grouping rule recovers a 5 mm feature there. MSG's honest contribution is to degrade to a coarser description rather than produce a confident wrong one.

It does not adapt the radii to the data. The radii are hyperparameters, fixed at 0.1 / 0.2 / 0.4 for every centroid in every cloud. A genuinely adaptive scheme would pick a radius per centroid from the local spacing — and then lose the fixed-physical-scale guarantee that motivated ball query over kNN in the first place. The tension is real and PointNet++ resolves it by refusing to choose, at 2× the cost.

It doubles the compute for a benefit that is zero on uniform data. If your deployment genuinely has uniform density — some industrial inspection setups do — MSG buys you nothing and costs you 80 ms per batch. Measure your density distribution before you pay for this.

Multi-resolution grouping: the cheaper cousin

MSG is expensive, and the paper says why in a sentence worth quoting: it "runs local PointNet at large scale neighborhoods for every centroid point", and at the lowest level the centroid count is highest. Running a radius-0.4 PointNet at all 512 level-1 centroids is a lot of gathered points.

Multi-resolution grouping (MRG) gets a similar effect for less. The feature of a region at level Li is the concatenation of two vectors:

Vector 1 — from below
Summarise the features of the sub-regions at level Li−1 using a set abstraction. Fine-grained, recursive, and unreliable when the sub-regions were themselves starved.
concatenate ↓
Vector 2 — from raw points
Run a single PointNet directly on all raw points in the region. Coarser, but it never went through a sparse intermediate level, so it degrades gracefully.

When the region is dense, vector 1 carries real detail and should dominate. When it is sparse, vector 1 was computed from sub-regions containing almost nothing and vector 2 should dominate. The network learns that weighting. And the saving is real: no large-radius grouping at the lowest, most populous level.

ModelSize (MB)Forward pass (ms)Comment
PointNet (vanilla)9.411.6fastest by a wide margin — one global pool
PointNet (full, with T-Nets)4025.3the alignment modules are not free
PointNet++ SSG8.782.4smallest model; sampling and grouping dominate the time
PointNet++ MSG12163.22× SSG — the price of three radii
PointNet++ MRG2487.0nearly SSG speed, most of MSG's robustness

Batch size 8, TensorFlow 1.1, a single GTX 1080. Two observations that matter for anyone shipping this. First, PointNet++ SSG is the smallest model in the table at 8.7 MB and still seven times slower than vanilla PointNet — because the cost has moved out of the matrix multiplies and into FPS and neighbourhood gathering, which are memory-bound scatter/gather operations, not FLOPs. A model that is small and slow is a strong hint that your bottleneck is data movement. Second, MSG doubles the time for its density robustness; MRG buys most of the same robustness for about 5% over SSG. If you are latency-bound on a robot, MRG is the line to read.

Density collapse and multi-scale rescue

One centroid on a locally flat patch, with the three MSG radii drawn around it. Drag the density slider to simulate range or dropout, and watch the occupancy of each ball against the N r2/4 prediction. The bar under each radius shows whether that scale still has enough support to mean anything.

points kept N = 1024

Slide it down to about 25% and read the r = 0.1 ring. In SSG mode that is the entire feature, and it is empty. In MSG mode the two larger rings are still populated, and the concatenated feature keeps carrying signal at a coarser granularity. That is the whole argument of Section 3.3, in one picture.

Random input dropout draws θ ~ U[0, 0.95] per cloud and then drops each point with probability θ, rather than using one fixed dropout rate. Why the two-level randomness?

Chapter 6: Segmentation by Feature Propagation

Classification wants one vector for the whole cloud, and both architectures deliver it. Segmentation wants one label per point — and both architectures have just spent their entire forward pass throwing points away.

PointNet's answer is simple and worth understanding before the harder one. After the global max pool produces a 1024-dim shape descriptor, copy it back onto every point and concatenate it with that point's own 64-dim local feature:

per-point input to the segmentation head = [ local (64) ‖ global (1024) ] = 1088 dims

Then run another shared MLP down to m class scores. Each point now decides its label knowing two things: what it is locally, and what the whole object is. That is enough to say "this is a leg-like local structure, and the object is a chair, therefore chair leg" — and the paper notes it is enough to predict per-point normals accurately, which is a real check that local information survived. It reached 83.7% mean IoU on ShapeNet part segmentation, a 2.3-point improvement on the previous best.

But notice what the global vector is: one 1024-dim summary, identical for every point. The only per-point signal is the 64-dim feature computed from that point's raw coordinates alone. There is still nothing about the neighbourhood.

PointNet++ has a harder problem

In PointNet++ the situation is worse, because subsampling is the architecture. After three levels you have 1024 → 512 → 128 → 1 points. Even if the final features are excellent, they live at 128 centroids and you need labels at all 1024 originals.

One option is to never subsample — sample all points as centroids at every level. That works and costs a fortune. The chosen option is to propagate features back up the hierarchy, in a U-Net-shaped decoder made of feature propagation (FP) levels.

Each FP level moves features from Nl points back to the Nl−1 points of the level below, in three steps.

Step 1 — interpolate
For each of the Nl−1 target points, find its k = 3 nearest points among the Nl coarse points and take an inverse-distance-weighted average of their features.
Step 2 — skip link
Concatenate the interpolated feature with the feature this point already had, saved from the corresponding set abstraction level on the way down.
Step 3 — unit PointNet
A shared MLP applied to each point independently — "similar to one-by-one convolution in CNNs" — to fuse the two sources into one updated feature.

Repeat until you are back at the original points, then a final MLP produces per-point class scores.

The interpolation, written out and worked

f(j)(x) = Σi=1..k wi(x) fi(j)  /  Σi=1..k wi(x),    wi(x) = 1 / d(x, xi)p

with the paper's defaults p = 2 and k = 3. This is classic inverse distance weighting, also known as Shepard interpolation: nearby sources dominate, and the weights are normalised so that if all three neighbours agree, the answer is that value exactly.

Work one channel by hand. Target point x has three nearest coarse points at distances 0.1, 0.2 and 0.4, carrying values 2.0, 5.0 and 9.0 in the channel we are tracking.

w1 = 1/0.12 = 100    w2 = 1/0.22 = 25    w3 = 1/0.42 = 6.25
Σw = 131.25
f(x) = ( 100(2.0) + 25(5.0) + 6.25(9.0) ) / 131.25 = ( 200 + 125 + 56.25 ) / 131.25 = 381.25 / 131.25 = 2.905

The answer is 2.905, very close to the nearest neighbour's 2.0 and barely influenced by the value 9.0 sitting four times further away. That is the p = 2 exponent doing its job: halving the distance quadruples the weight. The nearest source at 0.1 carries 76% of the total weight; the farthest at 0.4 carries 4.8%.

Change p to 1 and recompute to see the sensitivity: weights 10, 5, 2.5, sum 17.5, giving (20 + 25 + 22.5)/17.5 = 3.857. Much more blended. Push p toward infinity and it becomes pure nearest-neighbour copying, with a hard discontinuity everywhere the nearest neighbour changes. p = 2 is a compromise between blur and blockiness, and it is the same reason inverse-square weighting shows up everywhere from radial basis functions to point-cloud rendering.

Why the skip link is not optional. Interpolation alone can only produce values that are smooth blends of coarse features — every fine point within a coarse cell gets nearly the same answer, so a part boundary that falls inside one cell is smeared into a gradient. The skip connection reinjects the original fine-resolution feature at that exact point, which is the only thing in the decoder that knows where a boundary actually is. This is precisely the role of U-Net's skip connections in image segmentation, and PointNet++ arrives at it independently for the same reason.

Concept and realization: a feature propagation level

python# Move features from the COARSE set (xyz2, feat2) back onto the FINE set (xyz1),
# then fuse with the encoder's own features for those fine points.
# xyz1: (B, N1, 3)   xyz2: (B, N2, 3) with N2 <= N1
def feature_propagation(xyz1, xyz2, skip_feat, feat2, unit_mlp):
    if xyz2.shape[1] == 1:                      # the global-vector special case:
        interp = feat2.repeat(1, xyz1.shape[1], 1)   # one source -> pure copy,
    else:                                        # which is PointNet's own trick.
        d, i   = three_nn(xyz1, xyz2)             # (B, N1, 3) dists + source indices
        w      = 1.0 / (d + 1e-8)**2              # p = 2; epsilon guards a coincident point
        w      = w / w.sum(dim=2, keepdim=True)  # normalise so identical sources -> that value
        interp = (gather(feat2, i) * w.unsqueeze(-1)).sum(dim=2)   # (B, N1, C2)

    # The skip link: the ONLY thing in the decoder that knows where boundaries are.
    fused = cat([interp, skip_feat], dim=-1) if skip_feat is not None else interp
    return unit_mlp(fused)                       # (B, N1, C') — shared, per-point, 1x1

Three details in that listing earn their place.

The epsilon. If a fine point coincides exactly with a coarse point — which happens constantly, because the coarse points are a subset of the fine ones, chosen by FPS — then d = 0 and 1/d2 is infinite. The guard turns that into an enormous but finite weight, so after normalisation the coincident source gets essentially all of it. Which is correct behaviour: a point that is a coarse point should receive that coarse point's feature.

The normalisation. Dividing by the weight sum is what makes the interpolation an average rather than a sum. Without it, a fine point surrounded by three very close sources would receive a feature three hundred times too large, and a fine point in a sparse gap would receive one that is far too small. Normalisation makes the operation density-blind, which after Chapter 5 you should recognise as the recurring theme.

The unit PointNet is literally a 1×1 convolution. The paper says so — "similar to one-by-one convolution in CNNs" — and it is the same object as the shared MLP from Chapter 1: one linear map applied to every point independently. It is what fuses the interpolated coarse feature with the skip-linked fine feature into a single vector, and it is permutation-safe for the same reason everything else here is.

The real segmentation architectures

The ScanNet semantic labelling network, from the supplementary material, using FP(widths) for a feature propagation level:

SA(1024, 0.1, [32,32,64]) → SA(256, 0.2, [64,64,128]) → SA(64, 0.4, [128,128,256]) → SA(16, 0.8, [256,256,512])
→ FP(256,256) → FP(256,256) → FP(256,128) → FP(128,128,128,128,K)

Four levels down, four levels up, perfectly symmetric — an hourglass, with skip links across the waist. Read the radii on the way down: 0.1, 0.2, 0.4, 0.8, doubling each time, exactly like a stride-2 CNN doubling its effective kernel. Read the point counts: 1024, 256, 64, 16 — a factor of four each level. And note that the deepest level keeps 16 points rather than collapsing to one, because in a scene you want several coarse regions, not a single summary.

The ShapeNet part segmentation network is shallower, and it does collapse to a global vector, because a single object does have a meaningful global identity:

SA(512, 0.2, [64,64,128]) → SA(128, 0.4, [128,128,256]) → SA([256,512,1024])
→ FP(256,256) → FP(256,128) → FP(128,128,128,128,K)

Note the first FP level here interpolates from a single point — the global vector — back onto 128 centroids. With one source, inverse distance weighting degenerates to copying, so this level is exactly PointNet's "concatenate the global vector onto every point" trick, recovered as a special case of the general mechanism.

Why not simply never subsample?

The paper mentions the alternative in one clause — "always sample all points as centroids in all set abstraction levels, which however results in high computation cost" — and it is worth pricing, because the number is startling.

Take the ScanNet configuration: 8192 input points per cube, four levels. With subsampling, the levels carry 1024, 256, 64 and 16 centroids. Without it, all four levels carry 8192. The grouped tensor at each level is N' × K × C, so level 1 alone goes from

1024 × 32 × 64 = 2,097,152  →   8192 × 32 × 64 = 16,777,216  (8×)

and the deeper levels are far worse: level 4 goes from 16 × K × 512 to 8192 × K × 512, a factor of 512. Summing across levels, the encoder becomes roughly two orders of magnitude more expensive, and FPS — already O(N N') — becomes O(N2) at every level.

So the decoder is not an aesthetic choice. Downsampling is what makes the deep levels affordable, and feature propagation is the bill for it. This is the same trade a U-Net makes in images, and the interpolation-plus-skip structure is the same answer, arrived at for the same reason.

The ScanNet protocol, and the class imbalance underneath it

The ScanNet setup is worth knowing because it is a template for scene-scale point learning, and because two of its details are about a problem the architecture cannot solve.

The dataset is 1,513 scanned and reconstructed indoor scenes, split 1,201 train / 312 test. Training cubes are 1.5 m × 1.5 m × 3 m, sampled on the fly from a scene and randomly rotated about the vertical axis. A cube is kept only if at least 2% of its voxels are occupied and at least 70% of its surface voxels carry valid annotations — a filter, not a nicety, because a cube of empty air or unlabelled geometry contributes gradient noise and nothing else. Each cube is padded with augmented points to a fixed cardinality of 8,192. At test time the scene is split into cubes, every point gets a prediction, and points that appear in several cubes are resolved by majority vote.

Notice what that last step implies: the same physical point can get different labels from different cubes, because its position within the cube — and therefore its neighbourhood context — differs. Majority voting over overlapping windows is the point-cloud version of test-time augmentation, and it is doing real work.

The imbalance is the part the architecture does not address. In an indoor scan, floor and wall points outnumber chair and lamp points by a large factor, and a per-point cross-entropy will happily reach high overall accuracy by getting the structure right and the furniture wrong. That is exactly the failure the paper's Figure 6 shows for PointNet. Overall point accuracy and mean IoU diverge for this reason — PointNet's S3DIS numbers are 78.62% overall accuracy against 47.71 mean IoU — and if you report only the first you are reporting the floor.

What it bought

TaskPointNetPointNet++Prior best
ShapeNet part segmentation (mean IoU)83.785.184.7 (SSCNN), 81.4 (Yi et al.)
S3DIS semantic segmentation (mean IoU, 13 classes)47.7120.12 (handcrafted-feature baseline)
S3DIS overall point accuracy78.6253.19 (baseline)
3D detection from segmentation (mean AP @ IoU 0.5)24.2418.22 (Armeni et al.)

On ShapeNet part segmentation the jump is 83.7 → 85.1 mean IoU, which sounds modest until you note that PointNet++ used the same data with normals added and beat a spectral graph CNN without any eigendecomposition. On ScanNet the paper's Figure 6 makes the qualitative point that the table cannot: PointNet gets the room layout right and misses the furniture; PointNet++ segments the furniture. That is the local-structure argument, visible.

The S3DIS setup is worth knowing as an engineering pattern in its own right. The scans are split by room, then into 1 m × 1 m blocks; each point is a 9-dimensional vector — XYZ, RGB, and the point's normalised position within the room, 0 to 1 — and 4096 points are sampled per block on the fly during training, with all points used at test. That normalised-room-position channel is a quiet trick: it hands the network a global positional prior (ceilings are at the top, floors at the bottom) that the local block would otherwise have no way to know.

In a feature propagation level, why is the interpolated feature concatenated with a skip-linked feature from the encoder rather than used on its own?

Chapter 7: The Numbers Then, the Lineage Now

It is easy, nine years on, to read PointNet as obviously correct. It is worth reconstructing what it was actually competing against, and by how much it won, before tracing where the idea went.

ModelNet40, 2016 to 2017

The benchmark: 12,311 CAD models across 40 mostly man-made categories, split 9,843 train / 2,468 test. PointNet's protocol — worth copying if you ever reproduce this — samples 1024 points uniformly on mesh faces proportional to face area (so large flat surfaces get more points, which is what a scanner would do), normalises into a unit sphere, and augments with random rotation about the up-axis plus per-point Gaussian jitter at σ = 0.02.

MethodInputViewsAvg classOverall
SPH (hand-crafted)mesh68.2
3DShapeNetsvolume177.384.7
VoxNetvolume1283.085.9
Subvolumevolume2086.089.2
MVCNNimage8090.1
PointNet baseline (hand features + MLP)point72.677.4
PointNetpoint186.289.2
PointNet++ (SSG)point190.7
PointNet++ with normals, N = 5000point191.9

Three readings. First, PointNet ties the best volumetric method (89.2 overall) using one pass over raw points against twenty rendered subvolumes, at 8× fewer FLOPs. Second, MVCNN's 90.1 average-class is still ahead, and the paper attributes the gap to "loss of fine geometry details that can be captured by rendered images" — the Chapter 2 critical-points limitation, admitted. Third, PointNet++ closes and passes it: 90.7 on coordinates alone, 91.9 with normals and 5000 points.

The MNIST experiment in the PointNet++ paper is a good sanity check on the whole framing. Convert each digit image to a 2D point cloud of its lit pixel locations and classify the set. PointNet vanilla gets 1.30% error, PointNet 0.78%, PointNet++ 0.51% — against LeNet-5's 0.80% and Network-in-Network's 0.47%. A method that never learned that pixels have neighbours matches a purpose-built image CNN, on images.

And the non-Euclidean result deserves more attention than it usually gets. On SHREC15, 1200 non-rigid shapes in 50 categories, the authors built the metric space from geodesic distances along the surface rather than Euclidean distance through space, so that a horse's nose and its tail are far apart even when the horse is curled up:

Metric spaceInput featureAccuracy
EuclideanXYZ60.18
Euclideanintrinsic (WKS, HKS, curvature)94.49
Non-Euclidean (geodesic)intrinsic96.09
DeepGM (prior best)intrinsic93.03

The 60.18 → 94.49 jump from swapping the input feature, and the further 94.49 → 96.09 from swapping the distance function used for sampling and grouping, together make a structural point: PointNet++ is not tied to R3. Sampling and grouping only need a metric. Give them a geodesic one and the same architecture learns intrinsic surface structure. Give them a learned or graph metric and you have something close to a graph neural network.

The two architectures, side by side

PointNet (2016)PointNet++ (2017)
Scales seenTwo: one point, whole cloudFour: point, 0.2 ball, 0.6 effective, global
Where points interactOne max pool, onceOnce per group, per level
Translation invarianceLearned, approximate (input T-Net)Exact, per group, by centroid subtraction
Rotation invarianceLearned, approximate (T-Nets)Not addressed — T-Nets dropped
Density robustnessIncidental (global summaries degrade gracefully)Designed — MSG / MRG plus random input dropout
Model size40 MB (3.5M params)8.7 MB SSG, 12 MB MSG
Forward pass (batch 8)25.3 ms82.4 ms SSG, 163.2 ms MSG
ModelNet4089.290.7 / 91.9 with normals
ShapeNet parts (mIoU)83.785.1
BottleneckExpressiveness — no mid-level featuresLatency — sampling and gathering

Read the last row as the summary of the pair. PointNet is fast and blunt; PointNet++ is sharp and slow, and the slowness is not in the arithmetic. Every architecture since has been an attempt to keep the second column's accuracy at the first column's speed, and the levers they pull are always the neighbourhood machinery, never the MLPs.

Notice one more thing: PointNet++ dropped the T-Nets entirely. It is not that alignment stopped mattering; it is that centroid subtraction handles translation exactly and for free, and the remaining rotation problem was judged not worth 2.65M parameters. That is a good example of a later paper deleting its predecessor's most conspicuous module because a structural change made it redundant.

Part segmentation, and the partial-scan result that matters most

ShapeNet part segmentation is 16,881 shapes across 16 categories, annotated with 50 parts in total — most categories have two to five. The metric is mean IoU over parts, averaged within a category and then across categories, with the convention that an empty union counts as IoU 1 (otherwise a category whose part is genuinely absent would be punished for correctly predicting nothing).

MethodMean IoUNotable per-category
Yi et al. (hand-crafted + correspondences)81.4guitar 92.0, laptop 95.7
3D CNN baseline79.4ear phone 63.5, motorbike 58.7
PointNet83.7ear phone 73.0 (+9.5), cap 82.5 (+4.8)
SSCNN (spectral graph CNN)84.7
PointNet++85.1cap 87.7, car 77.3

PointNet's biggest per-category gains are on ear phone (+9.5 over Yi et al.) and cap (+4.8) — categories with few training shapes, 69 and 55 respectively. That is a signature of a method that is learning transferable geometry rather than memorising category-specific priors, and it is the segmentation-side echo of the Chapter 2 finding that critical sets generalise to unseen categories.

But the experiment worth taking away is the one people skip. The authors used a Kinect simulator to generate incomplete point clouds from six random viewpoints for every CAD model — the kind of one-sided, self-occluded scan a real depth camera produces — retrained with the same architecture and settings, and measured the loss:

complete CAD models → simulated partial Kinect scans:   −5.3 mean IoU

Five points, for throwing away everything the camera could not see. That number is Theorem 2 again: a partial scan is a subset of the full cloud, and if the surviving subset still contains most of the critical points, the descriptor barely moves. The whole robustness story — deletion, outliers, occlusion, partial views — is one theorem seen through four different experiments.

What the first-layer kernels learned

PointNet++'s Figure 8 is the point-cloud equivalent of the famous Gabor-filter visualisation from the first layer of an image CNN, and it is done in a way worth copying. The authors laid a voxel grid over space, found for each first-level neuron the local point sets that activated it most strongly (the top 100), aggregated those into the grid, kept the cells with the most votes, and converted back to points. The result is a rendering of "what pattern does this neuron detect".

What came out, on a model trained on ModelNet40's mostly-furniture categories: planes, double planes, lines and corners. Which is exactly the vocabulary you would design by hand if you were writing a classical 3D feature detector, and exactly the analogue of edges and blobs in an image CNN's first layer. Set abstraction did not just improve accuracy — it produced the interpretable low-level vocabulary that PointNet, having no local layer, could not have.

Where the idea went: the lineage in four branches

Branch 1 — sparse convolution wins the scene-scale fight. The voxel idea did not die; it got fixed. Instead of allocating a dense tensor, store only occupied cells in a hash map and define convolution over that sparse structure — Choy et al.'s Minkowski networks generalised this to arbitrary dimensions, including a fourth time axis for lidar sequences. This keeps the CNN's clean local receptive fields and its highly optimised kernels while paying only for occupied space, and for large outdoor scenes it became the default backbone for perception stacks. PointNet's critique of voxels was about the dense tensor, and sparse convolution answered it directly.

Branch 2 — attention replaces the mini-PointNet. Zhao et al.'s Point Transformer kept PointNet++'s sampling-and-grouping skeleton and swapped the per-group max-pooled MLP for a self-attention layer over the neighbourhood, reaching 70.4% mIoU on S3DIS Area 5 — the first crossing of the 70% line, 3.3 points over the previous best. The successor Point Transformer V3 made the opposite kind of move, arguing that at scale, simplicity and throughput beat mechanism: it replaced exact kNN neighbourhoods with a serialised neighbour mapping, which let it widen the receptive field from 16 to 1024 points while running 3× faster and using 10× less memory than its predecessor. Notice what survived every one of those redesigns — sample centroids, gather neighbourhoods, pool. That is PointNet++'s set abstraction, with the interior swapped.

Branch 3 — self-supervision, and PointNet as a component. Point-MAE (Pang et al., 2022) is the clearest example of the idea being absorbed rather than replaced. It masks most of a cloud and asks a transformer to reconstruct the missing bits, in the style of masked autoencoders for images. The pipeline: 1024 input points, FPS to n = 64 patch centres, kNN with k = 32 to form patches, mask 60–80% of the patches, encode only the visible ones with 12 transformer blocks of width 384 and 6 heads, decode with 4 blocks, and reconstruct masked patch coordinates under an l2 Chamfer distance.

The detail that matters for this lesson is the patch embedding. The authors considered flattening each 32×3 patch and applying a linear projection, as ViT does to image patches — then rejected it in one sentence: "we argue that linear embedding fails to follow the principle of permutation invariance." A patch is still a set. So they embed each patch with "a lightweight PointNet, which mainly consists of MLPs and max pooling layers." Six years on, PointNet is the tokeniser.

The results: pretrained on ShapeNet (about 51,300 models, 55 categories, 300 epochs), Point-MAE reaches 85.18% on the hardest ScanObjectNN variant — real scans with cluttered backgrounds — beating Point-BERT by 2.11 points, and 94.04% on ModelNet40 with 8192 points (93.8% at 1024, against 91.4% training from scratch). Few-shot accuracy advanced by 1.5–2.3 points. The same masking-and-reconstruction recipe that worked for text and images works for point sets, once you have a permutation-invariant tokeniser.

Branch 4 — alignment to language and images. ULIP (Xue et al., 2022) attacks the other bottleneck: 3D datasets are tiny and their label sets are fixed. It builds (image, text, point cloud) triplets automatically, freezes a pretrained vision-language model, and trains a 3D encoder to land in the space that model already occupies. Crucially it is backbone agnostic — PointNet++, Point-BERT, PointMLP all slot in — so the 3D architecture question and the supervision question separate cleanly. The consequence is open-vocabulary 3D: you can ask about a category by typing it, exactly as CLIP did for images.

What actually survived. Not the specific MLP widths, not the T-Nets — almost nobody builds T-Nets any more. What survived is a type signature: a function from an unordered, metric-carrying set to a vector, applied recursively over neighbourhoods. Sparse convolution, point transformers and masked autoencoders all implement that signature with different interiors. PointNet's real contribution was establishing that the signature is the right one, and proving that the symmetric-function shape is not a restriction (Theorem 1) and has a describable dependency structure (Theorem 2). Very few architecture papers leave behind two theorems that are still worth quoting.
Point-MAE embeds each 32-point patch with a lightweight PointNet rather than flattening it and applying a linear projection like ViT does to image patches. Why?

Chapter 8: Point-Cloud Embeddings in Practice

Both papers are written as classification and segmentation papers. But the object they actually produce — a fixed-length vector that describes a set of points and does not care how that set was ordered — is useful for a family of problems the papers barely mention. This chapter is about those, because in a real robotics or mapping stack you will use the embedding far more often than the classifier head.

The descriptor is a database key

Strip the classification head off and what you have is a function from a point set to a vector in R1024. Precompute it once for every object in a library, store the vectors, and retrieval becomes a dot product. The paper does this implicitly when it embeds the ModelNet40 test split's 1024-dim global signatures with t-SNE and observes that "similar shapes are clustered together according to their semantic categories" — the geometry of the embedding space carries category structure that nobody supervised directly.

Work a retrieval by hand, on four-dimensional descriptors so the arithmetic is visible. A query cloud embeds to

q = (0.6, 0.8, 0.0, 0.0),   ‖q‖ = √(0.36 + 0.64) = 1

and three library shapes embed to unit vectors a = (0.5, 0.5, 0.5, 0.5), b = (0.8, 0.6, 0, 0), c = (0, 0, 0.6, 0.8). Cosine similarity is the dot product of unit vectors:

q·a = 0.6(0.5) + 0.8(0.5) = 0.30 + 0.40 = 0.70
q·b = 0.6(0.8) + 0.8(0.6) = 0.48 + 0.48 = 0.96
q·c = 0 + 0 + 0 + 0 = 0.00

Rank: b, then a, then c. Nothing exotic — but notice that this is the entire inference cost of shape retrieval over a library of any size, once the library is embedded. Nearest-neighbour search over a million 1024-dim vectors is a solved engineering problem; running a network a million times is not.

A gotcha you will hit within a day: max-pooled descriptors are not length-normalised

Here is a property of PointNet's descriptor that follows directly from Chapter 2, and that no one warns you about.

Every component of u is a maximum over the points. Adding a point can raise a maximum and can never lower one. So u is componentwise monotone non-decreasing in the point set:

S ⊆ T  ⇒   uj(S) ≤ uj(T) for every j  ⇒   ‖u(S)‖ ≤ ‖u(T)‖

So a 100,000-point sweep of an object produces a systematically longer descriptor than a 1,000-point sweep of the same object — not a different direction, a different length. If you rank by raw dot product, close-range dense objects will beat far-range sparse ones for reasons that have nothing to do with shape. Normalise to unit length before you index, always. And if you cannot, at least fix the input point count, which is exactly why every experiment in both papers uses a constant N.

Two more normalisation traps in the same family. First, both papers normalise every cloud into a unit sphere before the network sees it — which means the descriptor is scale-blind by construction. A toy chair and a real chair embed identically. If absolute size matters for your task, you must feed it in separately as a scalar feature; the architecture threw it away. Second, PointNet's descriptor is not rotation invariant. The T-Net approximates an alignment, trained under up-axis rotation only. Query a database with a cloud tipped on its side and retrieval will fail in a way that looks like a model bug and is a symmetry bug.

Registration: aligning two scans

Registration is the problem of finding the rigid transform that maps one scan onto another. Classical pipelines do it in four stages, and PointNet-style networks slot into stage two.

1 — Keypoints
Pick repeatable locations in each cloud. FPS is a reasonable choice; so are curvature extrema.
2 — Local descriptors
Ball-query a neighbourhood around each keypoint, subtract the centroid, run a mini-PointNet, max-pool. This is exactly one set abstraction level, used on its own.
3 — Matching + RANSAC
Nearest-neighbour match descriptors across the two clouds, then sample triplets of matches and fit a rigid transform, keeping the transform with the most inliers.
4 — ICP refine
Polish the RANSAC estimate with iterative closest point on the dense clouds.

Stage two is where Chapter 4's centroid subtraction earns its keep: because the group is expressed relative to its centre, the descriptor is exactly translation invariant, which is half of the rigid-motion invariance registration needs. The other half — rotation — is not free, and you have three honest options: augment heavily with random rotations, compute a local reference frame from the neighbourhood (typically from the covariance's eigenvectors) and rotate the group into it before embedding, or use rotation-invariant inputs such as point-pair distances and angles rather than raw relative coordinates. The second is the most common in practice and is worth knowing about, because it is exactly the T-Net idea moved from global to local, where it is far better posed.

Place recognition and loop closure

A mapping robot revisits a corridor it drove through twenty minutes ago. It has to notice, because that recognition is what lets the pose graph close the loop and correct accumulated drift. Doing this by matching raw point clouds is hopeless; doing it by comparing one vector per location is easy.

The recipe is the retrieval pipeline with the semantics changed: embed each submap into a descriptor, store it with its pose, and on every new submap query the database for nearest neighbours. A hit above threshold becomes a loop-closure candidate, which then gets geometrically verified by registration — the pipeline above — before it is trusted enough to add a constraint to the graph.

Two design pressures shape what descriptor you want here, and both push away from plain max pooling. First, viewpoint: you may traverse the corridor in the opposite direction, so the descriptor must survive a yaw change of 180 degrees. Second, partial overlap: the two submaps share 60% of their content and each has 40% the other never saw. Max pooling is unhelpfully sensitive to the second, because a single strong response in the non-overlapping 40% can set a component of the descriptor. This is why the place-recognition literature moved to aggregation schemes with soft assignment — VLAD-style pooling over local descriptors, where every local feature contributes to a residual against a learned codebook rather than winner-takes-all. The set-abstraction front end usually stays; it is the pooling that gets replaced.

Sizing a retrieval system, end to end

Suppose you are building shape search over a library of one million CAD parts. Here is the whole system, priced.

Index build. One forward pass per part. PointNet classifies roughly 1,000 objects a second on a 1080-class GPU, so a million parts is about 17 minutes of compute — a coffee, not a project. Store the descriptor at 1024 float32s, or 4 KB per part: 4 GB for the library. Quantise to int8 and it is 1 GB, which fits in memory on a laptop.

Query. One forward pass (about 1 ms) plus a nearest-neighbour search. Exact search over a million 1024-dim vectors is a single matrix multiply of 1M × 1024 by 1024 × 1, roughly 2 GFLOPs — a couple of milliseconds on a GPU, tens of milliseconds on a CPU. An approximate index brings that under a millisecond if you need it.

The dimension question. 1024 is the pooled width, chosen for classification accuracy, not for retrieval. For search you usually want it smaller: memory, index build time and search time all scale linearly with it. A learned projection down to 128 or 256 — trained with a metric-learning objective on top of the frozen encoder — typically loses very little retrieval quality and cuts the index by 4–8×. This is the same move CLIP-style systems make, and the same reason.

The thing that will actually cost you. Not compute. Pose. Every caveat in the previous section is a source of false negatives, and the one that bites first is rotation: if the library was embedded from canonically-oriented CAD files and your queries are scans in arbitrary poses, recall collapses and no amount of index tuning helps. The two fixes are to embed each library item at several orientations (multiplying your index by the number of orientations, which is cheap and dumb and works), or to use a rotation-invariant descriptor and accept the information it discards.

Choosing keypoints, and why FPS is a reasonable default

The registration pipeline above begins with "pick repeatable locations". Repeatability is the whole game: a keypoint detector that fires at different physical locations on two scans of the same scene produces descriptors that cannot be matched, no matter how good the descriptor is.

Classical detectors look for curvature extrema or local maxima of some saliency function — the 3D analogue of corner detection. They are genuinely repeatable on clean data and fragile under noise, because curvature is a second derivative and second derivatives amplify noise.

FPS is a different kind of answer: it is not looking for salient points at all, it is looking for a covering. Its repeatability argument is not "the same points get chosen" — they will not be, since FPS starts from an arbitrary seed — but "the chosen points are spread evenly enough that every part of the surface is near some centroid in both scans". Combined with a descriptor computed over a fixed radius, that is often enough: the two centroids need not coincide, only to be close enough that their balls overlap substantially. This is the same insight that made ball query the right grouping rule, applied one level up.

It also makes the randomness measurement from Chapter 4 relevant here in a way the paper did not intend. A 0.17% accuracy standard deviation across FPS seeds says the downstream features barely notice which points were chosen — which is precisely the stability property a keypoint scheme needs.

Latency arithmetic, so the numbers are not a surprise

Take a 10 Hz lidar producing 100,000 returns per sweep — a million points a second, and 100 ms of budget per frame if you want to keep up.

PointNet's own benchmark: more than one million points per second for classification on a 1080X, which is roughly 1,000 objects a second or 2 rooms a second for segmentation. So a single global PointNet pass over a full sweep is genuinely real-time, and that is the reason it survived in production for pre-processing and proposal scoring long after better classifiers existed.

PointNet++ is a different story. From the timing table: SSG runs 82.4 ms for a batch of 8 clouds of 1024 points, so about 10 ms per 1024-point cloud, and MSG about 20 ms. To cover 100,000 points at 1024 per tile you would need roughly 100 tiles — a second of compute per frame, ten times over budget. The way out in practice is exactly what the S3DIS setup does: process spatial blocks, batch them, and accept that the hierarchy costs an order of magnitude more than the flat model. Chapter 5's table already told you where that time goes — SSG is the smallest model in the comparison at 8.7 MB and one of the slowest, because FPS and ball query are gather-bound, not FLOP-bound.

The engineering rule this implies. When a model is small and slow, adding FLOPs is nearly free and adding points is not. Optimise the neighbourhood search, not the MLP. That single observation is most of the motivation behind the later architectures — sparse convolution replaces neighbourhood search with hash lookups, and Point Transformer V3 explicitly abandoned exact kNN for a serialised ordering to buy 3× throughput and 10× memory.
Cross-domain bridge
PointNet is DeepSets, and a transformer without positional encoding is PointNet with a different pool
The pattern — per-element network, symmetric aggregation, output network — is exactly the DeepSets characterisation of permutation-invariant functions, and it appears anywhere the input is a set: a bag of detections fed to a tracker, a variable-length list of user events fed to a recommender, a set of candidate answers fed to a re-ranker. It is also what a transformer computes if you delete the positional encodings: self-attention over an unordered set is permutation equivariant, and a mean or CLS-token readout makes it invariant. Seen that way, Point Transformer is PointNet++ with attention where the max used to be, and the entire debate about which is better is a debate about the aggregation operator, not about the framing. If you have built a set-input model in any other domain, you have already built the skeleton — see our vector embeddings and similarity metrics lessons for the retrieval half of this chapter with different labels.
You index a library of PointNet descriptors and rank retrievals by raw dot product. Dense close-range scans consistently outrank sparse far-range scans of the same object. What is the cause?

Chapter 9: Connections and Cheat Sheet

Two papers, six months apart, by the same four people, in which the second one's job is to fix the first one's stated flaw. Here is the whole arc on one page.

The problem
A point cloud is an unordered set. N! feeding orders, about 102640 at N = 1024. Grids and renders make it CNN-shaped at 8× to 141× the FLOPs.
PointNet
Per-point shared MLP, one max pool, output MLP. Invariance by algebra, not by training. Universal for continuous set functions (Thm 1). 89.2% ModelNet40, 3.5M params, 440M FLOPs.
The consequence
The descriptor depends on at most K critical points (Thm 2). Robust: −3.7% at 50% deletion where VoxNet drops 40.3. And blind: fine detail lives in the non-critical points.
PointNet++
Set abstraction = FPS + ball query + mini-PointNet, stacked. Receptive fields grow, radii compose. 90.7% / 91.9% ModelNet40, 85.1 mIoU ShapeNet parts.
The lineage
Sparse convolution for scenes, transformers for the group operator, Point-MAE for self-supervision with a PointNet tokeniser, ULIP for language alignment.

Cheat sheet

IdeaOne lineNumber to remember
Permutation problemThe array has an order; the set does not1024! ≈ 102640
Symmetric functionγ(MAX{h(xi)}) — per-point net, then a pool that ignores orderEq. (1)
Why max, not meanMean divides a rare pattern by n; max passes it throughmeasured on ModelNet40
Theorem 1Universal approximator of continuous set functionsworst case = learned voxelisation
Theorem 2Output determined by CS, unchanged for any T with CS ⊆ T ⊆ NS|CS| ≤ K
RobustnessHalf the points deleted barely matters−2.4% / −3.8%; VoxNet 86.3 → 46.0
Bottleneck widthK is your resolution budget64 → 1024 buys 2–4%
Input sizeAccuracy saturatesaround 1K points
T-NetsLearned affine alignment, 3×3 and 64×64, identity-initialised+2.1% for 2.65M params
Orthogonality penalty‖I − AATF2, weight 0.001without it: 86.9 < 87.1 (no transform)
Set abstractionSample (FPS) → group (ball query) → mini-PointNetN × (d+C) → N' × (d+C')
FPSGreedy covering-radius minimisation, data dependentseed randomness: 0.17% accuracy std
Ball vs kNNFixed physical scale makes shared weights meaningfulr=0.2: 90.7 · kNN k=64: 90.3
Centroid subtractionExact per-group translation invariance, for freeone line, zero parameters
Density arithmeticExpected ball occupancy on a surfaceN r2/4 → 2.56 points at r = 0.1
MSGThree radii per level, concatenated[0.1, 0.2, 0.4] then [0.2, 0.4, 0.8]
Random input dropoutθ ~ U[0, 0.95], then per-point coin flipMSG+DP: <1% loss, 1024 → 256 points
MRGSub-region summary ‖ raw-point summary87.0 ms vs MSG's 163.2 ms
Feature propagationInverse-distance interpolate, skip link, unit PointNetp = 2, k = 3
Descriptor normMonotone in the point set — normalise before indexingS ⊆ T ⇒ ‖u(S)‖ ≤ ‖u(T)‖

A debugging table, because these models fail in recognisable ways

Every entry below is a symptom that has a specific cause in something derived in this lesson, which is unusual and worth exploiting.

SymptomLikely causeCheck
Accuracy drops when you shuffle the input rowsOrder leaked in somewhere — a real convolution kernel wider than 1, a flatten, an RNN, a sortAssert the output is bitwise equal under a random row permutation. Make this a unit test.
Retrieval favours dense scans over sparse onesDescriptor norm is monotone in the point set (Chapter 8)Plot ‖u‖ against point count. Normalise before indexing.
Works on your data, fails on a colleague'sPose distribution differs; the T-Net only ever saw up-axis rotationEvaluate under random SO(3) rotation. If it collapses, the bug is symmetry, not capacity.
Fine parts are missed; coarse layout is rightGlobal pooling with no local layer (Chapter 4)Look at the critical set size. If a few points win everything, no local detail is reaching the descriptor.
Good on CAD models, poor on real scansDensity mismatch — small-radius groups are starved (Chapter 5)Log group occupancy per radius at inference. Compare with N r2/4.
Out of memory when you raise the group capThe grouped tensor is N' × K × C before the pool (Chapter 4)Reduce K before reducing MLP width — memory is linear in K and the pool discards it anyway.
Adding the feature T-Net made things worseMissing or mis-weighted orthogonality penalty (Chapter 3)Log the singular values of the predicted 64×64. If the smallest is near zero, it is destroying dimensions.
Segmentation boundaries are smearedSkip connections missing or too narrow in the FP levels (Chapter 6)Ablate the skip. If nothing changes, it was never wired.
High overall accuracy, poor mean IoUClass imbalance — the model is predicting floor and wall (Chapter 6)Always report per-class IoU. Overall point accuracy on indoor scans is nearly meaningless.
The model is small but slowNeighbourhood search, not arithmetic (Chapters 5 and 8)Profile FPS and ball query separately from the MLPs. Optimise the gather.

Reproducing this today, if you want to

Both papers are old enough that the exact protocols are worth restating, because half of the reproduction difficulty is data preparation rather than modelling.

ModelNet40 classification. 9,843 train / 2,468 test. Sample 1024 points on mesh faces proportional to face area — area-weighted, not per-vertex, or flat surfaces will be under-represented and detailed regions over-represented. Normalise to zero mean and unit sphere. Augment with random rotation about the up-axis and per-point Gaussian jitter at σ = 0.02. PointNet++ adds random scaling and translation, uses Adam at learning rate 0.001, and reports about 20 hours to convergence on a GTX 1080 or Titan X.

ShapeNet part segmentation. 16,881 shapes, 16 categories, 50 parts, official split. Category label is assumed known at test time — you are predicting parts within a known category, not category and parts jointly. IoU convention: an empty ground-truth-and-prediction union counts as 1.

S3DIS semantic segmentation. 271 rooms across 6 areas, 13 classes. Split by room, then into 1 m × 1 m blocks. Each point is 9-dimensional: XYZ, RGB, and normalised position within the room. Sample 4096 points per block on the fly during training; evaluate on all points; use the standard k-fold protocol across areas rather than a random split, or you will leak.

The two things most likely to cost you a point or two if you get them wrong: area-weighted sampling, and remembering that "normalise into a unit sphere" is per-shape, not dataset-wide.

The three things worth arguing about

Max pooling is lossy in a way nobody fully fixed. Theorem 2 is a robustness result and a capacity ceiling at the same time. Later work replaced the pool with attention, with VLAD-style soft assignment, with sorted-k pooling — each recovering some of the discarded points at some cost. There is still no aggregator that is simultaneously as cheap as max, as robust as max, and as expressive as attention.

Neighbourhood search, not arithmetic, is the bottleneck. An 8.7 MB model that takes 82 ms is telling you plainly where the time is. Every major speedup since has come from changing how neighbourhoods are found — hashing, voxel binning, space-filling-curve serialisation — and not from changing the network.

Rotation is still not solved cleanly. The T-Net is a learned approximation with no guarantee, trained under up-axis rotation only. Local reference frames help and add fragility of their own; rotation-invariant input features help and discard information. This remains a live design decision in every point-cloud system, and if you are debugging a model that works on your dataset and fails on someone else's, check the pose distribution first.

"What I cannot create, I do not understand."
Write the twelve lines: a shared MLP over an N×3 tensor, a max over the point axis, an MLP head. Train it on ModelNet40 with 1024 points. You will be somewhere near 87% by dinner, and the number 89.2 will stop being a number you read.
Exit gate — teach it back before you leave.

Without scrolling up: (1) push the four toy points through the 3×2 MLP and produce the pooled vector, then name the critical set and justify why deleting C and D changes nothing; (2) write the three inequalities that define NS for that toy and test whether (0.4, 0.6) is inside; (3) reproduce the 3.5M parameter count from the layer widths and say which single layer is 30% of the model; (4) run FPS by hand on the six-point example and state the covering radius after three picks; (5) compute the expected occupancy of a radius-0.1 ball at N = 1024 and at N = 256, and explain what MSG does about the second number. If any of the five stalls, its chapter is one tap away.

References worth reading next

  1. Qi, C. R., Su, H., Mo, K., Guibas, L. J. "PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation," CVPR 2017 — arXiv:1612.00593. Chapters 0–3 and 6–7 are built on this.
  2. Qi, C. R., Yi, L., Su, H., Guibas, L. J. "PointNet++: Deep Hierarchical Feature Learning on Point Sets in a Metric Space," NeurIPS 2017 — arXiv:1706.02413. Chapters 4–6. Read the supplementary for the architecture strings.
  3. Pang, Y., Wang, W., Tay, F. E. H., Liu, W., Tian, Y., Yuan, L. "Masked Autoencoders for Point Cloud Self-supervised Learning" (Point-MAE), ECCV 2022 — arXiv:2203.06604. Where PointNet became the tokeniser.
  4. Zhao, H., Jiang, L., Jia, J., Torr, P., Koltun, V. "Point Transformer," ICCV 2021 — arXiv:2012.09164. Attention in place of the group-wise max.
  5. Wu, X. et al. "Point Transformer V3: Simpler, Faster, Stronger," CVPR 2024 — arXiv:2312.10035. Neighbourhood search is the bottleneck, argued at scale.
  6. Choy, C., Gwak, J., Savarese, S. "4D Spatio-Temporal ConvNets: Minkowski Convolutional Neural Networks," CVPR 2019 — arXiv:1904.08755. The sparse-convolution answer to the voxel critique.
  7. Xue, L. et al. "ULIP: Learning a Unified Representation of Language, Images, and Point Clouds for 3D Understanding," CVPR 2023 — arXiv:2212.05171. Backbone-agnostic alignment to a frozen vision-language space.
  8. Jaderberg, M., Simonyan, K., Zisserman, A., Kavukcuoglu, K. "Spatial Transformer Networks," NeurIPS 2015. The module the T-Net simplifies.
  9. Vinyals, O., Bengio, S., Kudlur, M. "Order Matters: Sequence to Sequence for Sets," ICLR 2016. The prior work PointNet cites for why the RNN route does not scale.
Which single sentence best captures why these two papers mattered more than their benchmark numbers suggest?