Skip to content

12 - Thermal Infrared Simulation

This chapter describes how to use LESS to simulate thermal infrared remote sensing images.

Thermal infrared remote sensing principle

All objects with a temperature above absolute zero emit thermal radiation. In the thermal infrared band (8-14 μm), the thermal radiation of surface objects is related to its temperature and emissivity:

L = ε × B(T, λ)

in: - L: Thermal radiance (W/m²/sr/μm) - ε: emissivity (0-1) - B(T, λ): Planck function (blackbody radiation) - T: Object temperature (K)

In the vegetation canopy, different components (sunny leaves, shady leaves, soil, tree trunks) have different temperatures, which results in rich spatial differentiation information of temperature in thermal infrared images.

ThermalProperty - Thermal properties

Each scene element can have thermal properties set, independent of optical properties:

import less

# Leaves: different temperatures on the sunny side and the shaded side
leaf_thermal = less.ThermalProperty(
    emissivity=0.98,
    temperature_sunlit=305.0,     # Sunny surface temperature (K) ≈ 32°C
    temperature_shaded=300.0,     # Shadow temperature (K) ≈ 27°C
)

# Soil: greater temperature difference
soil_thermal = less.ThermalProperty(
    emissivity=0.95,
    temperature_sunlit=312.0,     # Sunny side ≈ 39°C
    temperature_shaded=303.0,     # Shadow side ≈ 30°C
)

# Trunk: Use a single temperature
trunk_thermal = less.ThermalProperty(
    emissivity=0.97,
    temperature=300.0,            # Uniform temperature ≈ 27°C
)

Sunny side/shady side temperature

LESS automatically determines whether the patch is on the sun side or the shade side based on the relationship between the normal direction of each triangular patch and the sun direction:

  • Sunlit: Patch normal faces the sun and is not occluded → use temperature_sunlit
  • shaded: normal faces away from the sun or is occluded → use temperature_shaded

This is more realistic than using a single temperature.

ThermalImager —— Thermal infrared imager

sensor_thermal = less.ThermalImager(
    less.Orthographic(image_size=512),
    bands=[10600],        # 10.6 μm = 10600 nm(Landsat TIRS B10)
    quality=128,
)

Commonly used thermal infrared bands:

Sensor Band Center wavelength
Landsat TIRS B10 10.6 μm 10600 nm
Landsat TIRS B11 12.0 μm 12000 nm
ASTER B13 10.7 μm 10700 nm
MODIS B31 11.0 μm 11000 nm

Complete Example: Forest Thermal Infrared Simulation

import less
import numpy as np

# ── Create a scene ───────────────────────────────────────────────
scene = less.Scene()
scene.size = 50.0

# Soil (optical + thermal)
terrain = less.Terrain()
terrain.set_property(less.Lambertian(reflectance=0.15))
terrain.set_property(less.ThermalProperty(
    emissivity=0.95,
    temperature_sunlit=312.0,
    temperature_shaded=303.0,
))
scene.terrain = terrain

# Lighting (needs to set sky_temperature for long wave radiation)
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), atmosphere=less.NoAtmosphere())

# ──Trees (optical + thermal)─────────────────────────────────────
tree = less.Object("ash", mesh=less.examples.asset_path("FREX.obj"))

# Optical properties
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))

# thermal properties
tree.set_property("leaves", less.ThermalProperty(
    emissivity=0.98,
    temperature_sunlit=305.0,
    temperature_shaded=300.0,
))
tree.set_property("stem_branch", less.ThermalProperty(
    emissivity=0.97,
    temperature=300.0,
))

# place
rng = np.random.RandomState(42)
positions = less.place.random(scene, n=40, min_dist=4.0, seed=42)
n = len(positions)
scene.add(tree, positions=positions,
          scales=less.place.uniform_scale(n, 0.7, 1.3, seed=42),
          rotations=less.place.random_rotation(n, seed=42))
sensor_thermal = less.ThermalImager(
    less.Orthographic(image_size=512),
    bands=[10600],
    quality=128,
    name="Thermal IR",
)
image_thermal = scene.simulate(sensor_thermal)
image_thermal.save("12_thermal.png")
image_thermal.save("12_thermal.tif")    # Keep original radiance values

# ── Get RGB at the same time for comparison ─────────────────────────────────
sensor_rgb = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[650, 550, 450],
    quality=128,
)
image_rgb = scene.simulate(sensor_rgb)
image_rgb.save("12_rgb.png")

# ── Analyzing thermal images ───────────────────────────────────────────
data = image_thermal.data
print(f"Thermal radiance range: {data.min():.2f} ~ {data.max():.2f} W/m²/sr/μm")

print("Tutorial 12 completed!")

Supporting script: scripts/12_thermal.py

The role of sky_temperature

sky_temperature defines the equivalent radiant temperature of the sky in the thermal infrared band. It affects:

  1. Sky Long Wave Radiation: Contribution of downward thermal radiation from the sky
  2. Reflection component: The sky thermal radiation reflected by the surface of the object (especially the low emissivity surface)

Typical values: - Sunny day: 250-270 K - Cloudy: 270-290 K - Completely cloudy: ≈ ground temperature

Suggestions for setting temperature difference parameters

Component Sunny side temperature (K) Yin side temperature (K) Temperature difference
Positive leaves 303-310 298-303 3-7 K
Negative leaves 300-305 297-301 2-4 K
Bare soil 310-320 300-308 8-15 K
Grass 303-308 298-302 3-6 K
Trunk Single temperature 298-303 - -

These are manually specified temperature values. If you require physics-driven temperature simulation, please refer to Chapter 13: Energy Balance .

Multi-temporal thermal infrared simulation

Using the digital twin architecture, thermal infrared images at different times can be quickly simulated:

# Early morning (small temperature difference)
terrain.set_property(less.ThermalProperty(
    emissivity=0.95, temperature_sunlit=295.0, temperature_shaded=293.0))
tree.set_property("leaves", less.ThermalProperty(
    emissivity=0.98, temperature_sunlit=294.0, temperature_shaded=293.0))
scene.illumination = less.Illumination(source=less.Sun(zenith=70, azimuth=90), atmosphere=less.NoAtmosphere())

image_morning = scene.simulate(sensor_thermal)
image_morning.save("12_thermal_morning.png")

# Noon (big temperature difference)
terrain.set_property(less.ThermalProperty(
    emissivity=0.95, temperature_sunlit=318.0, temperature_shaded=305.0))
tree.set_property("leaves", less.ThermalProperty(
    emissivity=0.98, temperature_sunlit=308.0, temperature_shaded=301.0))
scene.illumination = less.Illumination(source=less.Sun(zenith=10, azimuth=180), atmosphere=less.NoAtmosphere())

image_noon = scene.simulate(sensor_thermal)
image_noon.save("12_thermal_noon.png")

Note: Only the attributes and lighting are modified here, not the geometry, so no rebuild is required.

Next step

The thermal imaging of TurbidBoundary directly consumes the resident temperature state and adaptive spatial field. use ThermalImager(mode="property") observable attribute temperature, use mode="eb" or mode="precomputed" can observe the energy balance results. Python no longer generates blade or upload sample-by-sample temperatures.

  • less.ThermalProperty
  • less.ThermalImagerless.Orthographic
  • less.Illuminationless.Sunless.Atmosphere
  • less.EnergyBalanceProcessless.Microclimate
  • less.Product.save()