17 - Scenes from LiDAR point clouds¶
less.lidar turns airborne, terrestrial or drone LiDAR point clouds into scenes that can be simulated: it extracts the terrain, segments individual trees, reconstructs crown shapes and fills the crowns with leaves. The input is a .las or .laz file and the output a less.Scene.
Install the optional component first:
The plot used in this chapter: als_plot.las (1.6 MB, a stand of about 123 m × 108 m). Download it into your working folder.
Workflow¶
| Step | Function | What it does |
|---|---|---|
| 1 | load_las |
Reads the point cloud and converts it to LESS coordinates |
| 2 | ground_filter |
Cloth-simulation filtering to separate ground and vegetation points |
| 3 | make_rasters |
Digital elevation, surface and canopy height models |
| 4 | segment_trees |
Watershed segmentation of individual trees on the canopy height model |
| 5 | reconstruct_crowns |
Crown shapes |
| 6 | populate_leaves |
Leaves inside the crowns |
| 6b | populate_understory |
An understorey layer (optional) |
from_las() runs all steps at once; they can also be called one by one.
Simplest use¶
import less
from less import lidar
scene = lidar.from_las("als_plot.las", lai=2.0, leaf_area=0.2)
scene.illumination = less.Illumination(
source=less.Sun(zenith=30, azimuth=180), atmosphere=less.NoAtmosphere())
sensor = less.OpticalImager(less.Orthographic(resolution=0.5),
bands=[550, 650, 850], quality=32)
brf = scene.simulate(sensor).to_brf()
brf.save("reconstructed.png", bands=[2, 1, 0]) # near-infrared, red, green composite
Orthographic(resolution=0.5) images at a ground resolution of 0.5 m; the number of pixels follows from the scene extent. The generated leaf geometry is saved in a <point cloud name>_lidar/ folder by default.
Before simulating, scene.show() lets you check in the viewer whether terrain, crowns and leaves look reasonable (see chapter 6).
Main parameters¶
Leaf area and leaves¶
scene = lidar.from_las(
"als_plot.las",
lai=2.0, # leaf area index of the whole scene, gaps included
leaf_area=0.05, # area of one leaf (m²)
leaf_shape="square", # "square" (two triangles) or "disk" (12 triangles)
leaf_angle="spherical",
)
Leaf area is given in one of three ways: lai (whole scene), lad (leaf area density inside every tree, m² m⁻³) or lai_per_tree (per tree ID). Large scenes with a high leaf area index produce millions of leaves; square leaves with a larger leaf_area (0.05–0.1 m²) greatly reduce the number of triangles.
The target total leaf area is leaf area index × scene area. Because the number of leaves is an integer and some leaves may fall outside the crowns and be dropped, check the actual value with scene.measure(less.LAIMeasurement()).
Crown shape and leaf positions¶
method sets the crown shape and sampling where the leaves go:
method |
Description | Use |
|---|---|---|
convex_hull (default) |
Convex hull of each tree | Sparse airborne data (below 5 points per m²) |
alpha_shape |
Tetrahedra filtered by an alpha threshold | Dense airborne or terrestrial data; captures concave crowns |
voxel |
3-D voxels | When leaf area density should be controlled per voxel |
sampling |
Leaf positions |
|---|---|
density (default) |
Randomly chosen points of the tree, with Gaussian jitter jitter_sigma |
uniform |
Uniform within the crown shape |
With the default density sampling the leaves follow the point cloud, and method only affects the crown volume and hence the number of leaves; with uniform, the crown shape directly determines where leaves are.
Understorey¶
scene = lidar.from_las(
"als_plot.las", lai=2.0, leaf_area=0.2,
understory_lai=0.3, # 0 means none
understory_height_range=(0.3, 2.0), # height above ground (m)
understory_leaf_area=0.02,
understory_leaf_angle="planophile",
)
The understorey is a separate object whose leaf density follows the low LiDAR returns.
Terrain¶
normalize=False (default) keeps the real terrain. normalize=True subtracts the ground elevation from every point and uses flat ground, which suits plot simulations where relief does not matter.
Caching¶
cache=True stores the point classification, rasters, segmentation and crown reconstruction. When only the leaf area, leaf spectra or leaf angles change afterwards, these are reused; when the point cloud or reconstruction parameters change, they are recomputed automatically.
Defaults¶
| Parameter | Default | Meaning |
|---|---|---|
resolution |
0.5 | Raster resolution (m) |
method |
"convex_hull" |
Crown shape |
min_tree_height |
2.0 | Canopy height threshold (m) |
min_tree_distance |
3.0 | Minimum horizontal distance between tree tops (m) |
leaf_area |
0.01 | Area of one leaf (m²) |
leaf_shape |
"square" |
Leaf shape |
leaf_angle |
"spherical" |
Leaf angle distribution |
understory_lai |
0.0 | No understorey |
normalize |
False |
Keep the terrain |
cache |
False |
No caching |
write_obj |
False |
Save binary geometry only, no OBJ |
output_mode |
"merge" |
All leaves in one object; "groups" gives one group per tree |
Step by step¶
Calling the steps separately lets you keep intermediate results, for example save the crowns once and try several leaf areas:
import numpy as np
points = lidar.load_las("als_plot.las")
points = lidar.ground_filter(points)
rasters = lidar.make_rasters(points, resolution=0.5)
segments = lidar.segment_trees(rasters, min_height=2.0, min_distance=3.0)
crowns = lidar.reconstruct_crowns(points, segments, method="convex_hull",
rasters=rasters, min_canopy_height=2.0)
crowns.save("plot.crowns")
crowns = lidar.Crowns.load("plot.crowns")
forest, positions = lidar.populate_leaves(crowns, lai=2.0, leaf_area=0.2, out_path="forest.obj")
scene = less.Scene()
scene.size = points.extent
scene.terrain = less.Terrain(property=less.SpectrumDB("dark_soil_mollisol"),
dem=rasters.dem.astype(np.float32))
scene.illumination = less.Illumination(
source=less.Sun(zenith=30, azimuth=180), atmosphere=less.NoAtmosphere())
scene.add(forest, positions=positions)
reconstruct_crowns needs rasters to remove points below the canopy.
Changing spectra¶
The leaf and ground spectra of a reconstructed scene can be changed without reconstructing:
scene = lidar.from_las("als_plot.las", lai=1.5, leaf_area=0.2, normalize=True, cache=True)
scene.illumination = less.Illumination(
source=less.Sun(zenith=45, azimuth=180), atmosphere=less.NoAtmosphere())
baseline = scene.simulate(sensor).to_brf()
baseline.save("plot_baseline.png", bands=[2, 1, 0])
scene.objects["forest"].set_property("leaves", less.SpectrumDB("larch_leaf_green"))
scene.terrain.set_property(less.SpectrumDB("loam_brown"))
larch = scene.simulate(sensor).to_brf()
larch.save("plot_larch.png", bands=[2, 1, 0])
project = less.Project(scene=scene, title="Plot 01")
project.add_sensor(sensor, name="Nadir multispectral")
project.save_directory("plot01.less")
Scope¶
- Only leaves are reconstructed; no stems or branches are generated;
- the leaf area must be supplied by the user and is not retrieved from the point cloud;
- the understorey is a single layer, not individual shrubs.