Skip to content

21 - Bidirectional reflectance

The reflectance of a surface changes with the sun and view directions. A vegetation canopy is brightest when the view direction coincides with the sun direction, because hardly any shadows are visible; this is the hotspot. Dense canopies are also usually brighter at large view angles than at nadir. The bidirectional reflectance factor (BRF) is the ratio of the radiance reflected by the surface in a direction to that reflected by an ideal Lambertian white panel under the same illumination.

The multi-angle reflectance sensor

BRFSensor computes the scene-average reflectance factor for a set of view directions in one run; it does not produce images:

import less

sensor = less.BRFSensor(
    bands=[650, 850],
    directions=[(0, 0), (30, 180), (60, 0)],   # (view zenith, view azimuth), degrees
    num_photons=500_000,
)

Photons are traced from the light sources and all directions share the same photon paths, so many directions are cheap. The result contains:

Attribute Shape Meaning
.brf (directions, bands) Reflectance factor for each direction and band
.directions list View directions
.wavelengths (bands,) Band centres

Principal plane of a maize field

The principal plane is the vertical plane containing the sun direction and the zenith. With the sun at 180° azimuth, directions at 180° azimuth are on the sun's side (backscattering) and directions at 0° on the opposite side (forward scattering). The hotspot lies on the backscattering side, at a view zenith equal to the sun zenith.

import numpy as np

scene = less.Scene()
scene.size = 10.0
scene.repetitive = True       # treat as an infinite uniform field
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.asset_path("maize.obj"))
maize.set_property(less.Prospect(cab=45, car=10, cw=0.012, cm=0.006, N=1.55))
positions = less.place.grid(scene, spacing=0.75, jitter=0.04, seed=42)
rng = np.random.default_rng(42)
scene.add(maize, positions=positions,
          scales=rng.uniform(0.9, 1.1, len(positions)),
          rotations=rng.uniform(0, 360, len(positions)))

zeniths = range(0, 65, 5)
directions = [(z, 180) for z in zeniths] + [(z, 0) for z in zeniths if z > 0]
result = scene.simulate(less.BRFSensor(bands=[650, 850], directions=directions,
                                       num_photons=500_000))

print(" view      red       NIR")
for (zenith, azimuth), (red, nir) in zip(result.directions, result.brf):
    signed = zenith if azimuth == 180 else -zenith      # positive: backward, negative: forward
    print(f"{signed:6.0f}  {red:8.4f}  {nir:8.4f}")

result.save("maize_brf.csv")

result.show(plane="principal", style="line") plots the principal plane with matplotlib. Results can also be saved as .json or .npy.

Hemispherical distribution

Computing on a grid of view zenith and azimuth angles gives the reflectance over the whole hemisphere:

import itertools
import matplotlib.pyplot as plt

zenith_grid = list(range(0, 75, 10))
azimuth_grid = list(range(0, 360, 30))
hemisphere = scene.simulate(less.BRFSensor(
    bands=[850], directions=list(itertools.product(zenith_grid, azimuth_grid)),
    num_photons=500_000))

values = np.zeros((len(azimuth_grid), len(zenith_grid)))
for (zenith, azimuth), brf in zip(hemisphere.directions, hemisphere.brf[:, 0]):
    values[azimuth_grid.index(azimuth), zenith_grid.index(zenith)] = brf

theta = np.deg2rad(azimuth_grid + [360])
values = np.vstack([values, values[:1]])          # close the circle
fig, ax = plt.subplots(subplot_kw={"projection": "polar"}, figsize=(6, 6))
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
mesh = ax.contourf(theta, zenith_grid, values.T, levels=20, cmap="viridis")
fig.colorbar(mesh, label="BRF (850 nm)")
fig.savefig("maize_brf_polar.png", dpi=150, bbox_inches="tight")

Images or multi-angle reflectance

Need Use
Mean reflectance in several directions, e.g. bidirectional reflectance curves BRFSensor
An image in one direction, for spatial texture and shadows OpticalImager with Orthographic(view_zenith=…)

More directions cost more in BRFSensor, but far less than imaging each direction separately.

Finite and periodic scenes

The maize field in this chapter uses periodic boundaries, so the result represents an infinite uniform field. Without them there is nothing outside the scene and the result includes the edges of the plot. When comparing with observations or other models, use the same boundary conditions on both sides.

Parameterised bidirectional reflectance

When the 3-D structure is not needed, the RPV model describes the bidirectional reflectance of a surface directly (see chapter 9):

flat = less.Scene()
flat.size = 10.0
flat.terrain = less.Terrain(property=less.RPV(rho0=0.08, k=0.7, theta=-0.15, rhoc=0.05))
flat.illumination = less.Illumination(
    source=less.Sun(zenith=30, azimuth=180), atmosphere=less.NoAtmosphere())
rpv = flat.simulate(less.BRFSensor(bands=[650], directions=directions, num_photons=200_000))

Companion script: scripts/21_brdf.py

Next