Skip to content

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:

pip install "less3d[lidar]"

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

Reconstruction 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.

Next