11 - 三角网格¶
用三角网格表示植被时,模型中的每一片叶子、每一段枝干都是真实的三角形。这种表示能保留冠层的结构细节,例如叶片的空间聚集、林隙和阴影。本章介绍三角网格物体的组成和放置方法,最后构建两个完整场景:一片秋季森林和一块玉米田。
物体与组件¶
物体(less.Object)通常来自一个 OBJ 文件。文件中的每个 group 成为一个组件,可以分别设置属性:
import less
tree = less.Object("ash", mesh=less.asset_path("FREX.obj"))
print(tree.components) # ['leaves', 'stem_branch']
tree.set_property("leaves", less.Prospect(cab=40, car=10))
tree.set_property("stem_branch", less.Lambertian(reflectance=0.1))
也可以逐个添加组件,组件可以来自不同的文件:
tree2 = less.Object("ash_by_parts")
tree2.add_component("leaves", less.Mesh(less.asset_path("FREX.obj"), group="leaves"))
tree2.add_component("stem_branch", less.Mesh(less.asset_path("FREX.obj"), group="stem_branch"))
指定 group 时只读取该组的三角形。
LESS 自带三个植物模型:
| 文件 | 内容 | 组件 |
|---|---|---|
FREX.obj |
欧洲白蜡树 | leaves、stem_branch |
maize.obj |
玉米植株 | 单一组件 |
tutorial_tree.obj |
简化树木(用于教学) | crown(封闭树冠)、trunk、leaves |
使用自己的模型时,注意 OBJ 的坐标约定和三角形正面的方向(见第 5 章)。
放置实例¶
同一个物体可以在场景中放置多次,每次放置称为一个实例。所有实例共用同一份几何,因此放置大量实例几乎不增加内存:
import numpy as np
scene = less.Scene()
scene.size = 50.0
positions = np.array([[10, 10, 0], [20, 15, 0], [30, 25, 0]])
handle = scene.add(tree, positions=positions,
scales=[0.8, 1.0, 1.2], # 每个实例的缩放倍数
rotations=[0, 90, 180]) # 绕 Z 轴旋转(度)
scales 可以是:
| 形式 | 含义 |
|---|---|
| 一个数 | 所有实例使用同一倍数 |
| 长度为实例数的数组 | 每个实例各自等比缩放 |
| 形状为(实例数, 3)的数组 | 每个实例在 X、Y、Z 方向分别缩放 |
放置工具¶
less.place 提供几种常用的位置生成方法:
positions = less.place.random(scene, n=60, min_dist=3.0, seed=42) # 随机,最小间距 3 m
positions = less.place.grid(scene, spacing=5.0, jitter=0.5, seed=1) # 规则网格加随机抖动
positions = less.place.grid(scene, spacing=(0.75, 0.40), margin=0.5) # 东西 0.75 m、南北 0.40 m
groups = less.place.random_split(scene, counts=[40, 20], min_dist=3.0, seed=7) # 两组互不重叠
随机的大小和朝向:
n = 60
scales = less.place.uniform_scale(n, lo=0.6, hi=1.5, seed=42)
rotations = less.place.random_rotation(n, seed=42)
已有样地清单时,可以用 less.place.from_csv("trees.csv", x="x", y="y") 读取坐标。
修改实例¶
场景构建后,可以通过句柄移动、旋转或缩放实例,然后调用 scene.update_instances() 提交,不需要重建整个场景:
scene.build() # 场景尚未模拟过时先构建
handle.set_scale(1.5, instance=0) # 单个实例
handle.set_scale(np.full(3, 0.9)) # 所有实例
handle.set_position(np.array([[12, 10, 0], [22, 15, 0], [32, 25, 0]]))
scene.update_instances()
周期边界¶
默认情况下场景是有限的:场景外是空的,阳光可以从顶部和迎光的侧面射入场景。研究大面积均匀的林分或农田时,可以让场景在水平方向周期重复:
开启后,从一侧边界射出的光线会从对侧重新进入,相当于把场景无限平铺。几何只保存一份,不会增加内存。True 表示光线最多穿越边界 100 次,也可以给出不小于 5 的整数作为上限。
| 设置 | 适用 |
|---|---|
scene.repetitive = False(默认) |
单株树、孤立地块、街区等有边界的目标 |
scene.repetitive = True |
可以看作水平无限延伸的均匀冠层 |
周期边界只能用于平坦地形。
叶面积指数¶
叶面积指数(LAI)是叶片单面总面积除以场景的水平面积:
lai = scene.measure(less.LAIMeasurement(property_filter=less.Prospect))
by_component = scene.measure(less.LAIMeasurement(group_by="component"))
property_filter 按属性类型挑选组件:上例只统计光学属性为 Prospect 的组件,也就是叶片。不加筛选时,枝干等所有物体组件都会计入。group_by 可以是 "total"、"object" 或 "component"。
地形不计入叶面积。分母始终是 scene.size 对应的水平面积,坡地也不使用坡面面积。
示例一:秋季森林¶
50 m × 50 m 的落叶林,60 棵白蜡树分成五种秋季叶色,大小和朝向随机:
import numpy as np
import less
scene = less.Scene()
scene.size = 50.0
scene.terrain = less.Terrain(property=less.Lambertian(reflectance=0.08))
scene.illumination = less.Illumination(
source=less.Sun(zenith=40, azimuth=150), atmosphere=less.NoAtmosphere())
autumn_leaves = {
"golden": less.Prospect(cab=10, car=20, cbrown=0.15, cw=0.012, cm=0.009, N=1.8),
"orange": less.Prospect(cab=3, car=12, canth=8, cbrown=0.25, cw=0.010, cm=0.008, N=1.7),
"red": less.Prospect(cab=2, car=5, canth=30, cbrown=0.15, cw=0.011, cm=0.009, N=1.8),
"dark_red": less.Prospect(cab=1, car=3, canth=40, cbrown=0.35, cw=0.009, cm=0.010, N=2.0),
"yellow_green": less.Prospect(cab=20, car=15, cbrown=0.05, cw=0.013, cm=0.009, N=1.7),
}
groups = less.place.random_split(scene, counts=[12] * len(autumn_leaves), min_dist=3.0, seed=42)
for (name, leaf), positions in zip(autumn_leaves.items(), groups):
tree = less.Object(f"ash_{name}", mesh=less.asset_path("FREX.obj"))
tree.set_property("leaves", leaf)
tree.set_property("stem_branch", less.Lambertian(reflectance=0.06))
n = len(positions)
scene.add(tree, positions=positions,
scales=less.place.uniform_scale(n, lo=0.6, hi=1.5, seed=len(name)),
rotations=less.place.random_rotation(n, seed=len(name)))
print("LAI:", round(scene.measure(less.LAIMeasurement(property_filter=less.Prospect)), 2))
nadir = scene.simulate(less.OpticalImager(
less.Orthographic(image_size=256), bands=[630, 532, 465], quality=32))
nadir.save("autumn_forest_nadir.png")
oblique = scene.simulate(less.OpticalImager(
less.Perspective(resolution=256, fov=35, position=(25, 60, 45), target=(25, 25, 6)),
bands=[630, 532, 465], quality=32))
oblique.save("autumn_forest_oblique.png")
每种叶色是一个独立的物体,因此可以分别设置属性;random_split 保证不同组的树木之间也保持最小间距。第二个传感器是透视相机:position 是相机位置,target 是视线对准的点,fov 是视场角(度)。
示例二:玉米田¶
10 m × 10 m 的玉米地,行距 0.75 m、株距 0.40 m:
import numpy as np
import less
scene = less.Scene()
scene.size = 10.0
scene.terrain = less.Terrain(property=less.Lambertian(reflectance=0.15))
scene.illumination = less.Illumination(
source=less.Sun(zenith=30, azimuth=150), atmosphere=less.NoAtmosphere())
maize = less.Object("maize", mesh=less.asset_path("maize.obj"))
maize.set_property(less.Prospect(cab=45, car=10, cw=0.012, cm=0.006, N=1.55))
positions = less.place.grid(scene, spacing=(0.75, 0.40), jitter=0.04, margin=0.3, seed=42)
n = len(positions)
rng = np.random.default_rng(42)
plants = scene.add(maize, positions=positions,
scales=rng.uniform(0.9, 1.1, n),
rotations=rng.uniform(0, 360, n))
print(n, "plants, LAI =", round(scene.measure(less.LAIMeasurement()), 2))
sensor = less.OpticalImager(less.Orthographic(image_size=128),
bands=[650, 550, 450], quality=32)
field = scene.simulate(sensor)
field.save("maize_field.png")
side = scene.simulate(less.OpticalImager(
less.Perspective(resolution=256, fov=35, position=(5, 14, 6), target=(5, 5, 1.2)),
bands=[650, 550, 450], quality=32))
side.save("maize_field_side.png")
不同生长期¶
只改变植株大小就能近似不同的生长期,不需要重新放置:
plants.set_scale(np.full(n, 0.3)) # 苗期
scene.update_instances()
seedling = scene.simulate(sensor)
plants.set_scale(rng.uniform(0.9, 1.1, n)) # 成熟期
scene.update_instances()
mature = scene.simulate(sensor)
常见作物的种植参数:
| 作物 | 行距 (m) | 株距 (m) | LAI |
|---|---|---|---|
| 玉米 | 0.60–0.75 | 0.20–0.40 | 2–5 |
| 小麦 | 0.15–0.25 | 0.03–0.05 | 3–7 |
| 大豆 | 0.40–0.60 | 0.05–0.10 | 3–6 |
| 水稻 | 0.25–0.30 | 0.15–0.20 | 4–8 |
场景的 LAI 取决于植株模型本身的叶面积和种植密度。
配套脚本:scripts/11_triangle_mesh.py