Skip to content

11 - LiDAR Point Cloud Simulation

This chapter describes how to use LESS to simulate airborne LiDAR (ALS) and terrestrial LiDAR (TLS) point cloud data.

LiDAR Simulation Principle

LiDAR (Light Detection and Ranging) acquires three-dimensional spatial information by emitting laser pulses and recording echo signals. The process of LESS simulation is:

  1. Calculate the emission position and direction of each laser pulse based on the scanner parameters and platform trajectory.
  2. Calculate the surface intersection point for Mesh, and calculate the laser propagation interval in the medium for TurbidBoundary
  3. Calculate pulse broadening, beam divergence, volume scattering, path extinction and multiple echoes
  4. Output the three-dimensional point cloud and retain the complete waveform.

Mesh and statistical vegetation

LiDAR uses the geometric and optical properties already in the scene, eliminating the need to create another set of canopies. explicit blade usage Mesh; statistical canopy uses TurbidBoundary and OpticalVegetation:

tree.add_component(
    "crown",
    less.TurbidBoundary("tree.obj", group="crown"),
)
tree.set_property(
    "crown",
    less.OpticalVegetation(
        leaf=less.Lambertian(
            reflectance=0.35,
            transmittance=0.15,
        ),
        leaf_area_density=1.5,
        leaf_angle_distribution="spherical",
    ),
)

The echoes from the statistical canopy are formed by backscattering from the leaves distributed continuously along the laser path. Ground and Mesh echoes in Attenuation also occurs when passing through the tree canopy. The transmit path of single station LiDAR coincides with the receive path, so the model uses Correlated propagation probabilities in the same gap, instead of treating uplink and downlink occlusion as two independent events. This processing corresponds to The occlusion hiding effect at zero phase angle does not require the user to set the empirical hotspot factor.

Mesh and TurbidBoundary can appear in the same scene or the same Object. Three backends Use the same statistical canopy waveform definition.

Three components of the LiDAR system

LESS's LiDAR simulation consists of three components:

LiDARSurvey = Scanner + Platform + Legs/Positions

Scanner ——Scanner parameters

import less

scanner = less.Scanner(
    beam_divergence=0.5,     # Beam divergence angle (mrad)
    pulse_rate=200_000,      # Pulse repetition frequency (Hz)
    scan_freq=80.0,          # Scan frequency (Hz)
    scan_angle=27.0,         # Half scan angle (°)
    wavelengths=[1064],      # Laser wavelength (nm)
    beam_samples=8,          # Number of beam samples (analog beam divergence)
)
Parameters Meaning ALS typical values ​​ TLS typical values ​​
beam_divergence Beam divergence angle (mrad) 0.3-0.5 0.1-0.3
pulse_rate Pulse frequency (Hz) 100k-400k -
scan_freq Scan frequency (Hz) 50-200 -
scan_angle Half scan angle (°) 15-30 -
wavelengths Laser wavelength (nm) 1064 1550
beam_samples Number of beam sampling points 4-16 1-4
angular_resolution TLS angular resolution (°) - 0.05-0.2

Platform ——Platform type

# drone
platform = less.UAV(altitude=50.0, speed=5.0)

# manned aircraft
platform = less.Aircraft(altitude=1000.0, speed=60.0)

# Ground tripod
platform = less.Tripod(height=1.5)

# Vehicle mounted
platform = less.Vehicle(height=2.5, speed=5.0)

Legs/Positions ——Flight path or scan position

# ALS: Define flight route (start point → end point)
legs = [
    less.Waypoint(-10, 25, active=True),       # starting point
    less.Waypoint(60, 25, active=True),         # end
]

# TLS: Define scan sites
legs = [
    less.ScanPosition(
        x=25, y=25,                             # site location
        azimuth_range=(0, 360),                  # Horizontal scan range
        zenith_range=(5, 80),                    # Vertical scanning range
    ),
]

Complete example: Airborne LiDAR (ALS)

import less
import numpy as np

# ──Constructing a forest scene───────────────────────────────────────────
scene = less.Scene()
scene.size = 50.0
scene.repetitive = False  # Neither the cycle nor the LiDAR workflow is currently publicly released

scene.terrain = less.Terrain(property=less.Lambertian(reflectance=0.15))
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), atmosphere=less.NoAtmosphere())

tree = less.Object("ash", mesh=less.examples.asset_path("FREX.obj"))
tree.set_property("leaves", less.Prospect(cab=40, car=10, cw=0.012, cm=0.008, N=1.5))
tree.set_property("stem_branch", less.Lambertian(reflectance=0.08))

rng = np.random.RandomState(42)
positions = less.place.random(scene, n=60, min_dist=3.0, seed=42)
n = len(positions)
scene.add(tree, positions=positions,
          scales=less.place.uniform_scale(n, 0.6, 1.5, seed=42),
          rotations=less.place.random_rotation(n, seed=42))
