跳转至

16 - Scene 与 Project:保存、复用和任务快照

LESS 的模拟核心是 less.Scene。只要场景、光照和传感器已经在 Python 脚本中定义,就可以直接调用 scene.simulate(sensor) 完成全部模拟。

less.Project 是可选的工程管理层。它把 Scene、命名传感器、资源和工程元 数据组织在一起,适合长期保存、分享和远程任务。

本章使用 LESS Project Format v2,涵盖:

  • 什么时候只用 Scene,什么时候使用 Project
  • 保存和加载独立场景;
  • 创建完整工程并管理多个传感器;
  • 构建一次 Scene Session,重复执行多个模拟;
  • 修改光照或物理属性而不重建几何;
  • 保存目录工程、导出便携包和生成远程任务快照;
  • 从旧版 LESS 工程迁移。

1. 先选择:Scene 还是 Project

二者不是两套模拟系统。Project 内部仍然持有一个普通的 Scene

less.Project
├── scene       → 真正的模拟运行时和构建状态
├── sensors     → 工程登记的命名传感器
├── resources   → 文件资源注册表
└── metadata    → 标题、作者、工程 ID 和修订号

按使用目的选择即可:

使用场景 推荐入口
Notebook、单次实验、算法验证 Scene
传感器由脚本临时创建 Scene
复用或分享一个纯场景模板 scene.save()
保存场景和多个传感器 Project
管理本地资源和工程修订 Project
提交远程计算任务 Project.export_snapshot()

如果不需要工程管理和打包,直接使用 Scene 就足够:

import less

scene = less.Scene()
scene.terrain = less.Terrain(
    property=less.Lambertian(reflectance=0.2)
)
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), atmosphere=less.HosekWilkieAtmosphere(turbidity=2.0))

sensor = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[450, 550, 650, 850],
)

# Scene 会在第一次 simulate() 时自动构建。
image = scene.simulate(sensor)

2. 保存和加载独立 Scene

scene.save() 只保存场景本身,包括:

  • 地形和 DEM;
  • 对象与实例;
  • 物理属性;
  • 光照;
  • Scene 的空间与后端配置。

它不保存工程传感器列表、资源注册表、工程 ID 或任务信息。

2.1 三种保存模式

scene.save("forest-scene.less")

默认得到一个自包含的 ZIP 文件,网格会被复制到包内。

scene.save("forest-scene.less", pack=False)

得到同名的非压缩目录,适合本地调试和大型网格。

scene.save("forest-scene.json")

得到轻量 JSON 引用文件。网格保持原路径,不会复制,因此不一定可移植。

2.2 加载 Scene

scene = less.Scene.load("forest-scene.less")

sensor = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[650, 550, 450],
)
image = scene.simulate(sensor)

加载后的 Scene 尚未构建;第一次 simulate() 会自动构建,无需额外准备。

建议给纯场景文件使用容易识别的文件名:

forest-scene.less
maize-template.less
urban-block-scene.less

3. 创建一个完整 Project

3.1 从空工程开始

import numpy as np
import less

project = less.Project.create(
    title="北方桦树林冠实验",
    author="LESS Lab",
    description="比较正射与多角度冠层反射率。",
    backend="auto",
)

scene = project.scene
scene.size = (100.0, 100.0)
scene.terrain = less.Terrain(
    property=less.Lambertian(reflectance=0.18)
)
scene.illumination = less.Illumination(source=less.Sun(zenith=32, azimuth=145), atmosphere=less.HosekWilkieAtmosphere(turbidity=2.2))

tree = less.Object("birch", mesh="assets/birch.obj")
tree.set_property("leaves", less.Prospect(cab=42))

positions = np.array([
    [20, 20, 0],
    [40, 30, 0],
    [65, 25, 0],
    [30, 70, 0],
    [70, 65, 0],
])
scene.add(tree, positions=positions)

Project.create() 只是帮我们创建了工程和初始 Scene。Scene 的全部 API 仍然 可以通过 project.scene 使用。

3.2 从已有 Scene 创建

已有代码不需要重写:

project = less.Project(
    scene=scene,
    title="北方桦树林冠实验",
    author="LESS Lab",
)

这也是旧版 Project(scene=..., sensors=...) 调用的兼容入口。


4. 管理工程资源

Project Format 把资源分为 managedexternal 两种存储策略。

4.1 托管资源

小型或必须随工程传递的文件应纳入工程:

mesh_resource = project.resources.import_file(
    "assets/birch.obj",
    resource_type="mesh",
)

mesh_path = project.resources.resolve(mesh_resource.id)
tree = less.Object("birch", mesh=mesh_path)

保存目录工程或便携包时,托管资源会复制到 resources/,并记录文件大小和 SHA-256 哈希。

