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:
Saving¶
A packed save puts the texture files into the scene file:
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 |