Heightmap building blocks¶
Three pieces, usable either standalone or as the lower half of
TerrainPipeline:
HeightMapBuilder— rasterize(N, 3)points into grid layers.multigrid_inpaint— fill NaN cells via Laplace diffusion.gaussian_smooth— NaN-aware separable blur.
All three accept np.ndarray or wp.array and return the matching type.
HeightMapBuilder¶
One kernel pass over the point cloud populates four grids in parallel:
max— highest z per cell (useful for top-surface terrain)mean— average z per cellmin— lowest z per cellcount— int32, number of points that landed in the cell
Empty cells are NaN (or 0 for count).
from terrain_toolkit import HeightMapBuilder
builder = HeightMapBuilder(
resolution=0.1, # meters per cell
bounds=(-5.0, 5.0, -5.0, 5.0), # (xmin, xmax, ymin, ymax)
)
layers = builder.build(points) # → HeightMapLayers
# layers.max, layers.mean, layers.min, layers.count
Internally the builder uses float atomics for mean accumulation and compare-
and-swap for max/min. Grid dimensions are derived from bounds /
resolution.
multigrid_inpaint¶
Fills NaN cells via Laplace diffusion on a pyramid. The algorithm:
- Build a pyramid by 2×2 NaN-aware downsampling until the smallest side
reaches
min_size. - Solve the coarsest level with
coarse_itersJacobi iterations. - Upsample and run
iters_per_levelrefinement iterations at each finer level.
Each level's iteration loop is CUDA-graph-captured, so Python launch overhead is amortized.
from terrain_toolkit import multigrid_inpaint
filled = multigrid_inpaint(
heightmap,
iters_per_level=50, # default
coarse_iters=200, # default
min_size=8, # don't pyramid below this dimension
)
Tuning¶
The defaults (50 / 200) are conservative for small grids. For typical lidar
grids (100×100 to 300×300 cells) you can drop to (20, 80) without visible
quality change. The coarse level is usually ~6×10 — 200 iters there is
overkill.
If inpaint is disabled on TerrainPipeline, the primary layer still flows
through but NaN cells remain, and traversability cannot be computed (the
cost kernels assume a fully filled grid — the constructor enforces this).
gaussian_smooth¶
Separable NaN-aware Gaussian blur. sigma is in cells, not meters:
from terrain_toolkit import gaussian_smooth
blurred = gaussian_smooth(heightmap, sigma=1.0, truncate=3.0)
sigma <= 0returns a fresh copy (no kernel launches).- Kernel radius is
ceil(truncate * sigma)cells. - NaN cells contribute 0 weight and 0 value, so the output NaN-hole pattern
shrinks by
radiuscells (a partially-inside window still produces a value).
diffuse_inpaint (single-resolution variant)¶
Same idea as multigrid_inpaint but without the pyramid — just Jacobi
iterations at full resolution. Simpler, slower to converge. Exposed as
terrain_toolkit.diffuse_inpaint. Use the multigrid version unless you have a
reason not to.
When to skip the pipeline and use these directly¶
- You want only a heightmap, no traversability cost.
- You're running several pipelines with different configs over the same input (build once, analyze many).
- You're prototyping a custom cost function and want to reuse the raster + inpaint stages.