"A point cloud is a bag of numbers that refuses to sit still on a grid. Half of lidar research is politely convincing it to."
A Tidying-Up AI Agent
The Big Picture
A lidar sweep hands you an unordered set of tens or hundreds of thousands of 3D points, with no fixed count, no grid, and no canonical order. Every powerful perception backbone you know, from convolutional networks to transformers, was built for tensors on a regular lattice. The gap between those two facts is the single most important design decision in lidar AI: how do you turn a formless point set into something a network can convolve or attend over, without throwing away the geometry that makes lidar valuable? This section lays out the three answers the field converged on, points, voxels, and pillars, and shows why pillars in particular became the workhorse of real-time autonomous driving. Choose the representation well and a detector runs at 60 Hz on an embedded GPU; choose badly and it either drowns in empty 3D cells or blurs away the small objects you most needed to see.
This section builds directly on the raw point-cloud data structure from Section 42.1 and on the general idea, introduced in Chapter 40, that 3D measurements can live either on an image-like grid or as free-floating points. We assume the neural-representation vocabulary of Chapter 13: embeddings, pooling, and why a network must respect the symmetries of its input. Our job here is narrow and load-bearing: define the encodings, derive the pillar pipeline that the sparse-convolution backbones of Section 42.3 and the transformers of Section 42.4 consume, and name the tradeoffs that decide which one you ship.
Why raw points resist standard networks
A point cloud \(P = \{\mathbf{p}_i\}_{i=1}^{N}\) has three properties that break naive convolution. It is unordered: any function that consumes it must be invariant to permutations of the \(N\) points, so a network cannot simply flatten the list into a vector. It is variable in size: \(N\) changes every frame, from a dense near-field return to a sparse distant one. And it is irregularly sampled: density falls off roughly as \(1/r^2\) with range and clusters along scan lines, so there is no fixed neighbor stencil. PointNet-style architectures address permutation invariance directly by applying a shared multilayer perceptron to each point and then pooling with a symmetric operator such as max, but pure point methods struggle to scale to the full-scene, hundred-thousand-point clouds of automotive lidar. The dominant alternative is to discretize: impose a grid, and let each cell aggregate the points that fall inside it. That single move restores order, fixes size, and regularizes density, at the cost of quantization.
Key Insight
Discretization is a bargain, not a free lunch. Voxelizing trades the exact continuous position of each point for the grid-friendliness a convolution needs. The art is choosing the cell size that keeps the geometry that matters: too coarse and two pedestrians merge into one occupied cell; too fine and 99 percent of your cells are empty, wasting memory and compute on nothing. Every representation in this section is a different point on that resolution-versus-emptiness curve.
Voxelization: a 3D grid of learned cell features
Voxelization partitions space into a regular 3D lattice of cubic cells (voxels) of side \(v_x \times v_y \times v_z\) over a defined range, then assigns each point to the voxel containing it. The voxel index of a point is simply
$$ \left( \left\lfloor \frac{x - x_{\min}}{v_x} \right\rfloor,\; \left\lfloor \frac{y - y_{\min}}{v_y} \right\rfloor,\; \left\lfloor \frac{z - z_{\min}}{v_z} \right\rfloor \right). $$The influential VoxelNet architecture (Zhou and Tuzel, 2018) turned each non-empty voxel into a learned feature vector with a Voxel Feature Encoding (VFE) layer: a small PointNet applied to the points inside that voxel, producing one fixed-length embedding per occupied cell. The result is a sparse 4D tensor (three spatial axes plus channels) that a 3D convolutional network can process. The decisive practical fact is sparsity: in a typical outdoor scene, well over 90 percent of voxels are empty, because lidar only sees surfaces, not filled volume. Running dense 3D convolutions over that mostly-empty grid is ruinously expensive, which is exactly the problem that sparse convolution, the subject of Section 42.3, was invented to solve. For now, hold onto the shape of the output: a set of occupied cells, each carrying a coordinate and a feature vector.
Pillars: collapse the vertical axis and go 2D
PointPillars (Lang et al., 2019) made a shrewd simplification. Instead of dividing height into many voxels, it uses a single voxel that spans the full vertical extent: a pillar, an infinitely tall column with a small \(x\)-\(y\) footprint. Every point is binned into one pillar by its horizontal position alone. A PointNet-style encoder turns the points in each pillar into a feature vector, and those vectors are scattered back to their \((x, y)\) locations to form a dense 2D pseudo-image: a bird's-eye-view feature map with channels where a camera image would have RGB. The payoff is enormous. A 2D backbone, the same kind of efficient convolutional network the rest of computer vision has spent a decade optimizing, now does the heavy lifting, and there is no expensive 3D convolution anywhere in the pipeline. The bird's-eye-view output feeds naturally into the shared BEV representation developed in Chapter 43.
The cost of pillars is vertical resolution: by collapsing the \(z\) axis into learned channels rather than explicit cells, the network must infer height structure from the pooled features instead of reading it off a grid. For road scenes, where objects sit on a roughly flat ground plane and their footprint is highly discriminative, this loss barely hurts. For scenes with rich vertical stacking, or for tasks needing fine height detail, keeping explicit voxels pays off. That is the whole voxel-versus-pillar decision in one sentence: pillars win on speed and simplicity; voxels win when vertical geometry carries signal.
import numpy as np
def scatter_to_pillars(points, x_range=(0, 70), y_range=(-40, 40),
cell=0.16, max_points=32):
"""Bin an (N,4) xyz+intensity cloud into a BEV pillar grid.
Returns a dense (C, H, W) pseudo-image of simple hand-crafted
per-pillar features (a real net replaces these with a PointNet)."""
xs, ys = points[:, 0], points[:, 1]
keep = ((xs >= x_range[0]) & (xs < x_range[1]) &
(ys >= y_range[0]) & (ys < y_range[1]))
pts = points[keep]
gx = ((pts[:, 0] - x_range[0]) / cell).astype(np.int64)
gy = ((pts[:, 1] - y_range[0]) / cell).astype(np.int64)
W = int((x_range[1] - x_range[0]) / cell)
H = int((y_range[1] - y_range[0]) / cell)
flat = gy * W + gx # one id per pillar
C = 4 # count, mean-z, max-z, mean-intensity
img = np.zeros((C, H * W), dtype=np.float32)
order = np.argsort(flat)
flat, pts = flat[order], pts[order]
ids, start = np.unique(flat, return_index=True)
for k, s in zip(ids, start):
e = start[np.searchsorted(start, s) + 1] if s != start[-1] else len(flat)
col = pts[s:e][:max_points] # cap points per pillar
img[0, k] = len(col)
img[1, k] = col[:, 2].mean()
img[2, k] = col[:, 2].max()
img[3, k] = col[:, 3].mean()
return img.reshape(C, H, W)
The code above strips PointPillars to its skeleton so the moving parts are explicit. Real implementations replace the four hand-crafted channels with a learned encoder and augment each point with its offset from the pillar center and from the pillar's mean, which restores some of the sub-cell position the binning discarded. But the control flow, bin horizontally, cap per pillar, scatter to a dense BEV image, is exactly what ships.
Library Shortcut
You never hand-roll the scatter loop above in production. The spconv library provides PointToVoxel, which turns a raw cloud into voxel or pillar tensors (indices, features, and per-cell point counts) in a single fused CUDA call, and OpenPCDet wires that straight into VoxelNet, PointPillars, and their successors behind a config file. What takes roughly 40 lines of careful NumPy indexing here becomes a two-line transform plus a YAML entry, and the library handles the GPU hashing, the deterministic point cap, and the coordinate bookkeeping that quietly corrupts a from-scratch version. Reach for the manual code only to understand the mechanism, then let the library own the hot path.
Practical Example: a delivery robot at a crosswalk
An urban sidewalk-delivery robot runs a 32-beam lidar and must clear intersections without stalling. Its team first tried a fine voxel grid with a 3D convolutional detector; accuracy on parked cars was excellent, but the model ran at 8 Hz on the robot's embedded GPU, too slow to react to a cyclist entering the crossing. Switching to a PointPillars encoder with a 0.16 m pillar footprint moved all convolution into 2D, lifted throughput past 35 Hz, and left latency budget for tracking. The one regression: short children close to the sensor were occasionally missed, because collapsing height merged a small standing figure with the ground return in the same pillar. The fix was not to abandon pillars but to shrink the pillar footprint near the robot and add the point-to-center offset features, recovering the small-object recall without giving back the speed. The representation choice, not the detector head, set the entire operating point.
Range images and choosing among the three
A fourth encoding deserves mention: the range image, which exploits the fact that a spinning lidar naturally produces an organized grid indexed by beam elevation and azimuth. Unprojecting is unnecessary; the sensor already hands you a dense 2D array whose pixels are ranges, so an ordinary 2D convolutional network applies directly and runs very fast. The catch is that objects change apparent size and shape with distance in a range image, and neighboring pixels can be far apart in 3D, which hurts the metric reasoning that bird's-eye-view representations make easy. In practice: reach for pillars as the default for real-time BEV detection on a compute budget; escalate to voxels with sparse convolution when vertical structure or fine localization matters and you can afford the 3D backbone; consider range images when raw latency dominates and your task tolerates their view-dependent geometry; and keep point-native methods (Chapters 42.4) for indoor or object-scale clouds where \(N\) is modest and every point counts.
Research Frontier
The voxel-versus-pillar dichotomy is softening. VoxelNeXt (2023) shows that a fully sparse voxel backbone can predict directly from sparse voxel features without ever forming a dense BEV map, closing much of the speed gap that once favored pillars, and it is the backbone we study in Section 42.3. In parallel, Point Transformer v3 (2024) demonstrates that serializing points along space-filling curves lets a transformer process raw points at voxel-backbone scale, questioning whether an explicit grid is needed at all. The current frontier treats the representation as learnable and hybrid rather than a fixed upstream choice.
Exercise
Consider a detection range of \([0, 70]\) m in \(x\) and \([-40, 40]\) m in \(y\). (a) With a pillar footprint of 0.16 m, how many pillars does the BEV grid contain? (b) A frame returns 90,000 points inside this range; if points were spread uniformly, what fraction of pillars would be occupied? Real lidar is far sparser than uniform, so treat your answer as a generous upper bound and explain why the true occupancy is lower. (c) You halve the pillar size to 0.08 m to sharpen small-object localization. State the two costs this incurs and which one dominates on an embedded GPU. Relate your reasoning to the model-optimization tradeoffs of Chapter 59.
Self-Check
- Name the three properties of a raw point cloud that prevent a standard convolution from consuming it directly, and state how discretization neutralizes each.
- What exactly does PointPillars collapse relative to VoxelNet, and what does the network gain and lose by doing so?
- Why is sparsity, more than resolution, the property that makes dense 3D convolution over a voxel grid impractical for automotive scenes?
What's Next
In Section 42.3, we confront the sparsity we kept flagging head-on. Sparse-convolution backbones such as CenterPoint and VoxelNeXt compute only on occupied cells, skipping the empty 90 percent entirely, which is what makes voxel-based detection fast enough to matter. We will see how the occupied-cell tensors defined in this section become the input to those backbones and how a fully sparse pipeline predicts objects without ever paying for the empty air between them.