Skip to content

12 - Statistical vegetation

When a tree has hundreds of thousands of leaves, or when the subject is a large canopy, modelling every leaf is neither practical nor necessary. Statistical vegetation only describes the space the canopy occupies; the leaves inside are described statistically by leaf area density, leaf angle distribution and leaf optics. Rays are intercepted, scattered and absorbed inside the canopy according to these statistics.

Representation Use
Triangle mesh When leaf shape, clumping and other geometric detail matter
Statistical vegetation Very many leaves; interest in canopy-scale radiation and physiology
Both together Mesh trunks and branches, statistical foliage

Canopy boundary

The extent of statistical vegetation is given by a closed triangle mesh, usually the outline of the crown:

import less

crown = less.TurbidBoundary(less.asset_path("tutorial_tree.obj"), group="crown")

The boundary mesh must:

  • enclose a closed, non-self-intersecting volume;
  • have consistently oriented faces, with no openings, duplicate faces or zero-area triangles.

The boundary itself neither reflects nor absorbs; it only marks where the statistical leaves are.

Leaves inside the canopy

OpticalVegetation describes the leaves inside the boundary:

vegetation = less.OpticalVegetation(
    leaf=less.Prospect(cab=40, car=10),
    leaf_area_density=1.5,               # m² leaf per m³ canopy
    leaf_angle_distribution="spherical",
    leaf_size=(0.08, 0.04),              # leaf length and width (m)
    leaf_shape="ellipse",
)
Parameter Meaning
leaf Optical property of the representative leaf: Prospect, Fluspect, MeasuredSpectrum …
leaf_area_density One-sided leaf area per unit canopy volume (m² m⁻³)
leaf_angle_distribution Leaf angle distribution
leaf_size, leaf_shape Actual leaf size and shape ("ellipse" or "rectangle"), which set the width of the hotspot

Leaf angle distributions:

Name Character
spherical Leaf normals evenly distributed over all directions; common when no measurements exist
uniform Inclination uniform between 0° and 90°
planophile Mostly horizontal leaves
erectophile Mostly vertical leaves
plagiophile Mostly intermediate inclinations
extremophile Many horizontal and many vertical leaves

Without leaf_size the leaves are treated as infinitely small and there is no hotspot.

A statistical crown on a mesh trunk

tutorial_tree.obj contains a closed crown crown and a trunk trunk. The crown becomes statistical vegetation and the trunk stays a mesh:

tree = less.Object("tree")
tree.add_component("crown", crown)
tree.add_component("trunk", less.Mesh(less.asset_path("tutorial_tree.obj"), group="trunk"))
tree.set_property("crown", vegetation)
tree.set_property("trunk", less.Lambertian(reflectance=0.12))

scene = less.Scene()
scene.size = (20.0, 20.0)
scene.terrain = less.Terrain(property=less.Lambertian(reflectance=0.15))
scene.illumination = less.Illumination(
    source=less.Sun(zenith=30, azimuth=150),
    atmosphere=less.SimpleSpectralAtmosphere(turbidity=2.5))
scene.add(tree, positions=[(5, 5, 0), (10, 12, 0), (15, 6, 0)])

image = scene.simulate(less.OpticalImager(
    less.Orthographic(image_size=128), bands=[550, 680, 850], quality=64))
brf = image.to_brf()
brf.save("turbid_brf.tif")

A single ray interacts with trunk surfaces, statistical leaves and the ground in turn, and multiple scattering is handled in one simulation. Statistical vegetation works with every sensor and process in this tutorial, exactly like meshes.

Leaf area

The leaf area of statistical vegetation is the volume inside the boundary times the leaf area density:

print(scene.measure(less.LAIMeasurement(group_by="component")))
print(scene.measure(less.LAIMeasurement(property_filter=less.Prospect)))

property_filter=less.Prospect matches both mesh leaves using Prospect and statistical vegetation whose representative leaf is Prospect. Scaling a boundary up increases its volume and hence its leaf area. Parts of a boundary outside the scene are not counted; in a periodic scene, parts crossing the edge are wrapped back into the scene.

A whole canopy layer

A box-shaped boundary covering the whole scene, combined with periodic boundaries, gives a horizontally uniform, infinite canopy, the same assumption as in one-dimensional canopy models:

from pathlib import Path

def write_box(path, width, height):
    """Write a width × width × height box in OBJ coordinates (Y up)."""
    v = [(0, 0, 0), (-width, 0, 0), (-width, height, 0), (0, height, 0),
         (0, 0, -width), (-width, 0, -width), (-width, height, -width), (0, height, -width)]
    faces = [(1, 4, 3), (1, 3, 2), (5, 6, 7), (5, 7, 8), (1, 5, 8), (1, 8, 4),
             (2, 3, 7), (2, 7, 6), (1, 2, 6), (1, 6, 5), (4, 8, 7), (4, 7, 3)]
    Path(path).write_text("g canopy\n" + "".join(f"v {x} {y} {z}\n" for x, y, z in v)
                          + "".join("f %d %d %d\n" % f for f in faces))

write_box("canopy_box.obj", 1.0, 2.0)

canopy = less.Object("canopy")
canopy.add_component("leaves", less.TurbidBoundary("canopy_box.obj", group="canopy"))
canopy.set_property("leaves", less.OpticalVegetation(
    leaf=less.Prospect(cab=40), leaf_area_density=1.5, leaf_angle_distribution="spherical"))

field = less.Scene()
field.size = (1.0, 1.0)
field.repetitive = True
field.terrain = less.Terrain(property=less.Lambertian(reflectance=0.15))
field.illumination = less.Illumination(
    source=less.Sun(zenith=30, azimuth=180), atmosphere=less.NoAtmosphere())
field.add(canopy, positions=[(0.0, 0.0, 0.0)])

print("LAI:", field.measure(less.LAIMeasurement()))      # 1.5 × 2 = 3
field_brf = field.simulate(less.BRFSensor(
    bands=[650, 850], directions=[(0, 0), (30, 180), (30, 0)], num_photons=500_000))
print(field_brf.brf)

The canopy is 2 m tall with a leaf area density of 1.5 m² m⁻³, so the LAI is 3.

Sampling settings

Optical imaging only needs the number of rays, controlled by quality, num_photons and similar parameters. The radiation field, energy balance, photosynthesis and fluorescence additionally place statistical leaf samples inside the canopy to record leaf absorption, temperature and physiological state. The number and distribution of these samples are set by TurbidSampling:

crown = less.TurbidBoundary(
    less.asset_path("tutorial_tree.obj"),
    group="crown",
    sampling=less.TurbidSampling(quality="standard", seed=7),
)
Parameter Role
quality "preview", "standard" or "high"
sample_budget Target number of samples per instance; defaults to the quality preset
seed Random seed; the same seed and settings give the same samples
orientation_theta_bins, orientation_phi_bins Number of discrete leaf orientations
max_samples_per_leaf_cell Upper limit of samples per spatial cell

Check a scene with "preview" first, then compute final results with "standard" or "high" and compare the two to confirm that the results are stable. These processes are covered in chapters 31–33.

Next