09 - Multispectral and Hyperspectral Simulation¶
This chapter describes how to use LESS to simulate multispectral and hyperspectral remote sensing data, including the selection of custom bands, predefined satellite sensors, and different atmospheric models.
Spectral band type¶
LESS provides several ways to specify the simulated spectral bands:
Discrete Band¶
The simplest way is to directly specify the center wavelength list:
import less
# Customized band (unit: nm)
bands = [475, 560, 668, 717, 842]
sensor = less.OpticalImager(
less.Orthographic(image_size=512),
bands=bands,
)
Predefined satellite sensors¶
LESS has built-in spectral response functions of commonly used satellite sensors (SRF):
# Sentinel-2A MSI (13 bands)
sensor_s2 = less.OpticalImager(
less.Orthographic(image_size=512),
bands=less.Sentinel2A(),
spectral_resolution=5,
quality=128,
name="Sentinel-2A",
)
# Landsat 8 OLI (11 bands)
sensor_l8 = less.OpticalImager(
less.Orthographic(image_size=512),
bands=less.Landsat8_OLI(),
spectral_resolution=5,
quality=128,
name="Landsat-8",
)
When using the SRF band, the simulation considers the shape of the spectral response function rather than the simple center wavelength. This is more accurate with broadband sensors such as Landsat.
Hyperspectral¶
Continuous spectrum, specifying start wavelength, end wavelength and step size:
# 400-2500 nm, one band every 10 nm (211 bands in total)
bands_hyper = less.Hyperspectral(start=400, stop=2500, step=10)
sensor_hyper = less.OpticalImager(
less.Orthographic(image_size=256), # Hyperspectral speeds up with smaller images
bands=bands_hyper,
quality=64,
name="Hyperspectral",
)
The calculation amount of hyperspectral simulation = the number of bands × the calculation amount of a single band. 211 bands will be about 70 times slower than 3 bands (there are batch optimizations inside LESS, so the actual ratio is smaller).
Atmospheric model selection¶
The lighting model determines the spectrum of solar radiation incident on the scene. Different lighting models are suitable for different band ranges:
HosekWilkieAtmosphere - visible sky model¶
# Fits 320-720 nm range, physical sky radiance distribution
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), atmosphere=less.HosekWilkieAtmosphere(turbidity=2.5))
Features: Sky radiance has a realistic angular distribution (brighter near the horizon, halo around the sun). But it only covers the visible light band.
SimpleSpectralAtmosphere - wide-band atmosphere model¶
# Suitable for 300-2500 nm full band
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), atmosphere=less.SimpleSpectralAtmosphere(turbidity=2.5))
Features: Built-in atmospheric models for Rayleigh scattering, aerosol extinction, ozone and water vapor absorption. Sky scattered light is isotropic. Covers the full shortwave range and is suitable for multispectral and hyperspectral simulations.
NoAtmosphere - Vacuum¶
# TOA Direct radiation does not attenuate and does not produce sky scattering
scene.illumination = less.Illumination(
source=less.Sun(zenith=30, azimuth=150),
atmosphere=less.NoAtmosphere(),
)
Features: The vacuum boundary will not attenuate TOA direct radiation, nor will it generate diffusion. If the atmospheric direct and diffuse transmission coefficients are known, use PrescribedAtmosphere.
Atmosphere——Native atmospheric transmission¶
# The solar geometry and atmospheric state are set separately
scene.illumination = less.Illumination(
source=less.Sun(zenith=30, azimuth=150),
atmosphere=less.Atmosphere.standard(
"midlatitude_summer",
aerosol="continental",
aot550=0.2,
ground_altitude_km=0.0,
),
)
The native model calculates gas absorption, Rayleigh scattering, aerosol scattering, and direct/diffuse multiple scattering from 300–2500 nm. A normal installation already contains the model and parameter database. For top-of-atmosphere imaging, parameter meanings and application scope, see Primary atmosphere and top-of-atmosphere imaging .
Atmospheric profile options:
| Parameter value | Meaning |
|--------|------|
| "tropical" | Tropical |
| "midlatitude_summer" | Mid-latitude summer |
| "midlatitude_winter" | Mid-latitude winter |
| "subarctic_summer" | Subarctic Summer |
| "subarctic_winter" | Subarctic winter |
| "us_standard" | American Standard Atmosphere |
Aerosol type:
| Parameter value | Meaning |
|--------|------|
| "continental" | Continental type |
| "maritime" | Marine type |
| "urban" | Urban type |
| "desert" | Desert type |
| "no_aerosols" | Aerosol-free |
If less.SixSAtmosphere uses 6S, less3d[atmosphere] needs to be installed separately.
Complete Example: Multi-Sensor Simulation¶
import less
import numpy as np
# ──Constructing the cornfield scene─────────────────────────────────────────
scene = less.Scene()
scene.size = 10.0
scene.repetitive = False # REPETITIVE_SCENE has not been released to the public yet
scene.terrain = less.Terrain(property=less.Lambertian(reflectance=0.15))
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), 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))
# Planting in rows
rng = np.random.RandomState(42)
row_sp, plant_sp, margin = 0.75, 0.40, 0.5
n_rows = int((10.0 - 2 * margin) / row_sp) + 1
n_plant = int((10.0 - 2 * margin) / plant_sp) + 1
xs = margin + np.arange(n_rows) * row_sp
ys = margin + np.arange(n_plant) * plant_sp
gx, gy = np.meshgrid(xs, ys, indexing='ij')
positions = np.column_stack([
gx.ravel() + rng.uniform(-0.04, 0.04, gx.size),
gy.ravel() + rng.uniform(-0.04, 0.04, gy.size),
np.zeros(gx.size),
])
scene.add(maize, positions=positions,
scales=rng.uniform(0.9, 1.1, len(positions)),
rotations=rng.uniform(0, 360, len(positions)))
sensor_rgb = less.OpticalImager(
less.Orthographic(image_size=512),
bands=[650, 550, 450],
quality=128, name="RGB",
)
img_rgb = scene.simulate(sensor_rgb)
img_rgb.save("09_rgb.png")
# ── 2. Sentinel-2A simulation ───────────────────────────────────
sensor_s2 = less.OpticalImager(
less.Orthographic(image_size=512),
bands=less.Sentinel2A(),
spectral_resolution=5,
quality=128, name="Sentinel-2A",
)
img_s2 = scene.simulate(sensor_s2)
img_s2.save("09_sentinel2a.tif")
# Calculate NDVI (Sentinel-2A: B4=red index 3, B8=NIR index 7)
data_s2 = img_s2.data
red_s2 = data_s2[:, :, 3] # B4 (665 nm)
nir_s2 = data_s2[:, :, 7] # B8 (842 nm)
ndvi_s2 = (nir_s2 - red_s2) / (nir_s2 + red_s2 + 1e-10)
print(f"Sentinel-2A NDVI: {np.nanmean(ndvi_s2):.3f} (mean)")
# ── 3. Hyperspectral ───────────────────────────────────────────
sensor_hyper = less.OpticalImager(
less.Orthographic(image_size=256),
bands=less.Hyperspectral(start=400, stop=1000, step=5),
quality=64, name="Hyperspectral",
)
img_hyper = scene.simulate(sensor_hyper)
img_hyper.save("09_hyperspectral.tif")
# Extract scene average spectrum
data_hyper = img_hyper.data
mean_spectrum = np.mean(data_hyper, axis=(0, 1))
print(f"Number of hyperspectral bands: {mean_spectrum.shape[0]}")
print(f"Average radiance (500 nm): {mean_spectrum[20]:.4f} W/m²/sr/nm")
print("Tutorial 09 completed!")
Supporting script:
scripts/09_multispectral.py
NDVI calculation example¶
The Normalized Vegetation Index (NDVI) is the most commonly used vegetation index and exploits the difference between the red and near-infrared bands:
# Use custom bands
sensor = less.OpticalImager(
less.Orthographic(image_size=512),
bands=[650, 842], # Red, NIR
quality=128,
)
image = scene.simulate(sensor)
data = image.data
red = data[:, :, 0]
nir = data[:, :, 1]
ndvi = (nir - red) / (nir + red + 1e-10)
# NDVI Typical range
# Bare soil: 0.05 ~ 0.15
# Sparse vegetation: 0.15 ~ 0.40
# Dense vegetation: 0.40 ~ 0.90
Spectral analysis: vegetation red edge¶
The Red Edge (~700-750 nm) is one of the most significant features of the vegetation spectrum—a sharp transition from strong absorption of red light to high reflection in the near-infrared.
Red edge features can be accurately captured using hyperspectral simulation:
# Detailed sampling of red edge areas
sensor_rededge = less.OpticalImager(
less.Orthographic(image_size=512),
bands=less.Hyperspectral(start=650, stop=800, step=2), # 2 nm resolution
quality=128,
)
image_re = scene.simulate(sensor_rededge)
data_re = image_re.data
# Extract the spectrum of a certain pixel
pixel_spectrum = data_re[256, 256, :]
wavelengths = np.arange(650, 800, 2)
# Red edge position = wavelength corresponding to the maximum value of the first derivative
deriv = np.gradient(pixel_spectrum, 2) # 2 nm step size
red_edge_pos = wavelengths[np.argmax(deriv)]
print(f"Red edge position: {red_edge_pos} nm")
Next step¶
Related API¶
less.SpectralBands、less.Hyperspectralless.Sentinel2A、less.Landsat8_OLIless.OpticalImager、less.Orthographicless.Atmosphere、less.HosekWilkieAtmosphereless.SimpleSpectralAtmosphere、less.PrescribedAtmosphere、less.NoAtmosphere