sylva.als¶
sylva.als ¶
Large-area airborne lidar (ALS and UAV lidar): tiles, chunks and buffers.
An airborne survey comes as hundreds of LAS/LAZ tiles, together far larger than memory, and no tile can be processed on its own: a ground filter or a DTM cell at a tile edge needs the points across it. This module follows the catalogue approach of Roussel et al. (2020):
catalogreads only the file headers (extent, point count, point format, CRS) and checks the tiling for overlaps, holes, mixed CRS and mixed formats, without reading a point.- The area is divided into chunks, one per tile or a regular grid
(
chunk_size), each read with abufferof points from its neighbours. Only the files that overlap a chunk are opened, and only the points inside its buffered box are kept. - Chunks run in parallel, as many at a time as the memory budget of
sylva.util.limitsallows, and results are assembled in chunk order, so they do not depend on the number of workers. Point outputs keep only the chunk's own (core) points; raster outputs are joined into one seamless raster on a grid shared by the whole catalogue.
apply runs any function this way; classify_ground,
dtm, chm, normalize, filter,
retile and decimate are built on the same engine, in the
Rust core, as are the area-based metrics of sylva.als.metrics
(grid_metrics, plot_metrics), available here too.
sylva.synthetic.als_flight simulates an airborne survey to try
them on.
Examples:
>>> from sylva import als
>>> cat = als.catalog("tiles/")
>>> print(cat.report())
>>> ground = als.classify_ground(cat, "ground/")
>>> dtm = als.dtm(ground, resolution=1.0)
>>> chm = als.chm(ground, resolution=0.5)
Catalog
dataclass
¶
Tiles of an airborne survey, known from their headers alone.
Build one with catalog; the processing functions of this module
take it and those that write tiles return the catalogue of what they
wrote, so that steps chain. It is plain data and pickles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tiles
|
list[Tile]
|
The readable files, in the order given. |
required |
missing
|
list[str]
|
Paths asked for that do not exist. |
list()
|
unreadable
|
list[tuple[str, str]]
|
|
list()
|
tolerance
|
float
|
How far (m) tile extents may overlap, or fall short of each other,
before |
1.0
|
Notes
A catalogue with missing or unreadable files can be inspected but not
processed: every processing function raises ValueError for it.
bounds
property
¶
bounds: tuple[float, float, float, float, float, float] | None
(xmin, ymin, zmin, xmax, ymax, zmax) over all tiles; None if empty.
issues ¶
issues() -> list[tuple[str, str]]
Everything that could make processing fail or mislead.
Returns:
| Type | Description |
|---|---|
list of (str, str)
|
|
validate ¶
validate() -> None
Raise if issues finds anything.
Raises:
| Type | Description |
|---|---|
ValueError
|
Listing every issue. |
report ¶
report() -> str
A plain-text summary: extent, points, density, formats, CRS and issues.
Returns:
| Type | Description |
|---|---|
str
|
Several lines, ready to print. |
summary ¶
summary() -> dict
The numbers behind report.
Returns:
| Type | Description |
|---|---|
dict
|
|
overlaps ¶
overlaps() -> list[tuple[int, int, float]]
Pairs of overlapping tiles.
Returns:
| Type | Description |
|---|---|
list of (int, int, float)
|
Tile indices |
gaps ¶
gaps() -> list[tuple[tuple[float, float, float, float], float]]
Holes in the coverage.
Returns:
| Type | Description |
|---|---|
list of (tuple, float)
|
The bounding box |
chunks ¶
chunks(chunk_size: float | None = None, buffer: float = 20.0, origin=None) -> list[Chunk]
Divide the catalogue into buffered chunks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chunk_size
|
float | None
|
Side of square chunks (m) on a regular grid; None for one chunk per tile. |
None
|
buffer
|
float
|
Width (m) of the band of neighbouring points read around each chunk. |
20.0
|
origin
|
|
None
|
Returns:
| Type | Description |
|---|---|
list of Chunk
|
West to east within south-to-north rows on a grid; tile order otherwise. Grid squares no tile reaches are left out. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a non-positive |
read ¶
read(bounds=None) -> PointCloud
Read the points inside a box, from whichever tiles hold them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bounds
|
|
None
|
Returns:
| Type | Description |
|---|---|
PointCloud
|
Points in tile order with the LAS attributes that every tile involved has. |
Chunk
dataclass
¶
A piece of a catalogue processed at once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Position in the list of chunks; results come back in this order. |
required |
core
|
tuple[float, float, float, float]
|
|
required |
outer
|
tuple[float, float, float, float]
|
The core grown by the buffer: every point inside is read. |
required |
own
|
int | None
|
For one chunk per tile, the index of that tile in the catalogue: its points are the core, every other file's are buffer. None on a grid. |
required |
files
|
tuple[int, ...]
|
Indices of the tiles overlapping |
required |
est_points
|
int
|
Points expected in |
required |
name
|
str
|
A file name for the chunk's outputs: the tile's own name, or
|
required |
Tile
dataclass
¶
One LAS/LAZ file of a Catalog, as its header describes it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
The file. |
required |
bounds
|
tuple[float, float, float, float, float, float]
|
|
required |
n_points
|
int
|
Point count from the header. |
required |
point_format
|
int
|
LAS point data record format (0-10). |
required |
version
|
tuple[int, int]
|
LAS version as |
required |
crs
|
str | None
|
|
required |
scale
|
tuple[float, float, float]
|
Coordinate quantisation and offset per axis. |
required |
offset
|
tuple[float, float, float]
|
Coordinate quantisation and offset per axis. |
required |
spatial_index
|
bool
|
True with a LAStools |
required |
file_size
|
int
|
Bytes on disk. |
required |
PlotMetrics
dataclass
¶
Metrics of plots, one row per plot, from plot_metrics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
columns
|
dict
|
The table, a dict of equal-length arrays: |
required |
names
|
list
|
The metric columns, in order. |
list()
|
ALSProfile
dataclass
¶
Gap-fraction counts of an area, per grid cell and height layer.
From gap_profile. Layer 0 holds the returns below
min_height (the pulses that got through the canopy), layers 1
to nz the bins of bin_size above it, and the last layer the
returns above the top bin. weight sums each return's share of its
pulse, weight_k that share times its extinction per unit vertical
plant area, G(θ) / cos θ at its beam zenith θ. Because these are
counts, cells and chunks add up exactly; the densities are derived from
them. It is plain data and pickles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xmin
|
float
|
South-west corner of the grid. |
required |
ymin
|
float
|
South-west corner of the grid. |
required |
resolution
|
float
|
Cell size (m). |
required |
min_height
|
float
|
Bottom of the first layer and layer thickness (m). |
required |
bin_size
|
float
|
Bottom of the first layer and layer thickness (m). |
required |
weight
|
ndarray
|
|
required |
weight_k
|
ndarray
|
|
required |
crs
|
str | None
|
CRS of the grid, if known. |
None
|
n_skipped
|
int
|
Returns left out (no trajectory position, or beyond |
0
|
pad ¶
pad() -> np.ndarray
Plant area density of every layer and cell (m²/m³).
Layer i lets through the weight below it out of the weight at
or below it, T; its density is -ln T / (k̄ dz), with k̄
the mean extinction of the returns at or below it (MacArthur & Horn
1969, with the beam angle of each return). A layer nothing passed
gets T = 0.5 / max(E, 1) (half a pulse of the E that
entered), a lower bound (not NaN).
Returns:
| Type | Description |
|---|---|
ndarray
|
|
pai ¶
pai() -> Raster
Plant area index of each cell above min_height.
-ln P / k̄, with P the share of the weight below
min_height (the gap probability) and k̄ the mean extinction
of all returns; saturated cells (no return below) as in pad.
With one beam angle this is the sum of the layer densities times
bin_size.
Returns:
| Type | Description |
|---|---|
Raster
|
NaN for cells without returns. |
cover ¶
cover() -> Raster
profile ¶
profile(mask=None) -> tuple[np.ndarray, np.ndarray]
Plant area density profile of an area, from its pooled counts.
The counts of the cells are added before the densities are derived, so this is the profile of the area seen as one column: an effective profile, lower than the mean of the cells' profiles where the canopy is clumped between cells.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
height |
ndarray
|
Bottom of each layer (m). |
pad |
ndarray
|
Plant area density (m²/m³); NaN above the highest return. |
pgap ¶
pgap(mask=None) -> tuple[np.ndarray, np.ndarray]
Gap probability profile of an area: the share of the pooled weight below each layer boundary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
height |
ndarray
|
|
pgap |
ndarray
|
Gap probability at each height (not corrected for beam angle). |
pooled_pai ¶
pooled_pai(mask=None) -> float
Plant area index of an area from its pooled counts (see profile).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
|
None
|
Returns:
| Type | Description |
|---|---|
float
|
|
metrics ¶
metrics(strata: float = 5.0) -> dict[str, Raster]
Summary metrics of each cell's profile, as rasters, for use
beside the height metrics of sylva.als.grid_metrics in a
model of height, cover or biomass.
pulses: the cell's weight (its number of pulses with the"equal"weighting); a check on how well the cell is sampled.paiandcover: aspaiandcover.fhd: foliage height diversity, the Shannon index-sum(p ln p)of the sharespof the plant area in each layer (MacArthur & MacArthur 1961; GEDI L2Bfhd_normal). 0 where there is no plant area.pad_maxandheight_pad_max: the density of the densest layer and the height of its middle.height_pad_meanandheight_pad_sd: the mean and standard deviation of height weighted by plant area: where the canopy is and how deep it is.- For each stratum
[a, b)ofstratam from the ground (the strata of GEDI L2B withstrata=5):pavd_<a>_<b>, its mean plant area density;pai_above_<h>, the plant area index aboveh, the bottom of the stratum's lowest layer;cover_above_<h>, the canopy cover ath.
A layer belongs to the stratum its middle falls in, and strata
below min_height are left out (so with min_height=1 the
lowest stratum is pavd_0_5 over 1 to 5 m). Layers no pulse
reached are left out of every sum. pai corrects each return for
its own beam angle; the layer sums behind pai_above_<h>
correct each layer by the mean angle of the returns that reached
it, so the two can differ slightly under a wide swath.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
strata
|
float
|
Thickness of the strata (m); at least |
5.0
|
Returns:
| Type | Description |
|---|---|
dict[str, Raster]
|
One raster per metric, in a fixed order. Cells without returns
are NaN, but for |
area_metrics ¶
area_metrics(mask=None, strata: float = 5.0) -> dict[str, float]
The metrics of metrics for an area, from its pooled counts
(the area seen as one column, as profile).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
|
None
|
|
strata
|
float
|
Thickness of the strata (m). |
5.0
|
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
|
plot_cells ¶
plot_cells(plots, radius=None) -> list[np.ndarray]
The cells of each plot: those whose centre lies inside it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plots
|
As for |
required | |
radius
|
As for |
required |
Returns:
| Type | Description |
|---|---|
list of numpy.ndarray
|
Row-major cell indices ( |
plot_metrics ¶
plot_metrics(plots, radius=None, ids=None, strata: float = 5.0)
The metrics of metrics for field plots, each from the
pooled counts of the cells whose centre lies inside it, as a table
to join with sylva.als.plot_metrics and the plot data.
The plot is represented by whole cells, so use a resolution well
below the plot size (a 2 m grid for a 0.1 ha plot). Because the
counts add up exactly, pooling small cells gives the profile of the
plot itself. area gives the area of the cells used.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plots
|
As for |
required | |
radius
|
As for |
required | |
ids
|
Optional identifiers, one per plot. |
None
|
|
strata
|
float
|
Thickness of the strata (m). |
5.0
|
Returns:
| Type | Description |
|---|---|
PlotMetrics
|
Columns |
ALSVoxels
dataclass
¶
Ray-traced voxel fields of an airborne survey, from ray_voxelize.
The lattice is anchored at multiples of voxel_size; every array is
(nz, ny, nx) with row 0 at the south and layer 0 at the bottom.
Columns no chunk traced are NaN. It is plain data and pickles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
origin
|
ndarray
|
|
required |
voxel_size
|
float
|
Voxel edge (m). |
required |
fields
|
dict[str, ndarray]
|
The fields and metrics asked for, by name (see
|
required |
reach
|
float
|
The largest horizontal distance (m) a pulse covered between the top of the grid and its last return: the buffer that brings every pulse crossing a chunk into it. |
0.0
|
n_pulses
|
int
|
Pulses traced (a pulse within reach of several chunks counts in each). |
0
|
crs
|
str | None
|
CRS, if known. |
None
|
profile ¶
profile(name: str | None = None, dtm: Raster | None = None, bin_size: float | None = None, min_beams: float = 1.0, mask=None) -> tuple[np.ndarray, np.ndarray]
Mean of a field per height above ground.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Field; the first |
None
|
dtm
|
Raster | None
|
Terrain: heights are those of voxel centres above it. Without one, above the grid floor. |
None
|
bin_size
|
float | None
|
Height bin (m); the voxel size if None. |
None
|
min_beams
|
float
|
Voxels crossed by fewer pulses are left out. |
1.0
|
mask
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
height |
ndarray
|
Bottom of each bin, from 0. |
mean |
ndarray
|
NaN for bins without voxels. |
pai ¶
pai(name: str | None = None, dtm: Raster | None = None, min_height: float = 0.0, min_beams: float = 1.0) -> Raster
Plant area index of each column: the sum of a density field times the voxel size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Density field; the first |
None
|
dtm
|
Raster | None
|
Terrain, for |
None
|
min_height
|
float
|
Voxels whose centre is lower above the DTM are left out (the ground itself, understorey). |
0.0
|
min_beams
|
float
|
Voxels crossed by fewer pulses count as 0. |
1.0
|
Returns:
| Type | Description |
|---|---|
Raster
|
|
Trajectory
dataclass
¶
The path of the scanner: positions, and optionally attitude, in time.
Build one with read_trajectory or estimate_trajectory,
or from arrays. Samples are sorted by time and a sample repeating an
earlier time is dropped. It is plain data and pickles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
time
|
ndarray
|
GPS time of each sample (s), on the clock of the returns'
|
required |
x
|
ndarray
|
Sensor position (m), in the frame of the points. |
required |
y
|
ndarray
|
Sensor position (m), in the frame of the points. |
required |
z
|
ndarray
|
Sensor position (m), in the frame of the points. |
required |
roll
|
ndarray | None
|
Attitude (degrees; heading clockwise from north), if known. |
None
|
pitch
|
ndarray | None
|
Attitude (degrees; heading clockwise from north), if known. |
None
|
heading
|
ndarray | None
|
Attitude (degrees; heading clockwise from north), if known. |
None
|
rms
|
ndarray | None
|
For an estimated trajectory: the root mean square distance (m) of the pulse lines from each sample, the pulses used, and the flight line. |
None
|
n_pulses
|
ndarray | None
|
For an estimated trajectory: the root mean square distance (m) of the pulse lines from each sample, the pulses used, and the flight line. |
None
|
line
|
ndarray | None
|
For an estimated trajectory: the root mean square distance (m) of the pulse lines from each sample, the pulses used, and the flight line. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Fewer than two samples at different times, arrays of different lengths, or non-finite values. |
from_dict
classmethod
¶
from_dict(d: Mapping) -> Trajectory
From a mapping of arrays, such as sylva.synthetic.ALSFlight.trajectory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
d
|
Mapping
|
|
required |
Returns:
| Type | Description |
|---|---|
Trajectory
|
|
positions ¶
positions(gps_time, max_gap: float | None = None, time_offset: float = 0.0) -> np.ndarray
Sensor position at each time, linearly interpolated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gps_time
|
Times, e.g. |
required | |
max_gap
|
float | None
|
Longest gap between samples (s) to interpolate across; ten median sample intervals if None. |
None
|
time_offset
|
float
|
Seconds added to |
0.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
attitude ¶
attitude(gps_time, max_gap: float | None = None, time_offset: float = 0.0) -> np.ndarray
Roll, pitch and heading at each time, linearly interpolated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gps_time
|
Times. |
required | |
max_gap
|
float | None
|
As for |
None
|
time_offset
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the trajectory has no attitude. |
LinearWindow
dataclass
¶
A window size that grows linearly with height:
clip(intercept + slope * h, min, max) metres for a site h m high.
Calling it on heights gives the sizes, so it can stand wherever a window function is taken; the Rust core evaluates it directly, which keeps catalogue runs exact.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
intercept
|
float
|
Size (m) at height 0 and its increase per metre of height. |
required |
slope
|
float
|
Size (m) at height 0 and its increase per metre of height. |
required |
min
|
float
|
Bounds on the size (m); |
1.0
|
max
|
float
|
Bounds on the size (m); |
1.0
|
Examples:
A window of x * 0.1 + 3 m, bounded:
>>> w = LinearWindow(3.0, 0.1, min=3.0, max=8.0)
>>> float(w(20.0))
5.0
Trees
dataclass
¶
Trees found by segment_trees or find_trees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id
|
ndarray
|
1, 2, ... in order of the tops' x, then y. |
required |
x
|
ndarray
|
The top: a CHM cell centre and its value, or the highest point of the tree and its height (m). |
required |
y
|
ndarray
|
The top: a CHM cell centre and its value, or the highest point of the tree and its height (m). |
required |
height
|
ndarray
|
The top: a CHM cell centre and its value, or the highest point of the tree and its height (m). |
required |
crown_area
|
ndarray
|
Area (m²) of the crown outline; NaN without a crown. |
required |
n_points
|
ndarray
|
Points labelled with the tree. |
required |
crowns
|
list
|
Crown outline of each tree, |
list()
|
tree_id
|
ndarray | None
|
For |
None
|
catalog
|
Catalog | None
|
For |
None
|
at_edge
|
int
|
Crowns that reached the inner edge of their chunk's buffer; more than 0 means the buffer may be too narrow for them to be complete. |
0
|
unmatched_points
|
int
|
Points written with id 0 because the tree their chunk gave them was found by no chunk as its own; 0 with an adequate buffer. |
0
|
crs
|
str | None
|
Coordinate system of the positions. |
None
|
table ¶
table() -> dict[str, np.ndarray]
The per-tree columns: id, x, y, height,
crown_area and n_points.
to_csv ¶
to_csv(path: str | Path) -> None
Write table as CSV, one row per tree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
to_geojson ¶
to_geojson(path: str | Path, geometry: str = 'crowns') -> None
Write the trees as a GeoJSON FeatureCollection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
geometry
|
('crowns', 'tops')
|
Crown polygons (trees without a crown of 3 vertices are left
out) or top points; the |
"crowns"
|
TreeTops
dataclass
¶
Tree tops from locate_trees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
ndarray
|
Position of each top (m): a CHM cell centre or a point. |
required |
y
|
ndarray
|
Position of each top (m): a CHM cell centre or a point. |
required |
height
|
ndarray
|
The CHM value or the point's height (m). |
required |
index
|
ndarray
|
Row-major cell index in the CHM, or the point's index in the cloud. |
required |
catalog ¶
catalog(paths, pattern: str = '*.la[sz]', recursive: bool = False, tolerance: float = 1.0) -> Catalog
Build a catalogue from LAS/LAZ files, reading only their headers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
A directory, a file, or a list of either. Directories are searched
for |
required | |
pattern
|
str
|
File name pattern (shell style, case-insensitive) within directories. |
'*.la[sz]'
|
recursive
|
bool
|
Search directories' subdirectories too. |
False
|
tolerance
|
float
|
See |
1.0
|
Returns:
| Type | Description |
|---|---|
Catalog
|
Tiles in sorted path order within each directory. Missing and
unreadable files are recorded rather than raised; see
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If nothing matches. |
apply ¶
apply(catalog: Catalog, fn: Callable, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None, out: str | Path | None = None, format: str | None = None, origin=None)
Run a function over a catalogue, chunk by chunk, with buffers.
Each chunk is read (only the files overlapping it, only the points in
its buffered box) and handed to fn as fn(cloud, chunk).
cloud carries a boolean buffer attribute, True for the points
around the chunk that belong to its neighbours; chunk is the
Chunk. Chunks with no points of their own are skipped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
From |
required |
fn
|
Callable
|
|
required |
chunk_size
|
float | None
|
Side of square chunks (m); None for one chunk per tile. |
None
|
buffer
|
float
|
Width (m) of the band of neighbouring points around each chunk.
Make it at least as wide as whatever |
20.0
|
workers
|
int | None
|
Chunks processed at once; the number of CPUs if None. Fewer are
run when that many chunks would not fit in the memory budget
( |
None
|
out
|
str | Path | None
|
Directory for point outputs: each chunk's core points are written
to |
None
|
format
|
str | None
|
|
None
|
origin
|
Grid corner for |
None
|
Returns:
| Type | Description |
|---|---|
(PointCloud, Catalog, Raster or list)
|
What |
Raises:
| Type | Description |
|---|---|
ValueError
|
For bad arguments, a catalogue with missing files, a chunk too large
for the memory budget, or rasters that do not line up. Whatever
|
Examples:
Any single-cloud function can run this way. A canopy height model from
tiles already normalised, on the catalogue grid (bounds from the
chunk's buffered box, snapped to the cells, keeps each chunk's raster on
that grid):
>>> def canopy(cloud, chunk):
... x0, y0, x1, y1 = chunk.outer
... b = (np.floor(x0), np.floor(y0), x1, y1)
... return ground.make_chm(cloud, 1.0, bounds=b)
>>> chm = als.apply(cat, canopy, buffer=0)
Numbers per chunk come back as a list:
>>> counts = als.apply(cat, lambda c, ch: int((~c.attrs["buffer"]).sum()))
chm ¶
chm(catalog: Catalog, resolution: float = 0.5, dtm='auto', dtm_resolution: float = 1.0, min_height: float = 0.0, drop_noise: bool = True, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None) -> Raster
Canopy height model of a whole catalogue: the highest point per cell.
Heights above ground are computed per chunk, then gridded as
sylva.ground.make_chm grids them (the "point to raster" method).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
The tiles. |
required |
resolution
|
float
|
Cell size (m). |
0.5
|
dtm
|
('auto', None or Raster)
|
Where heights come from. |
'auto'
|
dtm_resolution
|
float
|
Cell size (m) of the |
1.0
|
min_height
|
float
|
Cells with nothing at or above this height are 0. |
0.0
|
drop_noise
|
bool
|
Leave out points classified as noise (7 or 18), as
|
True
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
Raster
|
Maximum height per cell (m), on the catalogue grid, with the
catalogue's CRS. No pit filling. Cells in chunks without enough
ground for |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a bad |
classify_ground ¶
classify_ground(catalog: Catalog, out: str | Path, method: str = 'csf', last_returns: bool = False, cloth_resolution: float = 0.5, rigidness: int = 2, class_threshold: float = 0.3, iterations: int = 500, time_step: float = 0.65, cell_size: float = 0.5, max_window: float = 10.0, slope: float = 0.3, initial_distance: float = 0.15, max_distance: float = 2.0, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None, format: str | None = None) -> Catalog
Classify ground over a whole catalogue and write the classified tiles.
The filter of sylva.ground.classify_ground_csf or
sylva.ground.classify_ground_pmf runs on each tile with its
buffer, and each tile's own points are written with classification
2 (ground) or 1 (other). Points already classified as noise (7 or 18)
take no part and keep their class. On a single tile, with the same
parameters, the classes are those of the single-cloud function.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
The tiles. |
required |
out
|
str | Path
|
Directory for the classified tiles (created if needed), one per input tile with the same name. |
required |
method
|
('csf', 'pmf')
|
Cloth simulation or progressive morphological filter. |
"csf"
|
last_returns
|
bool
|
Only last returns ( |
False
|
cloth_resolution
|
float
|
CSF parameters, as in |
0.5
|
rigidness
|
float
|
CSF parameters, as in |
0.5
|
class_threshold
|
float
|
CSF parameters, as in |
0.5
|
iterations
|
float
|
CSF parameters, as in |
0.5
|
time_step
|
float
|
CSF parameters, as in |
0.5
|
cell_size
|
float
|
PMF parameters, as in |
0.5
|
max_window
|
float
|
PMF parameters, as in |
0.5
|
slope
|
float
|
PMF parameters, as in |
0.5
|
initial_distance
|
float
|
PMF parameters, as in |
0.5
|
max_distance
|
float
|
PMF parameters, as in |
0.5
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
format
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
Catalog
|
The written tiles. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown method, bad parameters, or an output that would overwrite an input tile. |
decimate ¶
decimate(catalog: Catalog, out: str | Path, method: str = 'random', fraction: float = 0.5, size: float = 1.0, seed: int = 0, chunk_size: float | None = None, workers: int | None = None, format: str | None = None) -> Catalog
Thin a catalogue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
The tiles. |
required |
out
|
str | Path
|
Directory for the thinned tiles. |
required |
method
|
('random', 'voxel', 'highest')
|
|
"random"
|
fraction
|
float
|
Share of points kept by |
0.5
|
size
|
float
|
Voxel or cell size (m). |
1.0
|
seed
|
int
|
Seed of |
0
|
chunk_size
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
format
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
Catalog
|
The written tiles. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown method, a fraction outside 0-1 or a non-positive size. |
dtm ¶
dtm(catalog: Catalog, resolution: float = 1.0, method: str = 'lowest', power: float = 2.0, k: int = 12, max_distance: float | None = None, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None) -> Raster
Digital terrain model of a whole catalogue, seamless across tiles.
Each chunk's DTM is made from its ground points (classification ==
2) with its buffer, as sylva.ground.make_dtm makes it, on one
grid for the whole catalogue; each cell is then taken from the chunk
whose core holds it. Away from the outer edge of the survey the result
equals the DTM of all the tiles merged into one cloud.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
Tiles with ground classified (e.g. by |
required |
resolution
|
float
|
Cell size (m). |
1.0
|
method
|
('lowest', 'tin', 'natural', 'idw')
|
As in |
"lowest"
|
power
|
float
|
IDW settings (see |
2.0
|
k
|
float
|
IDW settings (see |
2.0
|
max_distance
|
float
|
IDW settings (see |
2.0
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
Raster
|
On the catalogue grid (south-west corner at its minimum snapped down
to a multiple of |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown method or a non-positive resolution; if the tiles
have no |
filter ¶
filter(catalog: Catalog, out: str | Path, method: str = 'ror', radius: float = 2.0, min_neighbors: int = 3, k: int = 8, std_ratio: float = 3.0, classify: bool = False, chunk_size: float | None = None, buffer: float = 10.0, workers: int | None = None, format: str | None = None) -> Catalog
Remove noise (isolated points: birds, multipath, atmospheric returns).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
The tiles. |
required |
out
|
str | Path
|
Directory for the filtered tiles, one per input tile. |
required |
method
|
('ror', 'sor')
|
|
"ror"
|
radius
|
float
|
|
2.0
|
min_neighbors
|
float
|
|
2.0
|
k
|
int
|
|
8
|
std_ratio
|
int
|
|
8
|
classify
|
bool
|
Keep every point and set |
False
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
format
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
Catalog
|
The written tiles. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown method or non-positive settings. |
normalize ¶
normalize(catalog: Catalog, out: str | Path, dtm='auto', dtm_resolution: float = 1.0, replace_z: bool = False, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None, format: str | None = None) -> Catalog
Height above ground for every point of a catalogue, written as tiles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
The tiles. |
required |
out
|
str | Path
|
Directory for the normalised tiles, one per input tile. |
required |
dtm
|
'auto' or Raster
|
|
'auto'
|
dtm_resolution
|
float
|
Cell size (m) of the |
1.0
|
replace_z
|
bool
|
Replace z by the height and keep the elevation in an |
False
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
format
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
Catalog
|
The written tiles. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a bad |
retile ¶
retile(catalog: Catalog, out: str | Path, size: float, buffer: float = 0.0, origin=None, workers: int | None = None, format: str | None = None) -> Catalog
Cut a catalogue into new square tiles.
Each input file is read once, whatever its extent: its points are sent to
the tiles that hold them, held in temporary part files under out,
and each tile is then assembled from its parts. Flight lines and other
large files without a spatial index, which every chunk of the other
functions would decompress in full, are best retiled first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
Catalog
|
The tiles. |
required |
out
|
str | Path
|
Directory for the new tiles, named |
required |
size
|
float
|
Side of the new tiles (m). |
required |
buffer
|
float
|
With a buffer (m), each tile also holds the points within that
distance of it, flagged by a |
0.0
|
origin
|
|
None
|
|
workers
|
int | None
|
As for |
None
|
format
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
Catalog
|
The new tiles. Every input point is in exactly one of them (not counting buffers). |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a non-positive size or negative buffer. |
write_tiles ¶
write_tiles(cloud: PointCloud, out: str | Path, size: float, origin=None, format: str = 'laz', point_format: int = 6, scale: float = 0.001, epsg: int | None = None, tolerance: float = 1.0) -> Catalog
Write a cloud as square LAS/LAZ tiles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
Points; attributes are written as in |
required |
out
|
str | Path
|
Directory for the tiles, named |
required |
size
|
float
|
Tile side (m). |
required |
origin
|
|
None
|
|
format
|
str
|
|
'laz'
|
point_format
|
int
|
LAS point data record format. |
6
|
scale
|
float
|
Coordinate quantisation (m). |
0.001
|
epsg
|
int | None
|
EPSG code recorded in each file (as GeoTIFF keys), so that the catalogue knows its CRS. |
None
|
tolerance
|
float
|
For the returned catalogue. |
1.0
|
Returns:
| Type | Description |
|---|---|
Catalog
|
The tiles written (empty tiles are not written). |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a non-positive size, an unknown format or non-finite coordinates. |
cloud_metrics ¶
cloud_metrics(cloud: PointCloud, height=None, threshold: float = 2.0, entropy_bin: float = 1.0, cover_break: float = 2.0, min_height: float | None = None, drop_noise: bool = True, clamp_negative: bool = False) -> dict[str, float]
The standard metrics of one cloud.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
Returns, normalised (z is height above ground) unless |
required |
height
|
None to take z as the height, the name of an attribute holding it
( |
None
|
|
threshold
|
float
|
Height (m) of |
2.0
|
entropy_bin
|
float
|
Bin width (m) of |
1.0
|
cover_break
|
float
|
Height (m) above which a first return counts as canopy for
|
2.0
|
min_height
|
float | None
|
Returns lower than this are left out of every metric (heights below 0 are
often dropped first, since |
None
|
drop_noise
|
bool
|
Leave out returns classified as noise (7 or 18). |
True
|
clamp_negative
|
bool
|
Set heights below 0 to 0 before anything else (the other usual
habit). Ground returns a few centimetres below
the DTM are in nearly every cell of a survey, and make |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
Metric name to value. The intensity and return metrics are present
when the cloud has |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown attribute, heights of the wrong length or bad settings. |
grid_metrics ¶
grid_metrics(catalog, resolution: float = 20.0, metrics=None, func: Callable | None = None, dtm='auto', dtm_resolution: float = 1.0, threshold: float = 2.0, entropy_bin: float = 1.0, cover_break: float = 2.0, min_height: float | None = None, drop_noise: bool = True, clamp_negative: bool = False, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None)
Rasters of area-based metrics over a whole catalogue.
Every return is assigned to the cell of the catalogue grid that holds
it (cells are half-open, [x0, x0 + resolution)), and the metrics of
each cell are computed from all of its returns, whichever tiles they
come from. Each chunk computes the cells within about one cell of its
core from its points and its buffer, so the result is the same, to the
bit, for any chunk_size and any number of workers, and equals
the metrics of the cells of all the tiles merged into one cloud.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
The tiles (a |
required | |
resolution
|
float
|
Cell size (m); 10 to 30 m is usual for area-based models. |
20.0
|
metrics
|
None for every standard metric ( |
None
|
|
func
|
Callable | None
|
A function of one cell's returns, |
None
|
dtm
|
('auto', None, Raster or str)
|
Where heights come from, as for |
'auto'
|
dtm_resolution
|
float
|
Cell size (m) of the |
1.0
|
threshold
|
float
|
As for |
2.0
|
entropy_bin
|
float
|
As for |
2.0
|
cover_break
|
float
|
As for |
2.0
|
min_height
|
float
|
As for |
2.0
|
drop_noise
|
float
|
As for |
2.0
|
clamp_negative
|
float
|
As for |
2.0
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
int | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
dict of str to Raster, or Raster
|
One |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown metric, bad settings, a |
Examples:
>>> m = als.grid_metrics(cat, 20.0, ["zmean", "zq95", "cover"])
>>> top = als.grid_metrics(cat, 20.0, "zmax", dtm=None)
>>> def canopy(c):
... z = c.z[c.z > 2]
... return {"hmean": z.mean() if len(z) else np.nan, "npts": len(c)}
>>> mine = als.grid_metrics(cat, 20.0, func=canopy)
metric_names ¶
metric_names(threshold: float = 2.0) -> list[str]
Names of the standard metrics, in the order they are computed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
threshold
|
float
|
The height of |
2.0
|
Returns:
| Type | Description |
|---|---|
list of str
|
|
plot_metrics ¶
plot_metrics(catalog, plots, radius=None, metrics=None, func: Callable | None = None, dtm='auto', dtm_resolution: float = 1.0, threshold: float = 2.0, entropy_bin: float = 1.0, cover_break: float = 2.0, min_height: float | None = None, drop_noise: bool = True, clamp_negative: bool = False, ids=None, buffer: float = 20.0, workers: int | None = None) -> PlotMetrics
Area-based metrics of field plots from a catalogue.
Plots are grouped by the tiles they overlap; each group reads only
those tiles, and only the points in the group's box (grown by
buffer for dtm="auto"). A plot on a tile edge gets its points
from every tile it overlaps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
The tiles. |
required | |
plots
|
With |
required | |
radius
|
Radius (m) of circular plots, one for all or one per plot. |
None
|
|
metrics
|
None for every standard metric, or a list of names. |
None
|
|
func
|
Callable | None
|
A function of one plot's returns, as for |
None
|
dtm
|
Where heights come from, as for |
'auto'
|
|
dtm_resolution
|
Where heights come from, as for |
'auto'
|
|
threshold
|
float
|
As for |
2.0
|
entropy_bin
|
float
|
As for |
2.0
|
cover_break
|
float
|
As for |
2.0
|
min_height
|
float
|
As for |
2.0
|
drop_noise
|
float
|
As for |
2.0
|
clamp_negative
|
float
|
As for |
2.0
|
ids
|
Labels of the plots (one per plot), kept as an |
None
|
|
buffer
|
float
|
With |
20.0
|
workers
|
int | None
|
Groups of plots read at once, as for |
None
|
Returns:
| Type | Description |
|---|---|
PlotMetrics
|
One row per plot, in the order given. Plots outside every tile have
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For bad plots (a non-positive radius, a ring with fewer than three vertices), an unknown metric or bad settings. |
Examples:
>>> t = als.plot_metrics(cat, [(520.0, 310.0), (580.5, 402.0)], radius=11.28)
>>> t["zq95"], t.to_pandas()
>>> layer = masks.read_polygons("plots.shp")
>>> names = [f.properties["name"] for f in layer]
>>> t = als.plot_metrics(cat, layer, ids=names)
gap_profile ¶
gap_profile(source, trajectory=None, resolution: float = 10.0, bin_size: float = 1.0, min_height: float = 1.0, max_height: float | None = None, top_quantile: float = 0.99999, drop_noise: bool = True, weighting: str = 'equal', angles: str = 'auto', lad: str = 'spherical', lad_params: Sequence[float] = (), g: float | None = None, max_zenith: float = 90.0, anchor: str = 'ground', dtm='auto', dtm_resolution: float = 1.0, bounds=None, max_gap: float | None = None, time_offset: float = 0.0, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None) -> ALSProfile
Gap-fraction profiles, plant area density and plant area index.
The returns of each grid cell are binned by height above ground. A
pulse passes a height if it has weight below it, so the gap
probability at height z is the share of the cell's weighted returns
below z (Armston et al. 2013), and the transmittance of a layer the
weight below it over the weight at or below it. Inverting Beer-Lambert
layer by layer gives the plant area density profile of MacArthur & Horn
(1969), as computed by Bouvier et al. (2015); that is
weighting="all", angles="none", g=0.5 here.
Scan angle. A beam at zenith θ crosses dz / cos θ of a layer of
thickness dz and meets foliage in proportion to G(θ), the
projection of the leaf angle distribution, so each return carries the
extinction k = G(θ) / cos θ per unit of vertical plant area, and a
layer's density divides -ln T by the mean k of the returns that
reached it. Without this, returns from the edge of a 30° swath read 15 %
denser than at nadir. θ comes from the trajectory (the angle of the
line from each return to the sensor), else from the LAS scan_angle
(which includes roll but not pitch).
Which cell. An oblique pulse stopped in the canopy leaves its return
to one side of where it would have reached the ground. Counted where
they are, the returns of a cell come from pulses picked partly by their
outcome, which biases the gap probability wherever the pulse density
varies across the swath (by 5 % on the synthetic layer of the guide).
With a trajectory each return is therefore counted, by default, in the
cell where its beam meets the ground (anchor="ground"): the pulses
of a cell are then chosen by their geometry alone. Without a trajectory
the returns stay where they are.
Every return is counted once, so a catalogue gives the same counts,
cell for cell, whatever the chunks, as long as the buffer is wider than
the drift of a beam between its returns and the ground (canopy height
times the tangent of the scan angle); the buffer also serves the
"auto" DTM.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
A |
required | |
trajectory
|
|
None
|
|
resolution
|
float
|
Cell size (m). Cells should hold a few hundred pulses; 10-30 m is usual. |
10.0
|
bin_size
|
float
|
Layer thickness (m). |
1.0
|
min_height
|
float
|
Bottom of the lowest layer (m): returns below it, ground included, are pulses that went through. |
1.0
|
max_height
|
float | None
|
Top of the highest layer; returns above it are intercepted above
every layer. By default the layers stop where |
None
|
top_quantile
|
float
|
With |
0.99999
|
drop_noise
|
bool
|
Leave out returns classified as noise (7 or 18). |
True
|
weighting
|
('equal', 'first', 'all')
|
Share of a pulse each return stands for: |
"equal"
|
angles
|
('auto', 'trajectory', 'scan_angle', 'none')
|
Source of the beam zenith; |
"auto"
|
lad
|
str
|
Leaf angle distribution for |
'spherical'
|
lad_params
|
str
|
Leaf angle distribution for |
'spherical'
|
g
|
float | None
|
A constant |
None
|
max_zenith
|
float
|
Returns whose beam is further than this from nadir (degrees) are left out. |
90.0
|
anchor
|
('ground', 'return')
|
Count each return in the cell where its beam meets the ground (needs the trajectory; see above), or where the return is. |
"ground"
|
dtm
|
('auto', None or Raster)
|
Heights: |
'auto'
|
dtm_resolution
|
float
|
Cell size (m) of the |
1.0
|
bounds
|
|
None
|
|
max_gap
|
float | None
|
As for |
None
|
time_offset
|
float | None
|
As for |
None
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
ALSProfile
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For bad settings, a weighting whose attributes are missing, or angles that cannot be had. |
ray_voxelize ¶
ray_voxelize(source, trajectory, voxel_size: float = 1.0, fields: Sequence[str] | None = None, *, dtm: Raster | None = None, ground_class: int | None = 2, ground_distance: float = 0.2, class_attr: str = 'classification', leaf_classes: Sequence[int] = (), wood_classes: Sequence[int] = (), tree_attr: str = 'tree_id', intensity_attr: str = 'intensity', weighting: str = 'equal', attenuation: str | Sequence[str] = 'fpl', laser: str | None = None, beam: tuple[float, float] | None = None, lad: str = 'spherical', lad_params: Sequence[float] = (), average_leaf_area: float = 0.005, occlusion: bool = False, unbounded_range: float = np.inf, bounds=None, z_range=None, max_gap: float | None = None, time_offset: float = 0.0, fill_missing: bool = False, max_fill: int = 8, drop_incomplete: bool = False, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None) -> ALSVoxels
Trace the pulses of a survey through one voxel lattice.
The returns are grouped into pulses from the sensor as pulses
does, and traced by the ray tracer of sylva.voxels.ray_voxelize
along their actual beam directions, so every voxel's path lengths,
hits and mean beam angle, and so its plant area density, account for the
scan angle.
On a catalogue, each chunk reconstructs the pulses of its core and
buffer points and traces them through the columns whose centres lie in
its core; the columns are put together into one lattice anchored at the
catalogue's minimum corner snapped down to a multiple of
voxel_size. A pulse that crosses a chunk's core from outside it is
traced there as long as one of its returns lies in the buffer. That
needs a buffer at least the height of the grid above the pulse's last
return times the tangent of its zenith (for a 30 m canopy and a 30°
swath, 17 m); ALSVoxels.reach reports the largest such distance
among the pulses traced, and a warning is given when it exceeds the
buffer. With a sufficient buffer the result does not depend on the
chunks, up to floating-point rounding.
Use z as measured, not heights above ground: normalising moves each return by a different amount and bends the rays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
A |
required | |
trajectory
|
|
required | |
voxel_size
|
float
|
Voxel edge (m). Memory is about 0.4 kB per voxel of a chunk while tracing, and 8 bytes per voxel and field of the result. |
1.0
|
fields
|
Sequence[str] | None
|
Fields and metrics to keep (see |
None
|
dtm
|
Raster | None
|
Echo labels, as for |
None
|
ground_class
|
Raster | None
|
Echo labels, as for |
None
|
ground_distance
|
Raster | None
|
Echo labels, as for |
None
|
class_attr
|
Raster | None
|
Echo labels, as for |
None
|
leaf_classes
|
Sequence[int]
|
As for |
()
|
wood_classes
|
Sequence[int]
|
As for |
()
|
tree_attr
|
Sequence[int]
|
As for |
()
|
intensity_attr
|
Sequence[int]
|
As for |
()
|
weighting
|
str
|
As for |
'equal'
|
attenuation
|
str
|
As for |
'equal'
|
laser
|
str
|
As for |
'equal'
|
beam
|
str
|
As for |
'equal'
|
lad
|
str
|
As for |
'equal'
|
lad_params
|
str
|
As for |
'equal'
|
average_leaf_area
|
float
|
As for |
0.005
|
occlusion
|
float
|
As for |
0.005
|
unbounded_range
|
float
|
As for |
0.005
|
bounds
|
|
None
|
|
z_range
|
|
None
|
|
max_gap
|
float | None
|
As for |
None
|
time_offset
|
float | None
|
As for |
None
|
fill_missing
|
float | None
|
As for |
None
|
max_fill
|
float | None
|
As for |
None
|
drop_incomplete
|
float | None
|
As for |
None
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
ALSVoxels
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For bad settings, a lattice too large for the memory budget, or returns outside the trajectory. |
Warns:
| Type | Description |
|---|---|
UserWarning
|
When a catalogue's buffer is narrower than the pulses' reach. |
estimate_trajectory ¶
estimate_trajectory(source, interval: float = 0.5, min_pulses: int = 30, min_separation: float = 2.0, max_pulses: int = 4000, extend: float = 2.0, workers: int | None = None) -> Trajectory
An approximate trajectory from the returns themselves.
Every pulse with two or more returns defines a line through the
scanner: from its last return through its first. Within a short time
window the platform flies a nearly straight line at nearly constant
speed, so its position and velocity there follow from a linear least
squares fit of a moving point a + b (t - t_c) to those lines. This
is the idea of Roussel et al. (2020), after Gatziolis & McGaughey
(2019), which fits a fixed point per window; the
motion within the window is modelled here, so longer windows can be used
without the position lagging.
Windows of interval seconds are anchored at multiples of
interval in GPS time, per flight line (point_source_id). Each
window with min_pulses usable pulses (first and last returns at
least min_separation apart, the max_pulses widest used) gives a
sample at the mean time of its pulses; the first and last windows of a
line also give samples towards the line's first and last returns,
extrapolated with the fitted velocity by at most extend seconds (over
open ground a line has no multiple returns to fit). Lines more than three median distances (and
5 cm) from the first fit are left out of a second one, and a window
whose sensor comes out below its returns is rejected.
The result is approximate: it depends on the multiple returns (few over
open ground or sparse canopy), carries no attitude, and cannot follow
the platform's wobble within a window. Check rms and use a measured
trajectory where one exists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
A |
required | |
interval
|
float
|
Window length (s). |
0.5
|
min_pulses
|
int
|
Fewest usable pulses in a window. |
30
|
min_separation
|
float
|
Least first-to-last distance (m) of a usable pulse. |
2.0
|
max_pulses
|
int
|
Most pulses fitted per window. |
4000
|
extend
|
float
|
Longest extrapolation (s) at each end of a line. |
2.0
|
workers
|
int | None
|
Tiles read at once for a catalogue. |
None
|
Returns:
| Type | Description |
|---|---|
Trajectory
|
With |
Raises:
| Type | Description |
|---|---|
ValueError
|
For bad settings, points without |
pulses ¶
pulses(cloud: PointCloud, trajectory, max_gap: float | None = None, time_offset: float = 0.0, fill_missing: bool = False, max_fill: int = 8, drop_incomplete: bool = False, report: bool = False)
Group discrete returns into pulses fired from the sensor.
Returns sharing a gps_time (and point_source_id, when present)
are one pulse; a group that repeats a return_number (two channels
firing at the same instant) is split where it repeats. Each pulse starts
at the sensor position interpolated from trajectory at its time, and
points at its farthest return; each return's range is its distance from
the sensor. The result traces with sylva.voxels.ray_voxelize
like a terrestrial scan.
What discrete returns cannot give back:
- Returns missing from a pulse (a return below the detection
threshold, or clipped away at a tile or chunk edge) are counted from
number_of_returns; the pulse keeps the returns it has (drop_incompleteleaves it out). If its last return is missing, the tracer ends the pulse early. - Pulses with no return (into water, onto a dark roof, lost to
absorption) leave no record. With
fill_missing, a hole of at mostmax_fillpulses in a line's regular firing is filled with pulses without returns, their directions interpolated between the pulses on either side, provided the mirror swept steadily across the hole. Larger holes, and pulses at the edge of the data, are not recovered. - Returns closer than the range resolution were merged by the receiver, and the energy each return carried is not recorded, so every return stands for an equal share of its pulse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
Returns with |
required |
trajectory
|
A |
required | |
max_gap
|
float | None
|
Longest gap between trajectory samples (s) to interpolate across; ten median sample intervals if None. Returns in longer gaps, or outside the trajectory, are left out. |
None
|
time_offset
|
float
|
Seconds added to |
0.0
|
fill_missing
|
bool
|
Infer pulses without a return in holes of at most |
False
|
max_fill
|
bool
|
Infer pulses without a return in holes of at most |
False
|
drop_incomplete
|
bool
|
Leave out pulses with fewer returns than |
False
|
report
|
bool
|
Also return what was found. |
False
|
Returns:
| Type | Description |
|---|---|
Shots or (Shots, dict)
|
Pulses ordered by flight line and time. The report has
|
Raises:
| Type | Description |
|---|---|
ValueError
|
Without |
read_trajectory ¶
read_trajectory(path: str | Path, format: str | None = None, crs: str | None = None, columns: Sequence[str] | None = None, z_offset: float = 0.0) -> Trajectory
Read a trajectory file: an SBET or a delimited text table.
SBET (format="sbet", or a .out, .sbet, .sbt or
.bin file): the binary "smoothed best estimate of trajectory" of
Applanix POSPac and most GNSS/INS software, records of 17 little-endian
doubles (time in GPS seconds of the week, latitude, longitude and
platform heading in radians, ellipsoidal height, ...). Latitude and
longitude are projected to crs, which is required; the heading is
the platform heading minus the wander angle.
Text (any other file): columns separated by commas, semicolons or
white space, with a header line naming them or with columns given.
Names are matched case-insensitively: time (or gps_time, t,
timestamp), x (easting), y (northing), z
(height, altitude, elevation), roll, pitch and
heading (yaw); other columns are ignored. With crs, x
and y are longitude and latitude in degrees and are projected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
The file. |
required |
format
|
(None, 'sbet', 'text')
|
None to decide from the suffix. |
None
|
crs
|
str | None
|
The CRS of the points (e.g. |
None
|
columns
|
Sequence[str] | None
|
Names of the text columns, in order, instead of a header line. |
None
|
z_offset
|
float
|
Added to every height. SBET heights are ellipsoidal: for points in
orthometric heights, give minus the geoid separation. A wrong
height shows as a large |
0.0
|
Returns:
| Type | Description |
|---|---|
Trajectory
|
|
Raises:
| Type | Description |
|---|---|
OSError
|
If the file cannot be read or is not a whole number of SBET records. |
ValueError
|
Without |
week_seconds ¶
week_seconds(gps_time)
GPS seconds of the week of adjusted standard GPS times.
LAS files flagged for it store adjusted standard GPS time (GPS time
minus 10⁹ s); SBET trajectories count seconds from the start of the GPS
week. Within one week the two differ by a constant, which
week_seconds(t) - t gives, for the time_offset arguments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gps_time
|
Adjusted standard GPS times (scalar or array). |
required |
Returns:
| Type | Description |
|---|---|
float or ndarray
|
|
crown_hull ¶
crown_hull(xy, hull: str = 'convex', concavity: float = 2.0) -> np.ndarray
Outline of a crown's points seen from above.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xy
|
|
required | |
hull
|
('convex', 'concave')
|
The convex hull, or the characteristic shape of Duckham et al.
(2008): from the Delaunay triangulation, outline edges longer than
|
"convex"
|
concavity
|
float
|
Edge length (m) of the concave outline; smaller follows the points more closely. |
2.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
find_trees ¶
find_trees(catalog, out: str | Path | None = None, method: str = 'dalponte2016', resolution: float = 0.5, dtm='auto', dtm_resolution: float = 1.0, window=5.0, hmin: float = 2.0, shape: str = 'circular', tops_from: str = 'chm', th_tree: float = 2.0, th_seed: float = 0.45, th_cr: float = 0.55, max_cr: float = 10.0, dt1: float = 1.5, dt2: float = 2.0, R: float = 2.0, Zu: float = 15.0, speed_up: float = 10.0, hull: str = 'convex', concavity: float = 2.0, min_point_height: float = 0.5, smooth: int = 0, attribute: str = 'tree_id', chunk_size: float | None = None, buffer: float = 30.0, workers: int | None = None, format: str | None = None) -> Trees
Find the trees of a whole catalogue, and optionally write every tile with each point's tree id.
Each chunk is segmented with its buffer as segment_trees does
it, on the CHM grid shared by the whole catalogue. A tree belongs to
the chunk that holds its top (a CHM-cell top to the chunk whose core
is nearest the cell centre, as raster outputs are joined; a point top
to the chunk whose core holds the point) and is reported once, with
the crown that chunk finds for it. Ids are 1, 2, ... in order of the
tops' x, then y, over the whole catalogue, so they do not depend on the
chunks or the number of workers; with a wide enough buffer the trees
are those segment_trees finds in all the tiles merged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog
|
The tiles. |
required | |
out
|
str | Path | None
|
Directory for labelled tiles (one per input tile, or per chunk on a
grid): every point with its tree id in |
None
|
method
|
str
|
As for |
'dalponte2016'
|
resolution
|
str
|
As for |
'dalponte2016'
|
window
|
str
|
As for |
'dalponte2016'
|
hmin
|
str
|
As for |
'dalponte2016'
|
shape
|
str
|
As for |
'dalponte2016'
|
tops_from
|
str
|
As for |
'dalponte2016'
|
th_tree
|
float
|
As for |
2.0
|
th_seed
|
float
|
As for |
2.0
|
th_cr
|
float
|
As for |
2.0
|
max_cr
|
float
|
As for |
2.0
|
dt1
|
float
|
As for |
2.0
|
dt2
|
float
|
As for |
2.0
|
R
|
float
|
As for |
2.0
|
Zu
|
float
|
As for |
2.0
|
speed_up
|
float
|
As for |
2.0
|
hull
|
str
|
As for |
'convex'
|
concavity
|
str
|
As for |
'convex'
|
min_point_height
|
str
|
As for |
'convex'
|
smooth
|
str
|
As for |
'convex'
|
dtm
|
('auto', None or Raster)
|
Heights above ground: |
'auto'
|
dtm_resolution
|
float
|
Cell size (m) of the |
1.0
|
attribute
|
str
|
Name of the tree id attribute in the written tiles. |
'tree_id'
|
chunk_size
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
format
|
float | None
|
As for |
None
|
buffer
|
float
|
Width (m) of the band read around each chunk. For a crown near a
chunk edge to be complete and the same as in one run, the buffer
must hold the crown and the crowns and tops that compete with it:
at least half the largest window plus two crown diameters for the
CHM methods, twice |
30.0
|
Returns:
| Type | Description |
|---|---|
Trees
|
With |
Raises:
| Type | Description |
|---|---|
ValueError
|
As |
li2012 ¶
li2012(cloud: PointCloud, dt1: float = 1.5, dt2: float = 2.0, R: float = 2.0, Zu: float = 15.0, hmin: float = 2.0, speed_up: float = 10.0, heights=None) -> np.ndarray
Point-based tree segmentation of Li et al. (2012).
Points are taken from the highest down. The highest point left starts
a tree (its set P, with an empty set N); every point left within
speed_up of that top, from the highest down, joins P or N by its
smallest horizontal distances d1 to P and d2 to N. A local
maximum (highest within a disc of diameter R) joins N if d1 >
dt or d2 < d1 < dt, and P otherwise, where dt is dt2 for
points higher than Zu and dt1 below; any other point joins P
if d1 <= d2. P is the tree; N stays for the next ones. It stops
when the highest point left is lower than hmin. Equal heights are
taken in order of x, then y.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
Points; see |
required |
dt1
|
float
|
Spacing thresholds (m) below and above |
1.5
|
dt2
|
float
|
Spacing thresholds (m) below and above |
1.5
|
R
|
float
|
Diameter (m) of the local maximum window; 0 makes every point a local maximum. |
2.0
|
Zu
|
float
|
Height (m) above which |
15.0
|
hmin
|
float
|
Lowest tree top (m). |
2.0
|
speed_up
|
float
|
Largest crown radius (m) considered; it only saves time when larger than any crown. |
10.0
|
heights
|
Height of each point (array or attribute name); z if None. |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Tree of each point, 1, 2, ... in the order found (tallest first), 0 for none. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For non-positive thresholds or a negative |
locate_trees ¶
locate_trees(source, window=5.0, hmin: float = 2.0, shape: str = 'circular', heights=None) -> TreeTops
Tree tops by the local maximum filter (Popescu and Wynne 2004).
A CHM cell (its centre, with the cell's value) or a point is a tree top
when it is at least hmin high and nothing within its window is
higher. The window is a disc of diameter ws (or a square of side
ws) centred on the site, and ws may depend on the site's height.
Of equal-height maxima within each other's windows only the first, in
order of x then y, is kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Raster or PointCloud
|
A canopy height model (NaN cells take no part), or a cloud of heights above ground. |
required |
window
|
(float, LinearWindow or callable)
|
Window size |
5.0
|
hmin
|
float
|
Lowest tree top (m). |
2.0
|
shape
|
('circular', 'square')
|
Window shape. |
"circular"
|
heights
|
For a cloud: the height of each point, as an array or the name of an attribute; z if None (a normalised cloud). |
None
|
Returns:
| Type | Description |
|---|---|
TreeTops
|
In order of x, then y. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For a non-positive or non-finite window size, an unknown shape, or heights that do not match the cloud. |
segment_crowns ¶
segment_crowns(chm: Raster, tops, method: str = 'dalponte2016', th_tree: float = 2.0, th_seed: float = 0.45, th_cr: float = 0.55, max_cr: float = 10.0) -> Raster
Crowns on a canopy height model, grown from tree tops.
"dalponte2016" is the region growing of Dalponte and Coomes (2016)
(th_tree, th_seed, th_cr, max_cr): each crown adds, sweep after sweep, the 4-neighbours
of its cells that are higher than th_tree, higher than th_seed
times the top's CHM value and than th_cr times the crown's mean
height, at most 5 % above the top, and fewer than max_cr cells from
it in x and in y. "watershed" is a marker-controlled watershed
(Meyer and Beucher 1990): the CHM is flooded from the tops, highest
cells first, over 8-connected cells higher than th_tree, each cell
taking the crown that reaches it first; tops on cells not higher than
th_tree grow nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chm
|
Raster
|
Canopy height model. |
required |
tops
|
TreeTops or array(k, 3)
|
Tree tops |
required |
method
|
('dalponte2016', 'watershed')
|
How crowns grow. |
"dalponte2016"
|
th_tree
|
float
|
Cells not higher than this (m) are in no crown. |
2.0
|
th_seed
|
float
|
Growing thresholds of Dalponte and Coomes (0-1). |
0.45
|
th_cr
|
float
|
Growing thresholds of Dalponte and Coomes (0-1). |
0.45
|
max_cr
|
float
|
Largest crown extent, in cells from the top in x and in y
( |
10.0
|
Returns:
| Type | Description |
|---|---|
Raster
|
The crown of each cell: |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown method, thresholds outside 0-1, or a non-positive
|
segment_trees ¶
segment_trees(cloud: PointCloud, method: str = 'dalponte2016', resolution: float = 0.5, window=5.0, hmin: float = 2.0, shape: str = 'circular', tops_from: str = 'chm', th_tree: float = 2.0, th_seed: float = 0.45, th_cr: float = 0.55, max_cr: float = 10.0, dt1: float = 1.5, dt2: float = 2.0, R: float = 2.0, Zu: float = 15.0, speed_up: float = 10.0, hull: str = 'convex', concavity: float = 2.0, min_point_height: float = 0.5, smooth: int = 0, heights=None) -> Trees
Find the trees of one cloud: tops, crowns and each point's tree.
For "dalponte2016" and "watershed" a CHM is made at
resolution (the highest point per cell, as
sylva.ground.make_chm, on a grid aligned with multiples of the
resolution and two empty cells wider than the cloud), tops are found by
locate_trees on it (or on the points with tops_from=
"points"), crowns are grown by segment_crowns, and each point
takes the crown of its cell. "li2012" labels the points directly
(li2012) and a tree's top is its highest point. "tops"
finds tops only. find_trees runs the same on a catalogue.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
Points; see |
required |
method
|
('dalponte2016', 'watershed', 'li2012', 'tops')
|
Segmentation. |
"dalponte2016"
|
resolution
|
float
|
CHM cell size (m). |
0.5
|
window
|
Tree-top detection, as for |
5.0
|
|
hmin
|
Tree-top detection, as for |
5.0
|
|
shape
|
Tree-top detection, as for |
5.0
|
|
tops_from
|
('chm', 'points')
|
Find tops on the CHM or on the points. |
"chm"
|
th_tree
|
float
|
Crown growing, as for |
2.0
|
th_seed
|
float
|
Crown growing, as for |
2.0
|
th_cr
|
float
|
Crown growing, as for |
2.0
|
max_cr
|
float
|
Crown growing, as for |
2.0
|
dt1
|
float
|
|
1.5
|
dt2
|
float
|
|
1.5
|
R
|
float
|
|
1.5
|
Zu
|
float
|
|
1.5
|
speed_up
|
float
|
|
1.5
|
hull
|
('convex', 'concave')
|
Crown outline from each tree's points, as for |
"convex"
|
concavity
|
float
|
Edge length (m) of a concave outline. |
2.0
|
min_point_height
|
float
|
Points lower than this (m) are in no tree and outline no crown.
(For |
0.5
|
smooth
|
int
|
Smooth the CHM with a mean filter over |
0
|
heights
|
Height of each point (array or attribute name); z if None (a normalised cloud). |
None
|
Returns:
| Type | Description |
|---|---|
Trees
|
With |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown method, hull or shape, or a bad setting. |
Examples:
>>> from sylva import als
>>> w = als.LinearWindow(2, 0.1, 3, 8)
>>> trees = als.segment_trees(normalised, window=w)
>>> trees.to_geojson("crowns.geojson")