sensor_als = less.LiDARSurvey(
    scanner=less.Scanner(
        beam_divergence=0.5,
        pulse_rate=200_000,
        scan_freq=80.0,
        scan_angle=27.0,
        wavelengths=[1064],
        beam_samples=8,
    ),
    platform=less.UAV(altitude=50.0, speed=5.0),
    legs=[
        less.Waypoint(-10, 25, active=True),
        less.Waypoint(60, 25, active=True),
    ],
    range_resolution=0.15,     # Distance resolution (m)
    name="ALS LiDAR",
)

# ──Simulation──────────────────────────────────────────────────
print("Start ALS simulation...")
pointcloud = scene.simulate(sensor_als)

# ──Save point cloud ─────────────────────────────────────────────
pointcloud.save("11_als_pointcloud.ply")
print(f"Point cloud size: {pointcloud.num_points} points")
print(f"X range: {pointcloud.points[:, 0].min():.1f} ~ {pointcloud.points[:, 0].max():.1f} m")
print(f"Y range: {pointcloud.points[:, 1].min():.1f} ~ {pointcloud.points[:, 1].max():.1f} m")
print(f"Z range: {pointcloud.points[:, 2].min():.1f} ~ {pointcloud.points[:, 2].max():.1f} m")

Supporting script: scripts/11_lidar.py

Ground LiDAR (TLS)

TLS performs a hemispheric scan from a fixed site to obtain a high-density close-range point cloud:

sensor_tls = less.LiDARSurvey(
    scanner=less.Scanner(
        beam_divergence=0.2,
        wavelengths=[1550],
        beam_samples=1,
        angular_resolution=0.1,     # Angular resolution 0.1°
    ),
    platform=less.Tripod(height=1.5),
    legs=[
        less.ScanPosition(
            x=25, y=25,
            azimuth_range=(0, 360),    # All-round scan
            zenith_range=(5, 80),      # 5° ~ 80° (avoiding the zenith and ground)
        ),
    ],
    range_resolution=0.02,     # 2 cm distance resolution
    name="TLS",
)

pointcloud_tls = scene.simulate(sensor_tls)
pointcloud_tls.save("11_tls_pointcloud.ply")

TLS vs ALS comparison

Features ALS (Airborne) TLS (Terrestrial)
Perspective Top-down Bottom-up/horizontal
Coverage area Large (route length) Small (around the site)
Point Density Medium (1-20 pt/m²) High (>100 pt/m²)
Canopy penetration Good Severe shading
Typical applications Large area mapping, CHM Trunk measurement, structural analysis

Point cloud data structure

The PointCloud object contains the following properties:

pc = scene.simulate(sensor_als)

# Coordinates (N × 3)
print(pc.points.shape)       # (N, 3)
print(pc.points[:5])         # xyz of first 5 points

# Points
print(pc.num_points)         # Return total number of points

# strength
print(pc.intensity.shape)    # (N,)

# echo order
print(pc.return_number)      # echo several times
print(pc.num_returns)        # The total number of echoes of this pulse

Get the complete waveform

pc = scene.simulate(sensor_als, product="waveform")

waveform = pc.waveform["data"]
print(waveform.shape)                    # pulse × range bin × wavelength
print(pc.waveform["min_range"])          # The starting point of the first distance window, m
print(pc.waveform["range_resolution"])   # Distance sampling interval, m
print(pc.waveform["wavelengths"])        # Laser wavelength, nm

point_cloud and waveform use the same set of propagation calculations. Discrete echoes are generated from the merged surface with Extracted from the body scattering waveform, therefore the canopy echo, understory echo and ground echo in the point cloud are consistent with the saved waveform Be consistent.

\1 Multiple route scan

Real airborne LiDAR missions typically include multiple parallel routes:

# Three parallel routes, 20m apart
legs = []
for y_offset in [15, 25, 35]:
    legs.extend([
        less.Waypoint(-10, y_offset, active=True),
        less.Waypoint(60, y_offset, active=True),
    ])

sensor_multi = less.LiDARSurvey(
    scanner=less.Scanner(
        beam_divergence=0.5, pulse_rate=200_000,
        scan_freq=80.0, scan_angle=20.0,
        wavelengths=[1064], beam_samples=8,
    ),
    platform=less.UAV(altitude=50.0, speed=5.0),
    legs=legs,
    range_resolution=0.15,
    name="Multi-strip ALS",
)

Application scenarios

Application Required Data Method
Canopy Height Model (CHM) ALS Point Cloud Highest Point - Ground Point
LAI inversion ALS point cloud Gap fraction method
Single wood segmentation ALS/TLS Watershed, deep learning
Diameter at breast height (DBH) TLS Circle fitting
Biomass Estimation ALS Height-Area Statistics

Next step

  • less.Scannerless.LiDARSurvey
  • less.Aircraftless.UAVless.Tripodless.Vehicle
  • less.Waypointless.ScanPosition
  • less.LiDARProduct.save()less.LiDARProduct.waveforms