跳转至

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

周期边界

默认情况下场景是有限的:场景外是空的,阳光可以从顶部和迎光的侧面射入场景。研究大面积均匀的林分或农田时,可以让场景在水平方向周期重复:

scene.repetitive = True

场景重复

开启后,从一侧边界射出的光线会从对侧重新进入,相当于把场景无限平铺。几何只保存一份,不会增加内存。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

下一步