18 - 空间纹理与属性场¶
将 UV 图像、地理栅格、实测光谱、RGB 影像、温度和发射率作为空间变化的 Property 输入。
📋 LESS 中纹理的含义¶
LESS 的纹理是某个具名物理参数的空间数据源,并不是一套可以独立使用的完整材质。先把纹理赋给 Property 参数,再把 Property 赋给地形或对象 Component:
roof = less.Lambertian(
reflectance=less.RGBTexture("roof_albedo.png"))
building.set_property("roof", roof)
这样可以保持物理语义明确:同一幅图像可以被声明为反射率来源,但不会自动变成温度、发射率或透过率。
| 输入 | 公共类型 | 常见参数 |
|---|---|---|
| 每像素一个物理量 | ScalarTexture |
温度 |
| 实测多波段光谱 | SpectralTexture |
反射率、透过率、发射率 |
| 三通道图像 | RGBTexture |
可见光反射率的经验重建 |
常量和纹理使用同一套 Property 定义。因此,把 Component 从常量反射率改为空间反射率时,不需要修改传感器或模拟流程。
📍 选择空间映射¶
| 映射 | 适用数据 | 必需信息 |
|---|---|---|
mapping="uv" |
OBJ 模型、建筑、车辆、倾斜摄影网格 | OBJ 中的 vt 坐标 |
mapping=less.WorldXY() |
GeoTIFF、DEM 对齐产品、土地覆盖和 LST 栅格 | 栅格 CRS/仿射和 scene.georeference |
UV 纹理默认使用 wrap="repeat";world-XY 纹理默认使用 wrap="clamp",并要求明确处理场景超出栅格范围的情况。
⚠️ 不要给地形使用
mapping="uv"。 地形没有 OBJ UV 图集。LESS 会明确报错,而不会从任意位置取样。
📚 使用实测高光谱反射率¶
栅格存储线性地表反射率,数组形状为 (height, width, bands),波长可以非等间距。
import less
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)
GeoTIFF 的波长可来自逐波段 wavelength 标签、波段描述或 ENVI wavelength 标签。对 .npy 文件需要显式传入波长;对 .npz 文件,使用名为 data 和 wavelengths 的数组。
soil = less.SpectralTexture(
"soil_reflectance.npy",
wavelengths=(450.0, 550.0, 670.0, 865.0, 1610.0),
mapping="uv",
)
当 units="fraction" 时,数据必须是 [0, 1] 范围内的线性物理比例。实测反射率不要进行 sRGB gamma 变换。
📚 使用 RGB 影像¶
RGBTexture 会把 sRGB 转成线性 RGB,并用 spectral_reconstruction="jh19" 经验重建平滑的可见光光谱:
facade = less.Lambertian(
reflectance=less.RGBTexture(
"facade.png",
color_space="srgb",
spectral_reconstruction="jh19",
mapping="uv",
))
building.set_property("facade", facade)
⚠️ RGB 重建是估计值,不是实测高光谱反射率。 照片或卫星 RGB 合成图可能已经包含光照、阴影、大气、色调映射和压缩效应。定量模拟应优先使用地表反射率产品。
当前 JH19 重建只在实现所支持的可见光范围内定义。传感器波长超出该范围时会报错,除非显式给出 outside_spectral_domain 的物理比例回退值。因此,不能把 RGB 纹理当成 905 nm 或 1064 nm LiDAR 光谱。
📍 放置地理栅格¶
world-XY 映射要求场景与栅格使用同一个以米为单位的投影 CRS。场景 origin 是局部 (0, 0) 对应的世界坐标。
import less
scene = less.Scene()
scene.size = (1000.0, 1000.0)
scene.georeference = {
"crs": "EPSG:32650",
"origin": (500000.0, 4000000.0),
"units": "m",
}
terrain_reflectance = less.SpectralTexture(
"surface_reflectance.tif",
wavelengths="metadata",
mapping=less.WorldXY(outside="error"),
filter="bilinear",
nodata="component_default",
)
terrain_property = less.Lambertian(reflectance=0.15).with_field(
"reflectance", terrain_reflectance)
scene.terrain = less.Terrain(property=terrain_property)
LESS 会读取 GeoTIFF 的各波段,应用有效的 scale/offset 元数据,并使用栅格 mask 识别 nodata。CRS 不一致时会直接报错;LESS 不会隐式重投影科学栅格。
土地覆盖产品存的是类别码,而不是反射率。可以用 class_values 显式把每个类别映射为物理标量或光谱;未列出的类别按 nodata 策略处理:
worldcover = less.SpectralTexture(
"worldcover.tif",
wavelengths=(550.0, 850.0),
class_values={
10: (0.08, 0.45), # 示例值;请替换为研究采用的光谱库
20: (0.12, 0.38),
80: (0.04, 0.02),
},
mapping=less.WorldXY(),
filter="nearest",
nodata="component_default",
)
class_values 需要显式波长;类别码不能再应用 GeoTIFF scale/offset。LESS 不内置“权威的 WorldCover 光谱”,因为同一类别的反射率会随地点、季节和含水量变化。
TurbidBoundary 的统计虚拟叶片没有 OBJ UV。相应工作流可以使用 world-XY Field,但不能使用表面 UV 图集。
📚 映射温度和发射率¶
温度是单位为 K 的标量 Field,发射率可以是常量或光谱 Field:
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)
输入应为地表温度 LST,而不是大气顶亮温。nodata="component_default" 会让缺失像素使用 Property 的常量值。请像上例一样,把常量写在 Property 构造器中,再用 with_field() 或 with_fields() 绑定纹理。
🔄 让同一资产服务不同传感器¶
纹理波长描述的是属性数据,不是输出波段。每个传感器可以选择自己的波长或 SRF;LESS 会在所需波长处计算属性,并按传感器响应积分。
rgb = scene.simulate(less.OpticalImager(
less.Orthographic(image_size=512),
bands=[450.0, 550.0, 650.0],
))
hyperspectral = scene.simulate(less.OpticalImager(
less.Orthographic(image_size=512),
bands=list(range(400, 1001, 5)),
))
Scene 是持久的数字场景:几何和纹理不变时会重复使用资源。更换传感器波段不需要修改 OBJ,也不需要复制纹理。
💾 控制过滤、缺失值与驻留预算¶
| 选项 | 取值 | 含义 |
|---|---|---|
filter |
nearest, bilinear, trilinear |
空间重建和 MIP 过滤 |
wrap |
clamp, repeat |
UV 超出范围时的取样方式 |
nodata |
error, component_default, 数值 |
缺失像素策略 |
missing_uv |
error, component_default |
网格缺少 UV 时的策略 |
目前只有光学和热红外成像支持 trilinear,可根据像元在地表的覆盖范围选择合适的纹理层级。BRF、RadiationField、辐照度、LiDAR、能量平衡、光合作用和 SIF 应使用 nearest 或 bilinear。如果组合不受支持,LESS 会在开始模拟前明确报错,不会悄悄改用其他过滤方式。
模拟前可以查询完整的“场景 + 请求”组合:
if not scene.can_use(sensor, explain=True):
raise RuntimeError("当前模拟与空间纹理组合不可用")
image = scene.simulate(sensor)
未解析叶片的 TurbidBoundary 没有表面 UV 坐标,因此它的空间 Field 必须使用
WorldXY;同一场景中的普通 mesh 表面仍可使用 UV 纹理。
| outside_spectral_domain | error, 物理比例 | 波长超出数据域时的策略 |
| raster_bands | all, 一组从 1 开始的波段号 | 选择 GeoTIFF 波段 |
| scale, offset | metadata, 标量或逐波段值 | 将遥感整数 DN 转为物理量 |
| class_values | {类别码: 标量或光谱} | 将分类栅格转换为物理 Field |
大型 world-XY GeoTIFF 会按场景覆盖范围读取窗口。可以在 Scene 上设置硬驻留预算和 tile 对齐:
如果场景所需的完整栅格范围无法装入预算,LESS 会抛出 MemoryError,不会静默降低栅格分辨率。
💾 保存和移动纹理场景¶
引用保存会记录纹理绝对路径。打包保存会把纹理复制进 .less 归档,并使用内容派生文件名;原始栅格移动或删除后,归档仍可使用:
打包时会区分文件名相同但内容不同的源文件,并对同一源文件的重复引用去重。
🔧 常见错误¶
spectral texture wavelength metadata is missing¶
文件没有可读取的波长标签。请显式传入严格递增的 wavelengths=(...),或补充逐波段/ENVI wavelength 元数据。
world_xy geospatial textures require scene.georeference¶
构建场景前,为 scene.georeference 设置与栅格一致的投影 crs、origin 和米制单位。
texture contains nodata while nodata='error'¶
如果 Property 有科学上有效的常量默认值,使用 nodata="component_default";也可以显式提供有限数值作为回退。
requested wavelength is outside the texture spectral domain¶
应使用覆盖传感器波段的实测数据。只有当回退值具有明确物理意义时,才配置数值回退。
相关 API¶
less.Texture2Dless.SpectralTextureless.ScalarTextureless.UVMappingless.WorldXYMappingless.Property.with_field()与less.Property.with_fields()