跳转至

15 - 空间纹理与属性场

到目前为止,一个组件的属性在整个组件上都相同。纹理让属性随位置变化:屋顶的反射率来自一张图片,地表反射率来自一幅高光谱影像,地表温度来自一幅地表温度产品。

先下载本章示例数据,解压到工作目录。其中包含几幅很小的反射率、温度和发射率栅格,以及一个带纹理坐标的简化网格。它们只用于演示,定量研究请换成自己的数据。

纹理是属性的数据来源

纹理只是某一个物理参数的空间数据,需要放进一个属性才有意义。下面让屋顶的反射率来自一张图片:

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)

同一幅图片被声明为反射率后,不会被当作温度或透射率使用。

纹理类型 内容 常见用途
ScalarTexture 每个像元一个数 温度
SpectralTexture 每个像元一条光谱 反射率、透射率、发射率
RGBTexture 三通道图片 估算可见光反射率

纹理如何对应到场景

映射方式 适用数据 需要的信息
mapping="uv" 建筑、车辆、倾斜摄影等网格模型 OBJ 中的纹理坐标(vt)
mapping=less.WorldXY() GeoTIFF、土地覆盖、地表温度等地理栅格 栅格的坐标系和 scene.georeference

UV 纹理超出 0–1 时默认重复,地理栅格默认取边缘值。地形没有纹理坐标,只能使用 WorldXY;统计植被同样只能使用 WorldXY。

实测光谱纹理

光谱栅格存放线性反射率,数组形状为(行, 列, 波段),波段间隔可以不均匀。下面给叶片的正面反射率、背面反射率和透射率分别指定纹理:

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)

先在属性中写一个常数值,再用 with_field() 或 with_fields() 绑定纹理。nodata="component_default" 表示缺失像元使用这个常数。

wavelengths="metadata" 从 GeoTIFF 的波段标签中读取波长。.npy 文件需要直接给出波长;.npz 文件中的数组应命名为 data 和 wavelengths:

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

数值应是 0–1 之间的线性反射率,不要做 sRGB 伽马变换。

RGB 图片

RGBTexture 把 sRGB 颜色转换为线性 RGB,再估算一条平滑的可见光光谱:

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

由颜色估算的光谱只是近似值。照片中往往已经包含光照、阴影和色调调整,定量研究应使用地表反射率产品。这种估算只覆盖可见光,用于近红外或激光雷达波段时需要用 outside_spectral_domain 给出这些波长上的反射率。

地理栅格

地理栅格要求场景和栅格使用同一个以米为单位的投影坐标系。scene.georeference 的 origin 是场景局部坐标 (0, 0)(西北角)对应的地图坐标:

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))

GeoTIFF 中的比例、偏移和掩膜会自动应用。坐标系不一致时会报错,请事先把栅格投影到场景使用的坐标系。

土地覆盖图存放的是类别编号。用 class_values 给每个类别指定反射率:

landcover = 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",
)

同一类地物的反射率随地点、季节和湿度变化,这些数值应根据研究区确定。

温度和发射率

温度是以 K 为单位的标量纹理,发射率可以是常数或光谱纹理:

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)

这里需要的是地表温度,而不是传感器的亮度温度。

同一组纹理用于不同传感器

纹理的波长描述的是数据本身,而不是输出波段。不同传感器使用各自的波段,LESS 在需要的波长上计算属性:

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))

常用选项

选项 取值 含义
filter nearest、bilinear、trilinear 像元之间如何插值
wrap clamp、repeat UV 超出范围时如何取值
nodata error、component_default、数值 缺失像元如何处理
missing_uv error、component_default 网格没有纹理坐标时如何处理
outside_spectral_domain error、数值 波长超出数据范围时如何处理
raster_bands all 或波段编号列表 读取 GeoTIFF 的哪些波段
scale、offset metadata、数值 把整数值换算为物理量
class_values {类别: 数值或光谱} 把类别图转换为物理量

trilinear 会根据像元在地面上的覆盖范围选择合适的纹理分辨率,用于光学和热红外成像;其他传感器和过程使用 nearest 或 bilinear。不确定某种组合是否可用时,可以先检查:

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

大幅地理栅格只读取场景覆盖的范围。可以限制纹理占用的内存:

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

保存

打包保存会把纹理文件一起放进场景文件:

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

常见错误

错误信息 处理方法
spectral texture wavelength metadata is missing 文件中没有波长标签,直接给出 wavelengths=(...)
world_xy geospatial textures require scene.georeference 先设置 scene.georeference
texture contains nodata while nodata='error' 使用 nodata="component_default" 或给出一个回退值
requested wavelength is outside the texture spectral domain 换用覆盖传感器波段的数据,或设置 outside_spectral_domain

下一步