Skip to content

15 - Spatial textures and property fields

So far a component's property has been the same everywhere on the component. Textures let a property vary in space: the reflectance of a roof from a picture, the surface reflectance from a hyperspectral image, the land surface temperature from a temperature product.

First download the example data for this chapter and unzip it into your working folder. It contains a few small reflectance, temperature and emissivity rasters and a simple mesh with texture coordinates. They are for demonstration only; use your own data for quantitative work.

A texture is a data source for a property

A texture is the spatial data for one physical parameter and only makes sense inside a property. Here the roof reflectance comes from a picture:

import less

building = less.Object("building")
building.add_component("roof", less.Mesh("texture_examples.obj", group="roof"))
building.add_component("facade", less.Mesh("texture_examples.obj", group="facade"))
tree = less.Object("tree")
tree.add_component("leaves", less.Mesh("texture_examples.obj", group="leaves"))

roof = less.Lambertian(reflectance=less.RGBTexture("roof_albedo.png"))
building.set_property("roof", roof)

Once a picture is declared as reflectance, it is never used as temperature or transmittance.

Texture type Content Typical use
ScalarTexture One number per pixel Temperature
SpectralTexture One spectrum per pixel Reflectance, transmittance, emissivity
RGBTexture A three-channel picture Estimating visible reflectance

How textures map onto the scene

Mapping Data Needs
mapping="uv" Meshes of buildings, vehicles, photogrammetry models Texture coordinates (vt) in the OBJ
mapping=less.WorldXY() Geographic rasters: GeoTIFF, land cover, surface temperature The raster's coordinate system and scene.georeference

UV textures repeat outside 0–1 by default; geographic rasters use the edge value. Terrain has no texture coordinates and can only use WorldXY; the same applies to statistical vegetation.

Measured spectral textures

A spectral raster stores linear reflectance with shape (rows, columns, bands); band spacing may be uneven. Here the front reflectance, back reflectance and transmittance of the leaves each come from a texture:

leaf = less.Lambertian(
    reflectance=0.10, back_reflectance=0.12, transmittance=0.05,
).with_fields(
    reflectance=less.SpectralTexture(
        "leaf_front.tif", wavelengths="metadata", mapping="uv",
        filter="trilinear", nodata="component_default"),
    back_reflectance=less.SpectralTexture(
        "leaf_back.tif", wavelengths="metadata", mapping="uv"),
    transmittance=less.SpectralTexture(
        "leaf_transmittance.tif", wavelengths="metadata", mapping="uv"),
)
tree.set_property("leaves", leaf)

Write constant values in the property first, then attach textures with with_field() or with_fields(). nodata="component_default" makes missing pixels use those constants.

wavelengths="metadata" reads the wavelengths from the GeoTIFF band tags. For .npy files give the wavelengths directly; in .npz files name the arrays data and wavelengths:

soil = less.SpectralTexture(
    "soil_reflectance.npy",
    wavelengths=(450.0, 550.0, 670.0, 865.0, 1610.0),
    mapping="uv",
)

Values should be linear reflectance between 0 and 1, without an sRGB gamma.

RGB pictures

RGBTexture converts sRGB colours to linear RGB and estimates a smooth visible spectrum:

facade = less.Lambertian(reflectance=less.RGBTexture(
    "facade.png", color_space="srgb", spectral_reconstruction="jh19", mapping="uv"))
building.set_property("facade", facade)

A spectrum estimated from colours is only approximate. Photographs often already contain illumination, shadows and tone adjustments; for quantitative work use surface reflectance products. The estimate covers only visible wavelengths; to use the texture at near-infrared or LiDAR wavelengths, give the reflectance there with outside_spectral_domain.

Geographic rasters

Geographic rasters require the scene and the raster to use the same projected coordinate system in metres. The origin in scene.georeference is the map coordinate of the scene's local (0, 0), its north-west corner:

