10 - BRDF Simulation¶
This chapter describes how to use LESS to perform bidirectional reflectivity (BRF/BRDF) simulations to analyze the directional reflection characteristics of the earth's surface.
What is BRDF?¶
Bidirectional Reflection Distribution Function (BRDF) describes the reflection characteristics of the surface for different incident directions and different observation directions. In remote sensing, it is often characterized using the Bidirectional Reflectance Factor (BRF) - it is the ratio of surface reflectance to an ideal Lambertian body under a specific geometric condition.
Changes in BRDF are affected by the three-dimensional vegetation structure:
- Hot Spot: When the observation direction coincides with the direction of the sun, the reflectivity increases significantly (because no shadow is visible at this time)
- Bowl Shape: Vegetation canopies are usually more reflective at large viewing zenith angles
- Forward/Backward Scattering: The scattering properties of the blades cause forward and backward asymmetry
BRFSensor - Results from multiple directions at once¶
BRFSensor is the sensor of choice for BRF simulation:
- Transmit a set of directions, and run all the simulations in a single time (forward photon tracking, all directions share the same batch of photons, high efficiency)
- No images are displayed, only the average BRF value of the scene in each direction is displayed.
- Does not rely on the
scene.sizecamera layout, and there is no geometric boundary problem for large-angle observations
import less
sensor = less.BRFSensor(
bands=[650, 850],
directions=[(0, 0), (30, 180), (60, 0)], # (zenith°, azimuth°) list
num_photons=50_000_000, # The default is 5e7; if the noise is too loud, add it.
)
What is returned is BRFProduct:
| Properties | Shape | Description |
|---|---|---|
.brf |
[n_dirs, n_bands] |
BRF in each direction and each band |
.directions |
list | Input (z, a) list |
.wavelengths |
[n_bands] |
Band center wavelength (nm) |
Multi-angle BRF simulation¶
Construct a list of observation directions (main plane/full hemisphere is acceptable), and you can get the complete BRDF surface in one simulate().
import less
import numpy as np
# ──Constructing the scene───────────────────────────────────────────────
scene = less.Scene()
scene.size = 10.0
scene.repetitive = False # Limited scene (default); periodic tiling is limited to flat homogeneous terrain
scene.terrain = less.Terrain(property=less.Lambertian(reflectance=0.15))
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=180), atmosphere=less.NoAtmosphere())
maize = less.Object("maize", mesh=less.examples.asset_path("maize.obj"))
maize.set_property(less.Prospect(cab=45, car=10, cw=0.012, cm=0.006, N=1.55))
rng = np.random.RandomState(42)
positions = less.place.grid(scene, spacing=0.75, jitter=0.04)
n = len(positions)
scene.add(maize, positions=positions,
scales=rng.uniform(0.9, 1.1, n),
rotations=rng.uniform(0, 360, n))
# Sun azimuth angle = 180° (south), so the main plane is north-south
# - Backscatter: Azimuth 180° (same side)
# - Forward scatter: azimuth 0° / 360° (opposite side)
view_zeniths = np.arange(0, 65, 5)
directions = ([(z, 180) for z in view_zeniths] # backward
+ [(z, 0) for z in view_zeniths[1:]]) # forward (skip z=0 dup)
# ── Get all directions in a single simulation ──────────────────────────────
result = scene.simulate(less.BRFSensor(
bands=[650, 850],
directions=directions,
num_photons=50_000_000,
))
print("Main plane BRF (SZA=30°, SAA=180°)")
print(f"{'VZA':>6s} {'AZ':>4s} {'Red(650)':>10s} {'NIR(850)':>10s}")
for (z, a), (r, n) in zip(result.directions, result.brf):
signed_vza = z if a == 180 else -z # Common habits for the main plane: negative = forward
mark = " ← hot spot" if a == 180 and abs(z - 30) < 3 else ""
print(f"{signed_vza:>6d} {a:>4d} {r:>10.4f} {n:>10.4f}{mark}")
# Polar coordinates/principal plane curves can be solved in one line
result.show(plane="principal", style="line") # matplotlib pop-up chart
result.save("plot_brf.csv") # Can also export CSV / JSON / NPY
Supporting script:
scripts/10_brdf.py
Code interpretation¶
Two common modes for direction lists¶
# Principal plane (VZA scan)
directions = [(z, 180) for z in range(0, 65, 5)] \
+ [(z, 0) for z in range(5, 65, 5)]
# Full Hemisphere (VZA × VAA grid) – for polar plots
import itertools
directions = list(itertools.product(
range(0, 75, 10), # zeniths
range(0, 360, 30), # azimuths
))
Principal Plane¶
The principal plane is the plane containing the sun direction and the normal direction. In this plane:
- Backscattering direction: The observer is on the same side of the sun (observation azimuth = solar azimuth)
- Forward scattering direction: The observer is on the opposite side of the sun (observation azimuth = solar azimuth + 180°)
- Hotspot: Observation zenith angle = solar zenith angle, and in the backscattering direction
The importance of scene repetition¶
Periodic BRF datums typically use scene.repetitive=True to represent an infinitely tiled flat homogeneous plot. This tutorial uses a finite scenario, so the results include plot edge effects; the same boundary conditions should be maintained when comparing observations or other models.
BRFSensor vs OpticalImager¶
| What you want | Who to use |
|---|---|
| BRF scalar values in a set of directions (typical BRF / BRDF curve) | BRFSensor |
| High-resolution two-dimensional image in a certain direction (care about spatial texture/shadow) | OpticalImager + Orthographic(view_zenith=…) |
BRFSensor uses forward photon tracking, all directions share the same batch of photons, and running in N directions ≈ time-consuming in one direction; OpticalImager has to run again in each direction.
RPV BRDF model¶
In addition to simulating BRF through 3D scenes, LESS also supports the direct use of parametric BRDF models. The RPV model can be used for terrestrial BRDF:
# RPV model parameters
scene.terrain = less.Terrain(
property=less.RPV(rho0=0.08, k=0.7, theta=-0.15)
)
Parameter meaning:
| Parameters | Meaning | Range |
|---|---|---|
rho0 |
Reflectivity magnitude | 0-1 |
k |
Shape parameters: <1 bowl-shaped (brighter at large angles), >1 bell-shaped (brighter vertically) | 0-2 |
theta |
Scattering asymmetry: <0 backscattering enhancement, >0 forward scattering enhancement | -1 ~ 1 |
Custom visualization¶
result.show() defaults to the main plane line drawing. To draw a hemispheric polar plot (VZA × VAA grid), you need to organize the data yourself:
import matplotlib.pyplot as plt
import numpy as np
# directions = [(z, a), ...], assuming a regular grid
zeniths = sorted({z for z, _ in result.directions})
azimuths = sorted({a for _, a in result.directions})
brf_grid = np.zeros((len(azimuths), len(zeniths)))
for (z, a), val in zip(result.directions, result.brf[:, 0]): # Band 0
brf_grid[azimuths.index(a), zeniths.index(z)] = val
fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(7, 7))
ax.set_theta_zero_location('N'); ax.set_theta_direction(-1)
theta = np.deg2rad(azimuths)
r = zeniths
ax.contourf(theta, r, brf_grid.T, levels=20, cmap='RdYlGn')
ax.set_title(f"BRF @ {result.wavelengths[0]:.0f} nm")
plt.savefig("brf_polar.png", dpi=150, bbox_inches='tight')
Application scenarios¶
| Application | Description |
|---|---|
| Albedo estimation | Integrate BRF to obtain hemispheric albedo |
| Multi-angle remote sensing | Simulate MISR, POLDER and other multi-angle sensors |
| BRDF Calibration | Evaluate BRDF parametric model accuracy |
| Structural parameter inversion | Using BRF anisotropy to invert canopy structure |
Next step¶
Related API¶
less.BRFSensor、less.Scene.simulate()less.RPV、less.Prospect、less.Lambertianless.place.grid()、less.place.poisson_disk()less.BRFProduct.save()、less.BRFProduct.plot_principal_plane()