资源注册不会自动创建 Object。它只负责文件身份、路径和存储策略,场景对象 仍然由 less.Object 创建。

4.2 外部资源

非常大的 DEM、点云或共享数据可以只保留链接:

dem_resource = project.resources.register_external(
    "F:/remote-sensing/dem/large-area.tif",
    resource_type="terrain",
)

外部资源不会复制到日常工程中,因此工程更小,但不再完全可移植。打开工程的 机器必须能访问相同路径或共享挂载点。

提交远程任务前应确保外部资源在服务器上可见,或者先将其改为托管资源。 当前 export_snapshot() 会保留 external 路径,不会隐式复制超大文件。

4.3 检查资源

for resource in project.resources:
    errors = project.resources.verify(
        resource.id,
        checksum=True,
    )
    print(resource.name, errors or "OK")

5. 添加多个命名传感器

nadir = less.OpticalImager(
    less.Orthographic(
        image_size=512,
        view_zenith=0,
    ),
    bands=[450, 550, 650, 850],
)
project.add_sensor(nadir, name="冠层多光谱(正射)")

oblique = less.OpticalImager(
    less.Orthographic(
        image_size=512,
        view_zenith=30,
        view_azimuth=90,
    ),
    bands=[550, 650, 850],
)
project.add_sensor(oblique, name="冠层多角度(30°)")

查看和获取传感器:

print([sensor.name for sensor in project.sensors])

sensor = project.get_sensor("冠层多光谱(正射)")

移除传感器:

project.remove_sensor("冠层多角度(30°)")

传感器配置不是场景几何。添加、删除或修改传感器不需要重建 Scene。


6. 构建一次,运行多个传感器

推荐在批量模拟中显式构建:

project.build()

image_nadir = project.simulate("冠层多光谱(正射)")
image_oblique = project.simulate("冠层多角度(30°)")

两个传感器共享同一个 project.scene,因此第二次模拟不会再创建几何加速结构。

不指定名称时,Project 会依次执行全部登记传感器:

results = project.simulate()

for sensor_name, product in results.items():
    product.save(f"outputs/{sensor_name}.tif")

返回值是:

{
    "冠层多光谱(正射)": product1,
    "冠层多角度(30°)": product2,
}

Project.simulate() 最终仍然调用同一个 Scene:

project.simulate("冠层多光谱(正射)")

# 等价于:
sensor = project.get_sensor("冠层多光谱(正射)")
project.scene.simulate(sensor)

Project 不会创建另一份 GPU Scene,也不会通过 HTTP 中转本地模拟。


7. 参数热更新与几何重建

7.1 修改光照:不重建

project.set_illumination(
    less.Illumination(source=less.Sun(zenith=50, azimuth=210), atmosphere=less.HosekWilkieAtmosphere(turbidity=2.5))
)

print(project.session_status)  # parameters_dirty

image = project.simulate("冠层多光谱(正射)")

下一次模拟前只会同步光照参数,已有几何加速结构继续复用。

7.2 修改物理属性:不重建

forest = project.scene.objects["birch"]
forest.set_property(
    "leaves",
    less.Prospect(cab=25),
)

image = project.simulate("冠层多光谱(正射)")

也可以使用路径式 API:

project.scene.update_property(
    "birch.leaves",
    less.SpectrumDB("birch_leaf_green"),
)

7.3 修改几何:需要重建

以下变化需要重新构建或更新几何结构:

  • 增删对象;
  • 更换网格;
  • 改变地形几何;
  • 改变大量实例位置、旋转或缩放。
# ... 修改对象、实例或地形 ...
project.rebuild()

image = project.simulate("冠层多光谱(正射)")

快速判断当前状态:

print(project.is_built)
print(project.session_status)

session_status 的含义:

状态 含义
unbuilt 尚未构建
ready 可直接复用
parameters_dirty 参数待同步,不需要重建
geometry_dirty 几何变化,需要重建

8. 保存 Project

8.1 可编辑目录工程

需要频繁编辑时,推荐使用普通目录工程:

project.save_directory("birch-experiment.less")

目录内容:

birch-experiment.less/
├── project.json       # 格式版本、工程 ID、标题和修订号
├── metadata.json      # 兼容元数据
├── scene.json         # Scene 配置
├── sensors.json       # 命名传感器
├── resources.json     # 资源注册表和哈希
└── resources/
    ├── mesh/
    ├── terrain/
    └── ...

第一次保存后,可以直接保存回当前位置:

project.save_directory()

目录模式不需要每次压缩整个大型场景,适合频繁编辑和版本管理。

8.2 单文件便携工程

project.save("birch-experiment.less")

