Interpolation¶
sylva.geo.interpolate moves values between point clouds and rasters:
| Function | From | To | Methods |
|---|---|---|---|
transfer_attributes(source, target, names) |
attributes of one cloud | points of another | "nearest", "idw", "majority" |
grid(cloud, resolution, value) |
z or an attribute | a Raster |
"idw", "tin", "natural" |
sample_raster(cloud, raster, name) |
a Raster |
a new attribute | "bilinear", "nearest" |
sample_rasters(cloud, {name: raster}) |
several rasters | one attribute each | as above |
All of them run in the Rust core in parallel, and their results do not depend on the number of threads.
Worked example¶
The example tile (docs/examples/data/litch_tile.laz, 1.55 million points
with a ground classification) is thinned, labelled on the thinned copy, and
the labels are carried back to every original point. The ground points are
then gridded into a DTM that is read back at every point.
import sylva
from sylva import filters, ground
from sylva.geo import interpolate
cloud = sylva.read("docs/examples/data/litch_tile.laz")
thin = filters.voxel_downsample(cloud, 0.2) # 92,061 points
# Any per-point result computed on `thin` (here its own classification)
# goes back to the full-resolution cloud.
full = interpolate.transfer_attributes(thin, cloud, "classification",
method="majority", k=5)
# A DTM by linear interpolation on a triangulation of the (thinned) ground.
g = filters.voxel_downsample(cloud[ground.ground_mask(cloud)], 0.25)
dtm = interpolate.grid(g, 0.25, method="tin") # 81 x 81 cells, NaN outside the hull
# Terrain elevation at every point, and height above it.
cloud = interpolate.sample_raster(cloud, dtm, "ground")
height = cloud.z - cloud.attrs["ground"]
On this tile the majority vote over five neighbours restores the original class of 97.7 % of the points; the remainder lie at the boundary between ground and low vegetation, where the thinned cloud cannot tell them apart. The transfer takes about 0.2 s on eight cores; twenty million target points from a one-million-point source take a few seconds.
Attributes between clouds¶
transfer_attributes builds a k-d tree on the source and processes every
target point independently, so the target can be very large. Distances are
3D. The methods are:
"nearest": the value of the nearest source point. Works for any dtype, including strings, and keeps it."idw": the inverse-distance weighted mean over theknearest source points, weights1 / d**power. A target that coincides with source points takes the mean of their values, and non-finite source values are skipped. Numeric attributes only; the result is float64 (float32 for float32 attributes)."majority": the most common value among theknearest source points, with ties going to the value of the nearest one. This is the method for labels (tree_id,classification, a wood mask), which must not be averaged, and it smooths isolated mislabelled points that"nearest"would copy.
max_distance limits the neighbours to those within that distance. A target
with none gets the fill value: NaN for floating-point results and -1 for
integer and boolean labels, or whatever fill is given. When the fill does
not fit the attribute's dtype (-1 in a uint8 classification), the result
is widened to the smallest signed type that holds both, so pass fill=0 to
keep uint8 for LAS output:
labels = interpolate.transfer_attributes(thin, cloud, ["tree_id", "classification"],
method="majority", k=5, max_distance=0.1)
# tree_id: int64, -1 beyond 10 cm; classification: int16, -1 beyond 10 cm
las_ready = interpolate.transfer_attributes(thin, cloud, "classification",
method="majority", max_distance=0.1, fill=0)
# classification: uint8, 0 (never classified) beyond 10 cm
Points to rasters¶
grid interpolates "z" or a numeric attribute at the cell centres of a
grid laid out as ground.make_dtm lays it out: row 0 at ymin, and without
bounds a south-west corner snapped to a multiple of the resolution. Points
sharing an x, y are merged into one carrying their mean value.
| Method | Surface | Outside the data |
|---|---|---|
"idw" |
weighted mean of the k nearest points in x, y; stays within the range of the data; flat spots at the points for power above 1 |
every cell gets a value unless max_distance is set |
"tin" |
linear on each Delaunay triangle; exact for a plane; creases along triangle edges | NaN outside the convex hull |
"natural" |
natural-neighbour (Sibson) weights; exact for a linear function, smooth except at the data points | NaN outside the convex hull |
max_distance sets cells whose centre is farther than that from every point
to NaN, for every method, which keeps interpolation from bridging large
gaps such as the shadow behind a tree.
The triangulation and the natural-neighbour weights come from the spade crate. A TIN of two million points over a million cells takes about three seconds on eight cores.
DTMs by interpolation¶
ground.make_dtm keeps its default, the lowest ground point per cell with
empty cells filled, and accepts method="tin", "natural" or "idw" to
interpolate the ground points instead. Those surfaces pass through every
ground point, so they sit on the mean of the ground returns rather than
their minimum (on the example tile, a median of 5 cm above the default
DTM), and they carry the noise of the returns unless the ground is thinned
first. make_dtm fills cells outside the convex hull of the ground points
from the nearest interpolated cell, so the DTM has no NaN like the default;
call interpolate.grid on the ground points to keep them NaN.
g = filters.voxel_downsample(cloud[ground.ground_mask(cloud)], 0.25)
dtm = ground.make_dtm(g, 0.25, method="natural")
Rasters onto points¶
sample_raster adds the raster value at each point's x, y as a float64
attribute. "bilinear" interpolates between the four surrounding cell
centres, exactly as Raster.sample, and gives NaN if one of them is NaN;
"nearest" takes the cell containing the point. Points outside the raster
get NaN, unlike ground.normalize_height, which extends the DTM's edge
values. sample_rasters does the same for several rasters at once:
chm = ground.make_chm(ground.normalize_height(cloud, ground.make_dtm(cloud)), 0.5)
cloud = interpolate.sample_rasters(cloud, {"ground": dtm, "canopy_height": chm},
method="nearest")