Skip to content

08 - Building a farm scene

This chapter introduces how to construct a farmland scene with regular row planting, taking corn fields as an example.

Characteristics of farmland scenes

Unlike the random distribution of forest scenes, farmland plants are usually arranged in rows and rows:

  • row spacing: the distance between adjacent rows (e.g. corn 0.6-0.75 m)
  • In-row spacing: The distance between adjacent plants in the same row (such as corn 0.2-0.4 m)
  • There is some randomness in plant size and orientation, but there are basic rules for location

Use less.place.grid to create a row and column layout

import less

scene = less.Scene()
scene.size = 10.0

# grid() generates regular grid positions
positions = less.place.grid(scene, spacing=0.75, jitter=0.04)

The jitter parameter adds a small random offset to each location, making the planting less "perfect" and closer to a real field.

Non-square spacing

For situations where row spacing and plant spacing are different, we need to build the grid manually:

import numpy as np

scene_size = 10.0
row_spacing = 0.75      # line spacing
in_row_spacing = 0.40   # spacing between plants
margin = 0.5            # Leave the edges blank

# Calculate the number of rows and the number of plants in each row
n_rows  = int((scene_size - 2 * margin) / row_spacing) + 1
n_plant = int((scene_size - 2 * margin) / in_row_spacing) + 1

# Build grid
xs = margin + np.arange(n_rows)  * row_spacing
ys = margin + np.arange(n_plant) * in_row_spacing
gx, gy = np.meshgrid(xs, ys, indexing='ij')

# Add tiny jitter
rng = np.random.RandomState(42)
jitter = 0.04
px = gx.ravel() + rng.uniform(-jitter, jitter, gx.size)
py = gy.ravel() + rng.uniform(-jitter, jitter, gy.size)

positions = np.column_stack([px, py, np.zeros(len(px))])
print(f"Total {len(positions)} plants ({n_rows} rows × {n_plant} plants/row)")

Complete example: Cornfield

import less
import numpy as np

# ── Create a scene ───────────────────────────────────────────────
scene = less.Scene()
scene.size = 10.0
scene.repetitive = False  # Current public default: Limited farmland parcels

# Soil: typical loam
scene.terrain = less.Terrain(
    property=less.Lambertian(reflectance=0.15)
)

# Lighting: Sunny days in summer
scene.illumination = less.Illumination(source=less.Sun(zenith=30, azimuth=150), atmosphere=less.NoAtmosphere())

# ──Corn plant────────────────────────────────────────────
maize = less.Object("maize", mesh=less.examples.asset_path("maize.obj"))
maize.set_property(less.Prospect(
    cab=45, car=10, canth=0, cbrown=0,
    cw=0.012, cm=0.006, N=1.55,
))

# ──Planting in rows───────────────────────────────────────────
row_spacing = 0.75
in_row_spacing = 0.40
margin = 0.5
rng = np.random.RandomState(42)

n_rows  = int((scene.size - 2 * margin) / row_spacing) + 1
n_plant = int((scene.size - 2 * margin) / in_row_spacing) + 1

xs = margin + np.arange(n_rows)  * row_spacing
ys = margin + np.arange(n_plant) * in_row_spacing
gx, gy = np.meshgrid(xs, ys, indexing='ij')

px = gx.ravel() + rng.uniform(-0.04, 0.04, gx.size)
py = gy.ravel() + rng.uniform(-0.04, 0.04, gy.size)
n = len(px)

positions = np.column_stack([px, py, np.zeros(n)])
scales    = rng.uniform(0.90, 1.10, n)     # ±10% size difference
rotations = rng.uniform(0, 360, n)          # random orientation

scene.add(maize, positions=positions, scales=scales, rotations=rotations)
print(f"Plant {n} corn plants ({n_rows} rows × {n_plant} plants/row)")

# ──Construction─────────────────────────────────────────────────
lai = scene.measure(less.LAIMeasurement())
print(f"Scenario LAI = {lai:.2f}")

# ── Orthophoto RGB image ──────────────────────────────────────────
sensor_nadir = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[650, 550, 450],
    quality=128,
)
image = scene.simulate(sensor_nadir)
image.save("08_maize_field_nadir.png")

# ──Perspective camera (bird's eye view)───────────────────────────────────
half = scene.size / 2
sensor_persp = less.OpticalImager(
    less.Perspective(
        resolution=1024, fov=35,
        position=(half, scene.size + 4, 6),
        target=(half, half, 1.2),
    ),
    bands=[650, 550, 450],
    quality=128,
)
image_persp = scene.simulate(sensor_persp)
image_persp.save("08_maize_field_perspective.png")

print("Tutorial 08 completed!")

Supporting script: scripts/08_crop_field.py

Simulate different growth stages

Crops at different growth stages can be simulated by adjusting the scales parameters:

# Seedling stage: shrink to 30%
scales_seedling = np.full(n, 0.3)

# Jointing stage: 50-60%
scales_jointing = rng.uniform(0.50, 0.60, n)

# Maturity period: 90-110%
scales_mature = rng.uniform(0.90, 1.10, n)

If you only change the zoom but not the position, you can directly batch update through InstanceHandle:

# initial addition
handle = scene.add(maize, positions=positions, scales=scales_seedling, rotations=rotations)
image_seedling = scene.simulate(sensor_nadir)

# Update the entire batch to the mature stage and submit the instance transformation
handle.set_scale(scales_mature)         # Array (length = number of instances)
scene.update_instances()
image_mature = scene.simulate(sensor_nadir)

set_scale / set_rotation / set_position all support two calling methods:

Form Usage Purpose
Single instance handle.set_scale(0.8, instance=3) Debugging/Single point disturbance
Batch handle.set_scale(scales_array) Phenophase, growth dynamics

set_position also supports passing the (N, 3) array to batch update all positions; set_scale also accepts the (N, 3) array for non-uniform scaling (each instance X/Y/Z is scaled separately).

The role of scene repetition

The target semantics of the periodic boundary is to spread the 10m × 10m basic unit periodically, which is equivalent to infinite farmland. It will serve:

  1. BRF calculation: Eliminate light leakage at the edge of the scene
  2. LiDAR Simulation: Airborne scanning may cover a wider area
  3. Atmospheric Scattering: A sufficiently large surface is required as the lower boundary

Currently REPETITIVE_SCENE is released (flat homogeneous terrain only): True for infinite tiles (wrap capped at 100), integer >=5 for explicit capping. This tutorial uses the default False; periodic datums requiring the "homogeneous infinite canopy" assumption can set True directly.

For a single field with clear boundaries (such as a research quadrat), just use scene.repetitive = False - the oblique edge darkening problem in limited scenarios has been automatically corrected by silhouette emission, and there is no need to use periodic boundaries.

Density and planting parameters reference

Crops Typical row spacing (m) Typical plant spacing (m) LAI range
Corn 0.60-0.75 0.20-0.40 2-5
Wheat 0.15-0.25 0.03-0.05 3-7
Soybeans 0.40-0.60 0.05-0.10 3-6
Rice 0.25-0.30 0.15-0.20 4-8

The actual LAI depends on the leaf area of ​​the 3D plant model. The above table is for reference only.

Next step

  • less.Sceneless.Objectless.Terrain
  • less.place.grid()less.place.random_rotation()
  • less.Prospectless.Lambertian
  • less.LAIMeasurementless.Orthographicless.Perspectiveless.OpticalImager