默认生成一个 ZIP 格式的单文件 .less,同时把它设为当前工程路径。

如果只是另外导出一个便携副本,而不改变当前编辑工程:

project.pack("birch-experiment-portable.less")

二者的区别:

API 作用
save_directory() 保存并继续编辑目录工程
save(path) 保存并把该路径作为当前工程
pack(path) 导出便携副本,不切换当前工程

旧调用仍然有效:

project.save("birch-experiment.less", pack=False)

它等价于保存非压缩目录。

8.3 JSON 引用模式

project.save("birch-experiment.json")

JSON 模式主要用于兼容和轻量配置交换。资源保持路径引用,不适合作为完全可移植 的归档格式。


9. 打开、验证和关闭工程

目录和 ZIP 使用同一入口:

project = less.Project.open("birch-experiment.less")

兼容入口也可继续使用:

project = less.Project.load("birch-experiment.less")

运行前验证工程:

report = project.validate(verify_checksums=True)

for issue in report.issues:
    print(issue.severity, issue.path, issue.message)

report.raise_for_errors()

验证会检查:

  • Scene 是否有效;
  • 传感器名称是否缺失或重复;
  • 网格和资源是否存在;
  • 文件大小与可选 SHA-256 是否一致。

从 ZIP 打开的工程会解压到临时目录。完成构建和模拟后可以清理:

project.close()

不要在模拟前调用 close(),因为 Scene 中的网格可能仍指向解压目录。


10. 生成远程任务快照

远程任务不直接修改当前 Project 对象,而是从某个工程修订生成独立快照:

snapshot = project.export_snapshot(
    "jobs/nadir-job.less",
    sensors=["冠层多光谱(正射)"],
)

print(snapshot.project_id)
print(snapshot.revision)
print(snapshot.fingerprint)
print(snapshot.sensors)

快照中还会生成 snapshot.json

project_id     工程身份
revision       提交时的工程修订
fingerprint    配置内容指纹
sensors        本次任务选中的传感器
created_at     快照创建时间

服务器可以使用普通 Project API 打开快照:

job = less.Project.open("jobs/nadir-job.less")
job.validate().raise_for_errors()
job.build()
results = job.simulate()

本地模拟不需要快照,也不需要 HTTP:

result = project.simulate("冠层多光谱(正射)")

11. Project API 速查

# 创建与打开
less.Project.create(...)
less.Project(scene=..., sensors=[...])
less.Project.open(path)
less.Project.load(path)

# 传感器
project.add_sensor(sensor, name="Nadir")
project.get_sensor("Nadir")
project.remove_sensor("Nadir")

# 会话与模拟
project.build()
project.rebuild()
project.simulate("Nadir")
project.simulate()                  # 全部传感器
project.set_illumination(...)
project.show()

# 验证与持久化
project.validate()
project.save_directory(path)
project.save(path)
project.pack(path)
project.export_snapshot(path, sensors=[...])
project.close()

常用状态:

project.path
project.is_dirty
project.is_built
project.session_status
project.project_id
project.revision

12. 从旧版 LESS 工程迁移

原版 LESS 的模拟目录可以先转换为 Scene:

scene = less.Scene.from_less(
    "D:/LESS/simulations/my_project/"
)

from_less() 导入场景几何、地形、属性和光照,但不会自动把旧配置中的所有观测 参数变成新传感器。建议显式创建传感器并包装成 Project:

sensor = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[550, 650, 850],
)

project = less.Project(
    scene=scene,
    title="Migrated LESS experiment",
)
project.add_sensor(sensor, name="Nadir multispectral")
project.save_directory("migrated-experiment.less")

新版本也可以读取由旧版 less.Project.save() 生成的 v1 .less 工程。首次保存 后会使用 v2 结构。


13. 推荐工作流

纯 Python 临时实验

scene = make_scene()
result = scene.simulate(make_sensor())

可复现的本地工程

project = less.Project.create(title="Experiment")
# 配置 project.scene、resources 和 sensors
project.save_directory("experiment.less")
project.build()
results = project.simulate()

本地工程 + 远程服务

打开目录工程
    → 在 Python 中编辑 Scene 和传感器
    → 本地构建一次 Scene Session
    → 多个传感器重复模拟
    → 修改光照/属性时热更新
    → 需要远程计算时导出任务快照
    → lessd 打开快照并执行

关键原则只有一个:Scene 负责模拟,Project 负责组织和复现。

下一章:17 - 从 LiDAR 点云重建三维场景

相关 API

  • less.Scene.save()less.Scene.load()
  • less.Project.create()less.Project.open()less.Project.save()
  • less.Project.add_sensor()less.Project.simulate()
  • less.Project.validate()less.Project.snapshot()