scene = less.Scene()
scene.size = (1000.0, 1000.0)
scene.illumination = less.Illumination(
    source=less.Sun(zenith=30), atmosphere=less.NoAtmosphere())
scene.georeference = {"crs": "EPSG:32650", "origin": (500000.0, 4000000.0), "units": "m"}

surface = less.SpectralTexture(
    "surface_reflectance.tif", wavelengths="metadata",
    mapping=less.WorldXY(outside="error"), filter="bilinear",
    nodata="component_default")
scene.terrain = less.Terrain(
    property=less.Lambertian(reflectance=0.15).with_field("reflectance", surface))

Scale, offset and mask information in the GeoTIFF is applied automatically. If the coordinate systems differ, LESS reports an error; reproject the raster to the scene's coordinate system beforehand.

A land-cover map stores class codes. class_values gives each class a reflectance:

landcover = less.SpectralTexture(
    "worldcover.tif",
    wavelengths=(550.0, 850.0),
    class_values={
        10: (0.08, 0.45),   # tree cover (example values)
        20: (0.12, 0.38),   # shrubland
        80: (0.04, 0.02),   # water
    },
    mapping=less.WorldXY(),
    filter="nearest",
    nodata="component_default",
)

The reflectance of a class varies with place, season and moisture, so choose these values for your study area.

Temperature and emissivity

Temperature is a scalar texture in K; emissivity may be a constant or a spectral texture:

thermal = less.ThermalProperty(temperature=300.0, emissivity=0.95).with_fields(
    temperature=less.ScalarTexture(
        "lst.tif", units="K", mapping=less.WorldXY(outside="error"),
        nodata="component_default"),
    emissivity=less.SpectralTexture(
        "emissivity.tif", wavelengths=(10600.0, 12000.0),
        mapping=less.WorldXY(outside="error"), nodata="component_default"),
)
scene.terrain.set_property(thermal)

This needs land surface temperature, not the sensor's brightness temperature.

One set of textures for different sensors

The wavelengths of a texture describe the data, not the output bands. Each sensor uses its own bands and LESS evaluates the property where needed:

rgb = scene.simulate(less.OpticalImager(
    less.Orthographic(image_size=64), bands=[450.0, 550.0, 650.0], quality=16))
hyperspectral = scene.simulate(less.OpticalImager(
    less.Orthographic(image_size=64), bands=list(range(450, 851, 10)), quality=16))

Common options

Option Values Meaning
filter nearest, bilinear, trilinear Interpolation between pixels
wrap clamp, repeat Values when UV leaves the 0–1 range
nodata error, component_default, a number Handling of missing pixels
missing_uv error, component_default Handling of meshes without texture coordinates
outside_spectral_domain error, a number Handling of wavelengths outside the data
raster_bands all or a list of band numbers Which GeoTIFF bands to read
scale, offset metadata, numbers Conversion of integer values to physical quantities
class_values {class: value or spectrum} Conversion of class maps to physical quantities

trilinear chooses a texture resolution that matches the pixel footprint on the ground and is used by optical and thermal imaging; other sensors and processes use nearest or bilinear. If you are unsure whether a combination works, check first:

sensor = less.OpticalImager(less.Orthographic(image_size=64), bands=[550, 850], quality=16)
print(scene.can_use(sensor, explain=True))

Large geographic rasters are read only over the scene extent. The memory used by textures can be limited:

scene.texture_memory_budget_bytes = 1024 * 1024 * 1024
scene.texture_tile_size = 256

Saving

A packed save puts the texture files into the scene file:

scene.save("survey.less", pack=True)
restored = less.Scene.load("survey.less")

Common errors

Message What to do
spectral texture wavelength metadata is missing The file has no wavelength tags; give wavelengths=(...)
world_xy geospatial textures require scene.georeference Set scene.georeference first
texture contains nodata while nodata='error' Use nodata="component_default" or give a fallback value
requested wavelength is outside the texture spectral domain Use data covering the sensor bands, or set outside_spectral_domain

Next