sylva.change: airborne surveys¶
The functions for airborne surveys are re-exported from sylva.change
(change.align_surveys, change.chm_change and so on); they live in
sylva.change.als. See Change between airborne surveys.
sylva.change.als ¶
Change between two airborne lidar surveys of one area.
Every function works on catalogues of tiles (sylva.als.catalog)
through the chunk engine of sylva.als: each chunk reads the same
buffered box from both surveys, and the results do not depend on the chunk
size or the number of workers. As elsewhere in sylva.change, every
change carries its uncertainty or level of detection, and what the data
cannot support is labelled (below detection, no data, uncertain,
undetected, unobserved) rather than reported as change.
::
from sylva import als, change
a, b = als.catalog("2019/"), als.catalog("2024/") # ground classified
al = change.align_surveys(a, b, stable_classes=(2, 6)) # ground and roofs
chm = change.chm_change(a, b, resolution=1.0, alignment=al, harmonise=True)
gaps = change.gap_change(chm, height=2.0, min_area=10.0, years=5)
trees = change.tree_change(als.find_trees(a), als.find_trees(b), chm, alignment=al)
metrics = change.metric_change(a, b, resolution=20.0, metrics=["zq95", "cover"], alignment=al)
ALSAlignment
dataclass
¶
Offsets of a second airborne survey relative to a first, from
align_surveys.
A point p of the second survey belongs at p - offset in the frame
of the first. The offsets are estimated per block of a grid and given as
one constant, per block, or as a smooth field (model).
Attributes:
| Name | Type | Description |
|---|---|---|
xmin, ymin, block_size |
South-west corner and side (m) of the block grid. |
|
model |
{'constant', 'blocks', 'field'}
|
How the offset at a point is obtained. |
smoothing |
float
|
Standard deviation (m) of the Gaussian weights of the field. |
values, sigmas |
|
|
offset, sigma |
|
|
birge |
ndarray
|
|
noise_a, noise_b |
Median residual RMS (m) of planes fitted to each survey's stable returns: return noise plus the roughness of the surface within the plane radius. An upper bound on the return noise. |
|
blocks |
dict
|
Per block ( |
residuals |
ndarray | None
|
|
crs |
str | None
|
Coordinate system of the first survey. |
settings |
dict
|
The settings of the call. |
constant
classmethod
¶
constant(offset, sigma=(0.0, 0.0, 0.0)) -> ALSAlignment
A known constant offset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
offset
|
|
required | |
sigma
|
Its standard deviation (m). |
(0.0, 0.0, 0.0)
|
Returns:
| Type | Description |
|---|---|
ALSAlignment
|
|
offset_at ¶
offset_at(x, y) -> np.ndarray
Offset (dx, dy, dz) of the second survey at points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Coordinates (arrays of one length, or numbers). |
required | |
y
|
Coordinates (arrays of one length, or numbers). |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
sigma_at ¶
sigma_at(x, y) -> np.ndarray
Standard deviation of offset_at.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Coordinates (arrays of one length, or numbers). |
required | |
y
|
Coordinates (arrays of one length, or numbers). |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
apply ¶
apply(cloud: PointCloud) -> PointCloud
Move a cloud of the second survey into the frame of the first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
Points of the second survey. |
required |
Returns:
| Type | Description |
|---|---|
PointCloud
|
A copy with |
table ¶
table() -> dict[str, np.ndarray]
One row per block with an estimate: centre x, y, the
block's dx, dy, dz and their standard deviations
sd_dx, sd_dy, sd_dz, the model's model_dx,
model_dy, model_dz there, n_used, n_eff,
horizontal_determined, spread_before and spread_after.
to_csv ¶
to_csv(path) -> None
Write table as CSV.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Output file. |
required |
GapChange
dataclass
¶
Canopy gaps of two surveys and their dynamics, from gap_change.
Attributes:
| Name | Type | Description |
|---|---|---|
a, b |
The gaps of each survey. |
|
cells |
ndarray
|
|
status_a, closed_area |
Per gap of the first survey: |
|
status_b, formed_area |
Per gap of the second survey: |
|
areas |
dict
|
Area (m²) per cell code. |
years |
float | None
|
Time between the surveys, if given. |
summary ¶
summary() -> dict
Gap fractions, areas formed and closed (with annual rates as a
share of the area compared when years is known), gap counts by
status and the size-distribution exponents of both surveys.
to_geojson ¶
to_geojson(path, survey: str = 'b') -> None
Write one survey's gaps with their status and area formed or closed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Output file. |
required | |
survey
|
('a', 'b')
|
Which gaps. |
"a"
|
Gaps
dataclass
¶
Canopy gaps of one CHM, from canopy_gaps.
Attributes:
| Name | Type | Description |
|---|---|---|
labels |
ndarray
|
|
id, area, n_cells, x, y, mean_height, max_height |
Per gap: id (1, 2, ... in order of the first cell from the south-west), area (m²), cells, centre, and mean and largest CHM height of its cells (m). |
|
polygons |
list
|
Per gap, its outline along cell edges: a list of parts, each
|
area_with_data |
float
|
Area (m²) of the cells with a CHM value. |
xmin, ymin, resolution |
The CHM's grid. |
|
crs |
str | None
|
Its coordinate system. |
settings |
dict
|
|
table ¶
table() -> dict[str, np.ndarray]
id, area, n_cells, x, y, mean_height and
max_height per gap.
size_distribution ¶
size_distribution(bins=None) -> tuple[np.ndarray, np.ndarray]
Number of gaps per size class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bins
|
Bin edges (m²); by default powers of two from the smallest gap size allowed. |
None
|
Returns:
| Type | Description |
|---|---|
(edges, counts)
|
|
size_exponent ¶
size_exponent(xmin: float | None = None) -> tuple[float, float, int]
Exponent of a power law p(a) ~ a^-alpha fitted to the gap
sizes by maximum likelihood (Clauset et al. 2009).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xmin
|
float | None
|
Smallest size included (m²); the smallest allowed by default. |
None
|
Returns:
| Type | Description |
|---|---|
(alpha, standard error, number of gaps)
|
NaN with fewer than two gaps. |
to_geojson ¶
to_geojson(path, properties: dict | None = None) -> None
Write the gap outlines as GeoJSON polygons with table as
properties.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Output file. |
required | |
properties
|
dict | None
|
Extra per-gap columns (name to sequence). |
None
|
MetricChange
dataclass
¶
Area-based metrics of two surveys compared, from metric_change.
Attributes:
| Name | Type | Description |
|---|---|---|
names |
list
|
The metrics. |
a, b |
Per metric (name to |
|
difference |
dict
|
|
bias, sigma |
Mean and standard deviation of the change the cell would show from sampling alone (the permutation distribution). |
|
lower, upper, lod |
The interval a change must leave to be significant, and its half-width. |
|
classes |
dict
|
Per metric, |
settings |
dict
|
The settings of the call. |
report ¶
report() -> str
Per metric: median difference and the shares of cells with a significant gain and loss.
PAIChange
dataclass
¶
Plant area index of two surveys compared, from pai_change.
Attributes:
| Name | Type | Description |
|---|---|---|
pai_a, pai_b, sigma_a, sigma_b |
PAI of each survey per cell and its standard deviation. |
|
difference, sigma, lod |
|
|
classes |
ndarray
|
|
profile |
dict
|
The pooled profile of the whole area: |
mask ¶
mask(name: str) -> np.ndarray
Cells of one class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
('no_data', 'below_detection', 'gain', 'loss', 'saturated')
|
The class. |
"no_data"
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
SurfaceChange
dataclass
¶
A surface of two airborne surveys compared, from surface_change.
Attributes:
| Name | Type | Description |
|---|---|---|
surface |
{'chm', 'dsm', 'dtm'}
|
What was compared. |
a, b |
The surfaces of each survey on the first survey's catalogue grid
(for |
|
difference |
Raster
|
|
bias |
Raster
|
Change expected from the sampling of the surface alone (m): the mean of the permutation distribution. Negative where the second survey samples the cell more sparsely, since its highest return then falls shorter of the canopy top. |
lower, upper |
The interval (m) a change must leave to be significant: the permutation interval widened by the DTM and alignment errors. |
|
lod |
Raster
|
Its half-width (m), the level of detection. |
sigma |
Raster
|
Standard deviation of the change a cell would show without any (m). |
classes |
ndarray
|
|
sigma_a, sigma_b |
Standard deviation of each survey's value: the spread of its highest pulse under the permutation, and its DTM's error. |
|
pulses_a, pulses_b |
Pulses with a return in the cell, per m². |
|
density_a, density_b |
First returns per m² in each cell, after harmonisation. |
|
dod |
DoD
|
The difference with volumes and areas over the significant cells
( |
sensors |
dict
|
Per survey ( |
median_bias |
float
|
Median of |
notes |
list
|
Differences between the sensors that bias the comparison, in words. |
settings |
dict
|
The settings of the call. |
mask ¶
mask(name: str) -> np.ndarray
Cells of one class.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
One of |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
write ¶
write(directory, format: str = 'asc') -> list[str]
Write a, b, difference, lod and classes as
rasters named <surface>_<layer>.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
Output directory (created if needed). |
required | |
format
|
('asc', 'tif')
|
ESRI ASCII grids (with a |
"asc"
|
Returns:
| Type | Description |
|---|---|
list of str
|
The files written. |
ALSTreeChange
dataclass
¶
Trees of two airborne surveys compared, from tree_change.
table has one row per tree of the first survey (with its partner in
the second, if any) and one per tree found only in the second:
id_a, id_b
Ids in each survey (0 for none).
status
survivor, damaged, dead, undetected or
unobserved for a tree of the first survey; recruit,
released, undetected or unobserved for one found only
in the second.
x, y
Top in the first survey's frame.
distance
Between the partners' tops (m).
height_a, height_b, dh, sigma, lod
Heights (m), height change, its standard deviation and level of
detection.
dh_change
growth, decrease, below_detection or unmeasured
(empty without a partner).
crown_area_a, crown_area_b
Crown areas (m²).
crown_loss, crown_gain
Shares of the crown with a significant canopy loss or gain.
observed
Share of the crown with data in both surveys.
Attributes:
| Name | Type | Description |
|---|---|---|
table |
dict
|
The columns above, as NumPy arrays. |
crs |
str | None
|
Coordinate system of the positions. |
settings |
dict
|
The settings of the call. |
counts ¶
counts() -> dict[str, int]
Rows per status (undetected and unobserved split by the
survey the tree was found in).
summary ¶
summary(area: float | None = None, years: float | None = None, mask=None) -> dict
Totals: trees by fate, mean height growth of the survivors with
its standard error (from their scatter, and from their measurement
uncertainties alone), crown area lost and, with years, annual
mortality and recruitment rates (Sheil et al. 1995). With area
(m²), counts and crown areas are also given per hectare.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
area
|
float | None
|
Area covered (m²). |
None
|
years
|
float | None
|
Time between the surveys. |
None
|
mask
|
Rows to include (bool array), e.g. the trees in a plot. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
grid ¶
grid(resolution: float, bounds=None) -> dict[str, Raster]
Totals per grid cell: survivors, damaged, dead,
recruits, mean_growth, growth_se and
crown_area_lost (m²).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolution
|
float
|
Cell size (m). |
required |
bounds
|
|
None
|
Returns:
| Type | Description |
|---|---|
dict of Raster
|
|
to_csv ¶
to_csv(path) -> None
Write the table as CSV.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Output file. |
required |
report ¶
report(years: float | None = None) -> str
A summary in words.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
years
|
float | None
|
Time between the surveys, for annual rates. |
None
|
align_surveys ¶
align_surveys(catalog_a, catalog_b, block_size: float = 100.0, stable_classes=(2,), model: str = 'field', smoothing: float | None = None, horizontal: bool = True, horizontal_prior: float = 0.3, sample_spacing: float = 1.0, radius: float = 1.5, min_neighbours: int = 6, max_roughness: float = 0.15, max_slope: float = 1.0, max_offset: float = 2.0, correlation_length: float = 5.0, min_samples: int = 30, huber: float = 1.5, iterations: int = 20, chunk_size: float | None = None, workers: int | None = None) -> ALSAlignment
Offsets between two airborne surveys, on stable surfaces.
Every stable return of the second survey (one per sample_spacing
square) is compared with a plane fitted to the first survey's stable
returns within radius of it. A survey displaced by (dx, dy, dz)
sits dz - gx dx - gy dy above a plane of gradient (gx, gy), so
the vertical offset follows from every sample and the horizontal ones
from the variety of slopes and aspects (Nuth and Kääb 2011). Each block
is solved by iteratively reweighted least squares (Huber weights),
refitting the planes at the moved positions until the offsets settle.
A horizontal offset along a slope of one aspect, or on flat ground, looks
like a vertical one; a Gaussian prior of horizontal_prior keeps it
near zero there, and its standard deviation stays near the prior.
Surfaces of several aspects (terrain, roofs: stable_classes=(2, 6);
roads are class 11) fix it. Samples within correlation_length of each
other count as one independent sample.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_a
|
The reference and the later survey, with ground (and any other stable class) classified. |
required | |
catalog_b
|
The reference and the later survey, with ground (and any other stable class) classified. |
required | |
block_size
|
float
|
Side of the blocks (m); use the tile size for one estimate per tile. |
100.0
|
stable_classes
|
Classes of the returns taken as unchanged: 2 ground; 6 buildings; 11 roads. |
(2,)
|
|
model
|
('field', 'blocks', 'constant')
|
A smooth field (the blocks pooled with Gaussian weights of
|
"field"
|
smoothing
|
float | None
|
Standard deviation (m) of the field's weights; |
None
|
horizontal
|
bool
|
Estimate horizontal offsets (else only the vertical one). |
True
|
horizontal_prior
|
float
|
Prior standard deviation (m) of each horizontal offset. |
0.3
|
sample_spacing
|
float
|
Sampling of the second survey (m), radius (m) and fewest returns of the reference planes. |
1.0
|
radius
|
float
|
Sampling of the second survey (m), radius (m) and fewest returns of the reference planes. |
1.0
|
min_neighbours
|
float
|
Sampling of the second survey (m), radius (m) and fewest returns of the reference planes. |
1.0
|
max_roughness
|
float
|
Planes rougher than this RMS (m) or steeper than this gradient are left out (edges, walls, vegetation misclassified as ground). |
0.15
|
max_slope
|
float
|
Planes rougher than this RMS (m) or steeper than this gradient are left out (edges, walls, vegetation misclassified as ground). |
0.15
|
max_offset
|
float
|
Largest offset searched (m). |
2.0
|
correlation_length
|
float
|
Side (m) of the squares within which samples are not independent. |
5.0
|
min_samples
|
int
|
Fewest usable samples in a block. |
30
|
huber
|
float
|
Huber constant (robust standard deviations) and largest number of refits. |
1.5
|
iterations
|
float
|
Huber constant (robust standard deviations) and largest number of refits. |
1.5
|
chunk_size
|
float | None
|
Side of the chunks (m), rounded up to whole blocks; by default the median tile size. |
None
|
workers
|
int | None
|
Chunks at once (one per core by default). |
None
|
Returns:
| Type | Description |
|---|---|
ALSAlignment
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For bad settings, tiles without a classification, or if no block has enough stable samples in both surveys. |
canopy_gaps ¶
canopy_gaps(chm: Raster, height: float = 2.0, min_area: float = 10.0, max_area: float | None = None, connectivity: int = 8) -> Gaps
Canopy gaps of a CHM: connected cells no higher than height.
As ForestGapR's getForestGaps (Silva et al. 2019): cells at or below
the height threshold are joined across edges and corners
(connectivity=8) or edges only (4), and regions whose area lies
between min_area and max_area are gaps. A threshold of 2 m
follows Brokaw's (1982) definition of a gap as an opening reaching down
to within 2 m of the ground. NaN cells are never gap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chm
|
Raster
|
Canopy height model. |
required |
height
|
float
|
Gap height threshold (m). |
2.0
|
min_area
|
float
|
Size limits (m²); no upper limit by default. |
10.0
|
max_area
|
float
|
Size limits (m²); no upper limit by default. |
10.0
|
connectivity
|
(8, 4)
|
Cell neighbourhood. |
8
|
Returns:
| Type | Description |
|---|---|
Gaps
|
|
gap_change ¶
gap_change(change, chm_b: Raster | None = None, height: float = 2.0, min_area: float = 10.0, max_area: float | None = None, connectivity: int = 8, years: float | None = None) -> GapChange
Gap formation and closure between two surveys.
Gaps are found in each CHM as canopy_gaps finds them. A cell
forms a gap when it is in a gap of the second survey only and its CHM
fell significantly, and closes one when it is in a gap of the first
only and its CHM rose significantly (as ForestGapR's GapChangeDec,
Silva et al. 2019, but with the level of detection of
surface_change); a cell that crossed the threshold without a
significant change is uncertain. Each gap of the second survey is
new, expanded, stable or uncertain, each of the first
closed, shrunk, stable or uncertain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
change
|
A CHM |
required | |
chm_b
|
Raster | None
|
The second survey's CHM, on the grid of the first. |
None
|
height
|
float
|
As for |
2.0
|
min_area
|
float
|
As for |
2.0
|
max_area
|
float
|
As for |
2.0
|
connectivity
|
float
|
As for |
2.0
|
years
|
float | None
|
Time between the surveys, for annual rates. |
None
|
Returns:
| Type | Description |
|---|---|
GapChange
|
|
metric_change ¶
metric_change(catalog_a, catalog_b, resolution: float = 20.0, metrics=None, alignment: ALSAlignment | None = None, harmonise: bool = False, density_cell: float = 10.0, seed: int = 0, permutations: int = 100, stratum: float = 2.0, threshold: float = 2.0, entropy_bin: float = 1.0, cover_break: float = 2.0, min_height: float | None = None, drop_noise: bool = True, dtm_method: str = 'plane', dtm_resolution: float = 1.0, confidence: float = 0.95, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None) -> MetricChange
Differences of area-based metrics between two surveys, with their level of detection.
The metrics of sylva.als.grid_metrics are computed per cell for
both surveys in one pass, heights above each survey's own DTM, the second
survey moved by alignment and, with harmonise, the denser survey
thinned as surface_change thins it.
Whether a metric changed is tested by a stratified permutation test
(Pitman 1937): if nothing changed, the pulses of both surveys that fell
in one stratum m square of the cell sample the same canopy, and any
reassignment of them between the surveys that keeps each survey's number
of pulses in each square is as likely as the observed one. bias and
[lower, upper] are the mean and central confidence interval of
metric(b) - metric(a) over permutations such reassignments, drawn
from a stream seeded by the cell and seed so that the result does not
depend on the chunks. Keeping pulses in their squares keeps the pattern
in which each survey sampled the cell; resampling the cell's pulses as
independent draws would ignore it and understate the uncertainty. The
test covers sampling, not differences between the sensors (footprint,
sensitivity), which bias metrics systematically: harmonisation removes
the part due to pulse density only.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_a
|
The earlier and the later survey, with ground classified. |
required | |
catalog_b
|
The earlier and the later survey, with ground classified. |
required | |
resolution
|
float
|
Cell size (m). |
20.0
|
metrics
|
Names (see |
None
|
|
alignment
|
ALSAlignment | None
|
As for |
None
|
harmonise
|
ALSAlignment | None
|
As for |
None
|
density_cell
|
ALSAlignment | None
|
As for |
None
|
seed
|
ALSAlignment | None
|
As for |
None
|
permutations
|
int
|
Reassignments per cell; 0 for none (no level of detection, every
change |
100
|
stratum
|
float
|
Side (m) of the squares within which pulses are reassigned. |
2.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
|
dtm_method
|
str
|
Each survey's normalising DTM, as for |
'plane'
|
dtm_resolution
|
str
|
Each survey's normalising DTM, as for |
'plane'
|
confidence
|
float
|
Of the interval. |
0.95
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
MetricChange
|
|
pai_change ¶
pai_change(profile_a, profile_b, confidence: float = 0.95) -> PAIChange
Plant area index change from two gap-fraction profiles.
Both sylva.als.ALSProfile must come from
sylva.als.gap_profile on the same grid and layers (the same
resolution, bin_size, min_height and max_height over the
same area), each with its survey's trajectory. With W pulses in a
cell, a share P reaching the ground and a mean extinction k,
PAI = -ln P / k and, by the delta method on the binomial P,
its standard deviation is sqrt((1 - P) / (W P)) / k. A cell no pulse
crossed is saturated. The profiles are not moved by an alignment:
compare cells much larger than the horizontal offset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile_a
|
The two profiles. |
required | |
profile_b
|
The two profiles. |
required | |
confidence
|
float
|
Of the level of detection. |
0.95
|
Returns:
| Type | Description |
|---|---|
PAIChange
|
|
profile_change ¶
profile_change(profile_a, profile_b, mask=None) -> dict
Plant area density profile of an area in two surveys, from the pooled counts of its cells.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile_a
|
|
required | |
profile_b
|
|
required | |
mask
|
|
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
chm_change ¶
chm_change(catalog_a, catalog_b, resolution: float = 1.0, **kw) -> SurfaceChange
Canopy height model change: surface_change with surface="chm".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_a
|
The earlier and the later survey, with ground classified. |
required | |
catalog_b
|
The earlier and the later survey, with ground classified. |
required | |
resolution
|
float
|
Cell size (m). |
1.0
|
**kw
|
Any other argument of |
{}
|
Returns:
| Type | Description |
|---|---|
SurfaceChange
|
|
dtm_change ¶
dtm_change(catalog_a, catalog_b, resolution: float = 1.0, **kw) -> SurfaceChange
Terrain change: surface_change with surface="dtm".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_a
|
The earlier and the later survey, with ground classified. |
required | |
catalog_b
|
The earlier and the later survey, with ground classified. |
required | |
resolution
|
float
|
Cell size (m). |
1.0
|
**kw
|
Any other argument of |
{}
|
Returns:
| Type | Description |
|---|---|
SurfaceChange
|
|
harmonise ¶
harmonise(catalog_a, catalog_b, out_a, out_b, density_cell: float = 10.0, seed: int = 0, format: str | None = None, chunk_size: float | None = None, workers: int | None = None) -> tuple[Catalog, Catalog]
Thin two surveys to a common pulse density and write them as tiles.
In each density_cell square (on a grid through the origin), the
survey with more first returns keeps each of its pulses with probability
n_other / n_own, all returns of a pulse together, chosen by a hash of
the pulse (its GPS time and flight line) and seed: the same pulses
whatever the tiling, and the same that surface_change keeps with
harmonise=True. Tree detection and metrics on the thinned tiles then
compare like with like.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_a
|
The two surveys. |
required | |
catalog_b
|
The two surveys. |
required | |
out_a
|
Directories for the thinned tiles of each (one file per chunk). |
required | |
out_b
|
Directories for the thinned tiles of each (one file per chunk). |
required | |
density_cell
|
float
|
Side (m) of the squares in which densities are compared. |
10.0
|
seed
|
int
|
Seed of the pulse selection. |
0
|
format
|
('las', 'laz')
|
Output format (that of the input tiles by default). |
"las"
|
chunk_size
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
(Catalog, Catalog)
|
The thinned surveys. |
surface_change ¶
surface_change(catalog_a, catalog_b, surface: str = 'chm', resolution: float = 1.0, alignment: ALSAlignment | None = None, harmonise: bool = False, density_cell: float = 10.0, seed: int = 0, first_returns: bool = False, subcircle: float = 0.0, dtm_method: str = 'plane', dtm_resolution: float = 1.0, min_returns: int = 2, noise_a: float | None = None, noise_b: float | None = None, interpolation_error: float = 0.02, confidence: float = 0.95, horizontal_sigma: float = 0.0, vertical_sigma: float = 0.0, chunk_size: float | None = None, buffer: float = 20.0, workers: int | None = None) -> SurfaceChange
Difference of a CHM, DSM or DTM of two surveys with a level of detection per cell.
Both surveys are gridded in one pass with one algorithm (the highest return per cell), each normalised by a DTM of its own ground returns.
The highest of a cell's returns is a sample of its canopy: it falls
short of the top by an amount that depends on the number of pulses and
on how the heights are spread in the cell, and at a crown's edge one
survey may hit the crown where the other hits only ground. Within a
cell each pulse contributes its highest return there; if nothing
changed, the pulses of both surveys sample one surface and every split
of the pooled values into the two surveys' numbers is equally likely.
The distribution of max(b) - max(a) over the splits, exact from the
order statistics of the pooled values (a permutation test on the
maxima), is the change sampling alone would give: its mean is
bias, and its central confidence interval, widened by the DTM
errors (noise / sqrt(n_ground) plus interpolation_error times
the distance to the nearest ground return) and the alignment's
uncertainty (the horizontal part times the surface's gradient; for DSMs
and DTMs also the vertical part), is [lower, upper]. A change above
it is a gain, below it a loss; cells with fewer than min_returns
pulses in either survey are no_data. A DTM is compared with the DTM
and alignment errors alone.
The permutation assumes the two sensors sample the canopy alike except
for their numbers of pulses. notes names the sensor differences
(pulse density, returns per pulse, scan angle) that bias CHM change
otherwise; harmonise=True thins the denser survey, pulse by pulse,
to the other's pulse density in each density_cell square (as
harmonise does to whole catalogues), and first_returns=True
compares first returns only.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
catalog_a
|
The earlier and the later survey, with ground classified. |
required | |
catalog_b
|
The earlier and the later survey, with ground classified. |
required | |
surface
|
('chm', 'dsm', 'dtm')
|
Canopy heights, surface elevations, or terrain. |
"chm"
|
resolution
|
float
|
Cell size (m). |
1.0
|
alignment
|
ALSAlignment | None
|
From |
None
|
harmonise
|
bool
|
Thin the denser survey to the other's pulse density in squares of
|
False
|
density_cell
|
bool
|
Thin the denser survey to the other's pulse density in squares of
|
False
|
seed
|
bool
|
Thin the denser survey to the other's pulse density in squares of
|
False
|
first_returns
|
bool
|
Grid first returns only. |
False
|
subcircle
|
float
|
Replace each return by eight points on a circle of this radius (m); 0 for the returns themselves. |
0.0
|
dtm_method
|
('plane', 'tin', 'lowest')
|
Each survey's DTM (for normalisation, and the surface of |
"plane"
|
dtm_resolution
|
float
|
Cell size of the normalising DTM (m). |
1.0
|
min_returns
|
int
|
Fewest pulses in a cell of each survey. |
2
|
noise_a
|
float | None
|
Return noise of each survey (m), for the DTM error; by default the alignment's plane residuals, else 0.05 m. |
None
|
noise_b
|
float | None
|
Return noise of each survey (m), for the DTM error; by default the alignment's plane residuals, else 0.05 m. |
None
|
interpolation_error
|
float
|
Growth of the DTM error with distance to the nearest ground return (m per m). |
0.02
|
confidence
|
float
|
Of the interval. |
0.95
|
horizontal_sigma
|
float
|
Alignment uncertainty (m) when no |
0.0
|
vertical_sigma
|
float
|
Alignment uncertainty (m) when no |
0.0
|
chunk_size
|
float | None
|
As for |
None
|
buffer
|
float | None
|
As for |
None
|
workers
|
float | None
|
As for |
None
|
Returns:
| Type | Description |
|---|---|
SurfaceChange
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For bad settings, or if no chunk has ground returns in both surveys. |
tree_change ¶
tree_change(trees_a, trees_b, change: SurfaceChange, alignment: ALSAlignment | None = None, max_distance: float = 1.5, max_growth: float = 0.3, max_drop: float = 0.2, height_weight: float = 1.0, dead_fraction: float = 0.5, damage_fraction: float = 0.3, min_observed: float = 0.5, confidence: float = 0.95) -> ALSTreeChange
Airborne trees of two surveys matched, with height growth, mortality, damage and recruitment.
The trees are matched by the optimal assignment of
sylva.change.match_trees (Kuhn 1955; Munkres 1957) with the tree
height in place of the diameter: a pair must lie within
max_distance and its height may grow by at most max_growth or
fall by at most max_drop of the larger height. The CHM change says
what became of the others:
- a matched tree is a
survivor, ordamagedwhen a significant loss coversdamage_fractionof its crown or its height fell by more than its level of detection; - an unmatched tree of the first survey is
damagedwhen a tree of the second stands within2 * max_distanceof its top on significantly lowered canopy (a broken or collapsed crown),deadwhen a significant loss coversdead_fractionof its crown,unobservedwhen less thanmin_observedof its crown has data, and otherwiseundetected(its canopy is still there); - an unmatched tree of the second survey is a
recruitwhen the canopy at its top rose significantly from below1 - max_growthof its height (more than an existing crown can grow),releasedwhen it fell (an understorey tree exposed by the loss of a neighbour),unobservedwithout data, and otherwiseundetected(no significant change, or a rise a growing crown explains: a top split off a neighbour's crown).
The standard deviation of a height is that of its top's CHM cell in
change (noise, sampling of the apex, DTM); the level of detection of
a height change is the normal quantile of confidence times the
standard deviation of the difference.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trees_a
|
The trees of each survey, from |
required | |
trees_b
|
The trees of each survey, from |
required | |
change
|
SurfaceChange
|
The CHM change of the same surveys ( |
required |
alignment
|
ALSAlignment | None
|
Moves the second survey's trees into the first's frame (the one
|
None
|
max_distance
|
float
|
Matching: largest distance (m), largest relative height gain and
loss, and the weight of the squared relative height difference
next to the squared distance over |
1.5
|
max_growth
|
float
|
Matching: largest distance (m), largest relative height gain and
loss, and the weight of the squared relative height difference
next to the squared distance over |
1.5
|
max_drop
|
float
|
Matching: largest distance (m), largest relative height gain and
loss, and the weight of the squared relative height difference
next to the squared distance over |
1.5
|
height_weight
|
float
|
Matching: largest distance (m), largest relative height gain and
loss, and the weight of the squared relative height difference
next to the squared distance over |
1.5
|
dead_fraction
|
float
|
Shares of the crown, as above. |
0.5
|
damage_fraction
|
float
|
Shares of the crown, as above. |
0.5
|
min_observed
|
float
|
Shares of the crown, as above. |
0.5
|
confidence
|
float
|
Of the levels of detection. |
0.95
|
Returns:
| Type | Description |
|---|---|
ALSTreeChange
|
|