sylva.qsm¶
sylva.qsm ¶
Quantitative structure models: skeletonisation and cylinder fitting.
A QSM is a set of connected cylinders. build_qsm bins geodesic
distance from the base over a kNN graph, splits bins into connected segments
(Verroust and Lazarus 2000; Xu et al. 2007), fits a RANSAC cylinder to each
and links parents.
Buttress
dataclass
¶
An irregular stem base as a closed mesh; build with buttress_mesh.
Attributes:
| Name | Type | Description |
|---|---|---|
vertices, faces |
Closed triangle mesh ( |
|
volume |
float
|
Volume below |
top, top_z |
Where the buttress ends: height above ground (m) and absolute z. |
|
heights, areas, solidities |
Per slice: bottom height (m), cross-section area (m²), and solidity (area over convex-hull area; about 1 for a round stem, low for flanges). |
|
open |
ndarray
|
Per slice, True where the outline did not close (part of the bark
unseen): the seen bark was kept, thickened to |
total_volume ¶
total_volume(model: QSM) -> float
Buttress volume plus the QSM's volume above the buttress.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
QSM
|
The tree's cylinder model. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Volume (m³). |
fuse ¶
fuse(model: QSM, sides: int = 12, contiguous: bool = True, overlap: float = 0.1) -> 'TreeMesh'
Join this buttress to a QSM as one mesh of the whole stem.
The buttress replaces the cylinders below its top: the model is cut
at top_z (QSM.above) and both surfaces go into one mesh,
so nothing is counted twice and the volume is the one
total_volume reports. The parts stay watertight and
separately labelled rather than being welded into a single shell — a
boolean union of a flanged base and a thousand tubes is not something
a triangle mesh survives cleanly.
A cut tube ends square to its own axis, so a leaning stem would hang
over the buttress top by up to its radius times the lean. overlap
cuts the wood that much lower, letting it reach down inside the base
where the seam cannot be seen. It changes no volume: those are read
from top_z either way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
QSM
|
The tree's cylinder model, in the same frame as the buttress. |
required |
sides
|
int
|
Facets around each cylinder. |
12
|
contiguous
|
bool
|
One continuous tube per branch (see |
True
|
overlap
|
float
|
How far below |
0.1
|
Returns:
| Type | Description |
|---|---|
TreeMesh
|
Vertices, faces, a part label per face, and the volumes. |
Examples:
>>> b = buttress_mesh(cloud, base_xy=(x, y))
>>> b.fuse(model).to_ply("tree.ply")
to_obj ¶
to_obj(path: str | Path) -> None
Write the mesh as a Wavefront OBJ object named buttress.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
to_ply ¶
to_ply(path: str | Path) -> None
Write the mesh as a binary PLY.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
TreeMesh
dataclass
¶
A buttress and a QSM as one mesh; build with Buttress.fuse.
Attributes:
| Name | Type | Description |
|---|---|---|
vertices, faces |
The two surfaces in one mesh ( |
|
part |
ndarray
|
Per face: 0 for the buttress, 1 for the wood above it. |
buttress_volume, wood_volume |
Volume below and above |
|
top_z |
float
|
Absolute height where the buttress ends and the cylinders start. |
offset |
float
|
Distance (m) between the middle of the base and the middle of the wood at the join. A stem that sits over its base is a few centimetres out. |
overhang |
float
|
Share of the wood's cross-section at the join that lies outside the base. Anything much above zero means the two do not agree about where the stem is: usually a buttress top found too low, a stem axis pulled off by the flanges, or a neighbour's wood in the cloud. |
to_obj ¶
to_obj(path: str | Path) -> None
Write the mesh as objects buttress and wood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
to_ply ¶
to_ply(path: str | Path, color=None) -> None
Write the mesh as a binary PLY, the buttress darker than the wood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
color
|
One RGB triple (0-255) for every face; by default the buttress is bark brown and the wood keeps the stem colour. |
None
|
QSM
dataclass
¶
A tree as connected cylinders; build with build_qsm.
Attributes:
| Name | Type | Description | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
cylinders |
ndarray
|
|
Examples:
>>> q = build_qsm(wood_points(tree_cloud))
>>> q.total_volume, q.dbh
>>> q.to_csv("tree_001.csv"); q.to_ply("tree_001.ply")
total_volume
property
¶
total_volume: float
Woody volume of the tree (m³); multiply by basic density for biomass.
max_branch_order
property
¶
max_branch_order: int
Highest branch order (0 for a bare stem or empty model).
column ¶
column(name: str) -> np.ndarray
One column of cylinders.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
A name from |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
A view, length |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown name. |
metrics ¶
metrics(crown_branch_length: float = 1.0, crown_slice: float = 0.5) -> dict
Tree architecture from the cylinders.
Height, DBH, volumes and lengths (total, stem, branches, and per
branch order), branch and tip counts, path fraction (mean base-to-tip
path over the longest), crown base height (lowest first-order branch
at least crown_branch_length long), stem lean and its direction,
sweep (greatest offset of the stem from the chord between the base and
the crown base, over its length), the stem taper profile, the crown
outlined by the branches (see sylva.trees.crown_shape, slices
crown_slice high), length-weighted median insertion and zenith
angles of first-order branches (deg), and the share of volume and
length whose radius was fitted to points rather than filled in by
the taper and pipe-model priors -- a quality flag for the model.
Heights are above the stem base.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
crown_branch_length
|
float
|
Shortest first-order branch (m) that marks the crown base. |
1.0
|
crown_slice
|
float
|
Slice height (m) of the stacked crown hulls. |
0.5
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Notes
Validated against the source meshes of simulated trees: height -2 %,
DBH -1 %, crown area -3 %, branch zenith error 3 degrees
(Benchmarks > QSMs). A low measured_volume_fraction means most
of the volume came from priors; treat such trees with care.
branches ¶
branches() -> dict[str, np.ndarray]
One row per branch (branch_id chain; the stem is order 0), as
columns: id, order, parent (branch it grows from, -1 for
the stem), n_cylinders, length (m), volume (m3),
base_radius and length-weighted mean_radius (m),
base_height and tip_height (m above the stem base),
insertion_angle (deg between its direction over 0.5 m past its
first cylinder, which only joins it to the parent's axis, and the
parent's direction over 0.5 m either side of the junction), zenith and azimuth of the base-to-tip chord (deg),
tortuosity (length over chord), n_children and
measured_fraction (share of the length fitted to points).
Returns:
| Type | Description |
|---|---|
dict
|
|
summary ¶
summary() -> dict
Headline numbers with units in the keys.
Returns:
| Type | Description |
|---|---|
dict
|
|
to_csv ¶
to_csv(path: str | Path) -> None
Write the cylinders as CSV with a header of COLUMNS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file; read back with |
required |
to_treefile ¶
to_treefile(path: str | Path) -> None
Write in the raycloudtools/treetools _trees.txt format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file, for |
required |
from_csv
classmethod
¶
from_csv(path: str | Path) -> QSM
Read cylinders written by to_csv.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
CSV with a header line and the 12 |
required |
Returns:
| Type | Description |
|---|---|
QSM
|
|
mesh ¶
mesh(sides: int = 12, contiguous: bool = False) -> tuple[np.ndarray, np.ndarray, np.ndarray]
Triangle mesh of the cylinders.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sides
|
int
|
Facets around each cylinder. |
12
|
contiguous
|
bool
|
One continuous tube per branch instead of one closed tube per cylinder: consecutive cylinders share a ring, so a branch has no caps inside it and its surface runs unbroken from base to tip. The ring frame is carried along the branch so the facets do not twist, and a shared ring takes the mean of the two radii. Each branch is still its own closed surface, pushed into its parent rather than welded to it, and the mesh is about half the size. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
vertices |
ndarray
|
|
faces |
ndarray
|
|
owner |
ndarray
|
Cylinder row of each face. |
to_obj ¶
to_obj(path: str | Path, sides: int = 12, contiguous: bool = False) -> None
Write the cylinder mesh as a Wavefront OBJ (Blender, MeshLab, CloudCompare).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
sides
|
int
|
Facets around each cylinder. |
12
|
contiguous
|
bool
|
One continuous tube per branch (see |
False
|
volume_above ¶
volume_above(z: float) -> float
Cylinder volume above a horizontal plane, cutting cylinders that cross it.
Use it to join a buttress_mesh to the model: the buttress
volume below its top plus the cylinders above it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
float
|
Absolute height of the plane (e.g. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Volume (m³); a cylinder crossing the plane counts by the share of its axis above it. |
above ¶
above(z: float) -> QSM
The model above a horizontal plane, cutting cylinders that cross it.
The companion of volume_above: a cylinder crossing the plane
keeps the part above it, one below it is dropped, and a child whose
parent went is made a branch of its own. Use it to leave room for a
Buttress under the wood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
float
|
Absolute height of the plane (e.g. |
required |
Returns:
| Type | Description |
|---|---|
QSM
|
A new model; the original is unchanged. |
Notes
A cylinder is cut where its axis crosses the plane, so a leaning one keeps a slanted stub rather than being squared off.
to_ply ¶
to_ply(path: str | Path, sides: int = 12, color=None, contiguous: bool = False) -> None
Write the cylinder mesh as a binary PLY with face colours.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
sides
|
int
|
Facets around each cylinder. |
12
|
color
|
One RGB triple (0-255) for every face; by default faces are coloured by branch order, brown stem to green twigs. |
None
|
|
contiguous
|
bool
|
One continuous tube per branch (see |
False
|
PlotQSMs
dataclass
¶
Every tree of a plot modelled; build with build_plot.
Attributes:
| Name | Type | Description |
|---|---|---|
models |
dict[int, QSM]
|
|
buttresses |
dict[int, 'Buttress']
|
|
skipped |
dict[int, str]
|
|
volume ¶
volume(tree_id: int) -> float
Wood volume of one tree (m³), buttress included where there is one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tree_id
|
int
|
Which tree. |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
table ¶
table() -> list[dict]
One row per tree, ready for a CSV.
Returns:
| Type | Description |
|---|---|
list of dict
|
|
to_csv ¶
to_csv(path: str | Path) -> None
Write table as a CSV.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
write_meshes ¶
write_meshes(directory: str | Path, fmt: str = 'ply', sides: int = 12, contiguous: bool = True, prefix: str = 'tree') -> list[Path]
Write a surface mesh per tree into directory.
A tree with a buttress is written fused (Buttress.fuse), so
the flanged base and the cylinders above it come out as one file;
every other tree is its cylinder mesh.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
str | Path
|
Created if it does not exist. |
required |
fmt
|
('ply', 'obj')
|
PLY is binary and carries face colours; OBJ is text and keeps the buttress and the wood as named objects. |
"ply"
|
sides
|
int
|
Facets around each cylinder. |
12
|
contiguous
|
bool
|
One continuous tube per branch (see |
True
|
prefix
|
str
|
File name stem; files are |
'tree'
|
Returns:
| Type | Description |
|---|---|
list of pathlib.Path
|
The files written, in tree order. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown format. |
write_cylinders ¶
write_cylinders(directory: str | Path, prefix: str = 'tree') -> None
Write one cylinder CSV per tree into directory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
str | Path
|
Created if it does not exist. |
required |
prefix
|
str
|
File name stem; files are |
'tree'
|
buttress_mesh ¶
buttress_mesh(cloud: PointCloud, base_xy, ground_z: float | None = None, height_attr: str = 'height', resolution: float = 0.02, slice_height: float = 0.05, close_radius: float = 0.08, max_radius: float = 4.0, max_height: float = 6.0, top: float | None = None, solidity: float = 0.9, max_flare: float = 1.0, smooth: int = 10) -> Buttress
Rebuild a buttressed or otherwise irregular stem base as a closed mesh.
A cylinder cannot follow a flanged base: a circle fitted to a star-shaped section misses the flanges or spans the gaps between them (at 1.3 m a big tropical tree can be a 3 m wide star that a circle explains 15 % of). Here the base is rebuilt volumetrically, so any shape works:
- The tree's points are cut into
slice_heightslices and rasterised atresolution. - A morphological closing of
close_radiusbridges gaps that occlusion leaves in the bark, and flood-filling from outside gives the solid cross-section. Where the outline does not close (part of the bark unseen), the seen bark is kept, thickened toclose_radius(a flange seen from outside becomes a flange of that thickness), plus a circle where the points form a good arc of a round stem. - Slices are built from the top down. A buttress only widens towards
the ground, so each section contains the one above, and the part of a
slice kept is the part connected to the section above, and no wider
than
max_flareallows: neighbouring stems, shrubs and logs stay out, litter and ground around the base are not closed into the solid, and the core carries down where near the ground only the outsides of the flanges were seen. - The buttress ends where the section turns convex (solidity at or above
solidityfor four slices), unlesstopis given. - The stacked sections become a watertight surface (surface nets, Gibson 1998, smoothed as in Taubin 1995); the volume is the sum of slice areas, with the boundary cells counted as half.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
One tree's points (or the plot's; only points within |
required |
base_xy
|
Stem centre |
required | |
ground_z
|
float | None
|
Terrain elevation at the stem, to place the mesh; |
None
|
height_attr
|
str
|
Attribute holding height above ground. |
'height'
|
resolution
|
float
|
Raster cell (m). |
0.02
|
slice_height
|
float
|
Slice thickness (m). |
0.05
|
close_radius
|
float
|
Gaps in the outline up to twice this wide are bridged (m). Larger closes more occlusion but also fills narrow gaps between flanges. |
0.08
|
max_radius
|
float
|
Horizontal reach from the stem centre (m). |
4.0
|
max_height
|
float
|
Highest possible top (m above ground). |
6.0
|
top
|
float | None
|
Buttress top (m above ground); found from the solidity if None. |
None
|
solidity
|
float
|
Solidity at which a section counts as a round stem. |
0.9
|
max_flare
|
float
|
How fast a section may widen going down (m out per m down); the default 1.0 is 45°. Flanges flare well within this, while litter and the ground around the base do not, so a scan line that rings the stem is no longer closed into the solid. 0 lifts the limit. |
1.0
|
smooth
|
int
|
Taubin smoothing passes over the mesh. |
10
|
Returns:
| Type | Description |
|---|---|
Buttress
|
Empty (no faces, zero volume) if too few points are near the stem. |
Examples:
>>> b = qsm.buttress_mesh(tree, (t.x, t.y))
>>> b.volume, b.top, b.total_volume(model)
>>> b.to_obj("buttress.obj")
write_obj ¶
write_obj(path: str | Path, meshes: list[tuple[ndarray, ndarray]], names: list[str] | None = None) -> None
Write several meshes to one OBJ file, one named object each.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
meshes
|
list[tuple[ndarray, ndarray]]
|
|
required |
names
|
list[str] | None
|
Object names; |
None
|
write_ply_mesh ¶
write_ply_mesh(path: str | Path, vertices: ndarray, faces: ndarray, face_rgb: ndarray | None = None) -> None
Write a binary little-endian PLY triangle mesh.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Output file. |
required |
vertices
|
ndarray
|
|
required |
faces
|
ndarray
|
|
required |
face_rgb
|
ndarray | None
|
Optional |
None
|
build_qsm ¶
build_qsm(cloud: PointCloud, base_xy=None, k: int = 15, max_edge: float = 1.0, bin_length: float = 0.1, min_points: int = 1, ransac_threshold: float = 0.02, max_radius: float = 1.0, taper_limit: float = 1.1, max_rmse: float = 0.03, smooth_steps: int = 10, apex_radius: float = 0.0025, min_arc_deg: float = 90.0, min_inlier_fraction: float = 0.05, prune_points: int = 5, fit_min_points: int = 50, crop_length: float = 0.0, butt_height: float = 0.6, relative_tolerance: float = 0.08, base_radius: float = 0.0, allometry_tolerance: float = 0.3, stem_radius_cap: float = 0.0, buttress_equivalent_area: bool = True, buttress_max_inlier_fraction: float = 0.3, pipe_slack: float = 1.2, branch_min_inlier_fraction: float = 0.3, spacing_scale: float = 1.5, radius_power: float = 0.0, power_above_spacing: float = 0.025, sensor_noise: float = 0.02, cluster_eps: float = 0.1, centre_fit_points: int = 100, radius_smooth_steps: int = 15, butt_swell: float = 1.1, butt_vertical_run: int = 4, butt_max_lean_deg: float = 50.0, chain_max_d: float = 0.1, fourier_min_radius: float = 0.15, min_weight: float = 0.0, min_mean_weight: float = 0.5, weights=None) -> QSM
Skeletonise then fit cylinders for a single segmented tree.
Skeleton nodes (geodesic bins of bin_length) are Taubin-smoothed and
a RANSAC circle is fitted to each node's points in the plane
perpendicular to the skeleton; accepted fits need a contiguous arc of
min_arc_deg and min_inlier_fraction inliers. Radii are then
regularised along every root-to-tip path: an allometric prior anchored
on base_radius (pass the measured DBH / 2) replaces weak fits more
than allometry_tolerance away, chains are made non-increasing, gaps
are interpolated and unmeasured branches take the prior, a pipe model
(Shinozaki et al. 1964) that shares a parent's cross-section among its
children. Past 2.5 cm point spacing the radius is a power mean of the
distances to the axis, after raycloudtools (Devereux et al. 2026).
Leafy tips shorter than crop_length are not reconstructed. Run
wood_points first on leafy trees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
One segmented tree, ideally wood only ( |
required |
base_xy
|
Stem position (e.g. |
None
|
|
k
|
int
|
kNN graph: neighbours per point and longest edge (m). Raise
|
15
|
max_edge
|
int
|
kNN graph: neighbours per point and longest edge (m). Raise
|
15
|
bin_length
|
float
|
Geodesic shell width (m); sets the cylinder length. |
0.1
|
min_points
|
int
|
Smallest segment kept. |
1
|
ransac_threshold
|
float
|
Circle inlier distance (m). |
0.02
|
max_radius
|
float
|
Upper limit on any radius (m); set from the measured DBH. |
1.0
|
taper_limit
|
float
|
A child may be at most this times its parent's radius; wider fits are clamped. |
1.1
|
max_rmse
|
float
|
Circle fits with a larger RMSE (m) are rejected. |
0.03
|
smooth_steps
|
int
|
Smoothing passes over skeleton node positions. |
10
|
apex_radius
|
float
|
Smallest tip radius (m). |
0.0025
|
min_arc_deg
|
float
|
Contiguous arc a circle fit must cover (degrees). |
90.0
|
min_inlier_fraction
|
float
|
Inlier share a stem / branch circle needs. |
0.05
|
branch_min_inlier_fraction
|
float
|
Inlier share a stem / branch circle needs. |
0.05
|
prune_points
|
int
|
Unmeasured leaf segments with fewer points are pruned as foliage. |
5
|
fit_min_points
|
int
|
Segments with fewer points take their radius from the taper model. |
50
|
crop_length
|
float
|
Leafy tips with less subtree length (m) are not reconstructed. |
0.0
|
butt_height
|
float
|
Below this height (m) only the largest component per shell is kept. |
0.6
|
relative_tolerance
|
float
|
Refit band around the circle, as a fraction of its radius. |
0.08
|
base_radius
|
float
|
Breast-height radius (m) anchoring the taper prior; 0 estimates it. Pass a field DBH / 2 when you have one. |
0.0
|
allometry_tolerance
|
float
|
Weak fits further than this fraction from the prior are replaced. |
0.3
|
stem_radius_cap
|
float
|
With a |
0.0
|
buttress_equivalent_area
|
bool
|
Use an equivalent-area radius where a circle explains fewer than that share of a section's points (buttresses, fluting). |
True
|
buttress_max_inlier_fraction
|
bool
|
Use an equivalent-area radius where a circle explains fewer than that share of a section's points (buttresses, fluting). |
True
|
pipe_slack
|
float
|
Children's summed cross-section may exceed the parent's by this factor (pipe model); 0 disables. |
1.2
|
cluster_eps
|
float
|
Clustering radius within a shell (m); 0 uses graph components. |
0.1
|
centre_fit_points
|
int
|
Clusters with at least this many points get a circle-fitted centre. |
100
|
radius_smooth_steps
|
int
|
Smoothing passes over radii along each axis. |
15
|
butt_swell
|
float
|
Unmeasured stem nodes below the lowest fit may exceed it by this factor. |
1.1
|
butt_vertical_run
|
int
|
Replace the stem below the first run of this many cylinders within
|
4
|
butt_max_lean_deg
|
int
|
Replace the stem below the first run of this many cylinders within
|
4
|
chain_max_d
|
float
|
Skeleton chaining radius (m). |
0.1
|
fourier_min_radius
|
float
|
Sections at least this wide (m) and well covered use an equivalent-area Fourier contour instead of a circle; 0 disables. |
0.15
|
min_weight
|
float
|
With |
0.0
|
min_mean_weight
|
float
|
With |
0.5
|
weights
|
Wood weight per point in [0, 1], for example a wood confidence
( |
None
|
Returns:
| Type | Description |
|---|---|
QSM
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Notes
On 72 destructively harvested trees volume has -3.8 % bias and
19.9 % rRMSE (Benchmarks > QSMs). On simulated trees the deficit is
in twigs: about a third of the twig length is recovered, so
measured_length_fraction is low even when volume is good.
fit_cylinder ¶
fit_cylinder(xyz: ndarray, axis_init=None) -> dict
Least-squares cylinder through 3D points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xyz
|
ndarray
|
|
required |
axis_init
|
Starting axis direction; the principal direction of the points if None. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
|
fit_cylinder_ransac ¶
fit_cylinder_ransac(xyz: ndarray, threshold: float = 0.02, iterations: int = 100, sample_size: int = 12, seed: int = 0) -> dict
Cylinder fit that tolerates outliers (leaves, twigs, noise).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xyz
|
ndarray
|
|
required |
threshold
|
float
|
Inlier distance from the surface (m). |
0.02
|
iterations
|
int
|
RANSAC trials. |
100
|
sample_size
|
int
|
Points per trial fit. |
12
|
seed
|
int
|
Random seed. |
0
|
Returns:
| Type | Description |
|---|---|
dict
|
As |
skeletonize ¶
skeletonize(cloud: PointCloud, base_xy=None, k: int = 15, max_edge: float = 1.0, bin_length: float = 0.1) -> dict
Graph skeleton of one tree, the first stage of build_qsm.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
One tree's (wood) points. |
required |
base_xy
|
Stem position; the lowest points if None. |
None
|
|
k
|
int
|
Neighbours in the kNN graph. |
15
|
max_edge
|
float
|
Longest graph edge (m). |
1.0
|
bin_length
|
float
|
Width of the geodesic distance bins (m). |
0.1
|
Returns:
| Type | Description |
|---|---|
dict
|
|
build_plot ¶
build_plot(cloud: PointCloud, labels, stems=None, voxel_size: float = 0.01, wood=True, buttress: bool = False, min_points: int = 2000, height_attr: str = 'height', **params) -> PlotQSMs
A QSM for every tree of a segmented plot.
The loop that build_qsm needs around it: each tree's points are
taken from labels, thinned, put through the wood filter and fitted,
and a tree that cannot be fitted is recorded rather than raising. Progress
is reported (sylva.util.progress), so a plot of a few hundred trees is
not silent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
The whole plot, height-normalised (needed for |
required |
labels
|
Tree id per point, as |
required | |
stems
|
The detected trees, used for the stem centre each model is built
around, and their DBH, which anchors each model's base radius
( |
None
|
|
voxel_size
|
float
|
Thin each tree to this spacing first (m); 0 keeps every point. |
0.01
|
wood
|
(bool, array or str)
|
Where each tree's wood comes from. True (default) runs
|
True
|
buttress
|
bool
|
Look for a buttress on each tree ( |
False
|
min_points
|
int
|
Trees with fewer points than this are skipped. |
2000
|
height_attr
|
str
|
Attribute holding height above ground. |
'height'
|
**params
|
Passed to |
{}
|
Returns:
| Type | Description |
|---|---|
PlotQSMs
|
The models, any buttresses, and why a tree was skipped. |
Examples:
>>> labels = trees.segment_trees(cloud, stems)
>>> plot = qsm.build_plot(cloud, labels, stems)
>>> plot.total_volume, len(plot), plot.skipped
>>> plot.to_csv("trees.csv"); plot.write_cylinders("qsms/")
Notes
Volumes are the cylinders' own unless a buttress was meshed, in which
case PlotQSMs.volume is the mesh below its top plus the cylinders
above it.
A QSM needs points on the stem surface: with roughly 1 cm spacing the
default 0.1 m shells hold plenty, but a cloud thinned to 3-5 cm leaves
most shells with too few, and those cylinders take their radius from the
taper and pipe-model priors instead. That runs large - on one 20 m
savanna tree, 0.34 m DBH at full resolution against 1.10 m at 5 cm - so
build_plot warns when the median model was hardly fitted at all, and
measured_length in PlotQSMs.table says so per tree.
wood_points ¶
wood_points(cloud: PointCloud, k: int = 20, threshold: float = 0.85, voxel_size: float | None = 0.02, medium_threshold: float = 0.75, scale_radius: float = 0.0, passage: bool = True, min_passage: int = 3, target_res: float = 0.2, graph_k: int = 10, max_edge: float = 1.0, base_height: float = 0.25, assign_dist: float = 0.05, assign_scale: float = 0.0, component_res: float = 0.05, component_min: int = 200, sor_k: int = 50, sor_std: float = 1.0, dilate_dist: float = 0.03, method: str = 'passage') -> PointCloud
Leaf / wood separation for one tree's points, returning the wood
thinned to voxel_size.
Two cues are combined. Local anisotropy -- planarity + linearity over
k neighbours (and, with scale_radius > 0, again over that wider
neighbourhood, the lower score counting: better leaf labels, but it
removes wood a QSM needs, see sylva.leaves.classify_leaf_wood)
-- marks bark and branch surfaces
above threshold (high likelihood) and medium_threshold (kept
only after statistical outlier removal, sor_k / sor_std, and
next to wood already found; set it at or above threshold to skip
the step, which on small leafy crowns pulls foliage in around the
stem). Topology recovers what anisotropy
misses (the path-frequency cue of Vicari et al. 2019): shortest paths
from the base over a kNN graph are traced to one target per
target_res cell, and any point that at least
min_passage of those paths run through is wood, with its neighbours
within assign_dist -- a roughly or thinly scanned trunk is neither
planar nor linear locally but every path to the crown crosses it. A
path follows one side of a stem, so this keeps the points on and beside
the paths rather than the whole section; assign_scale (try 0.03)
widens the reach with the share of the tree a point carries, up to that
fraction of the tree height on the trunk. High-likelihood points survive in connected
components (component_res) that touch passage wood or hold
component_min points; the result is dilated by dilate_dist for
thin branches. passage=False gives the anisotropy-only filter.
This filter is tuned to give a QSM what it needs (every stem and
branch surface), not the most accurate leaf labels; for leaf area and
leaf angles use sylva.leaves.classify_leaf_wood.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cloud
|
PointCloud
|
One segmented tree. |
required |
k
|
int
|
Neighbours for the anisotropy features. |
20
|
threshold
|
float
|
High- and medium-likelihood anisotropy cut-offs (0-1). |
0.85
|
medium_threshold
|
float
|
High- and medium-likelihood anisotropy cut-offs (0-1). |
0.85
|
voxel_size
|
float | None
|
Thin the input to this spacing (m) first; None or 0 keeps every point. |
0.02
|
scale_radius
|
float
|
Second, wider feature scale (m); 0 disables. |
0.0
|
passage
|
bool
|
Path-passage wood: enable, paths a point must carry, and target cell size (m). |
True
|
min_passage
|
bool
|
Path-passage wood: enable, paths a point must carry, and target cell size (m). |
True
|
target_res
|
bool
|
Path-passage wood: enable, paths a point must carry, and target cell size (m). |
True
|
graph_k
|
int
|
kNN graph neighbours, longest edge (m), and height (m) of the base region the paths start from. |
10
|
max_edge
|
int
|
kNN graph neighbours, longest edge (m), and height (m) of the base region the paths start from. |
10
|
base_height
|
int
|
kNN graph neighbours, longest edge (m), and height (m) of the base region the paths start from. |
10
|
assign_dist
|
float
|
Reach (m) around passage points, and its growth with the share of the tree a point carries. |
0.05
|
assign_scale
|
float
|
Reach (m) around passage points, and its growth with the share of the tree a point carries. |
0.05
|
component_res
|
float
|
Connected-component cell size (m) and minimum size. |
0.05
|
component_min
|
float
|
Connected-component cell size (m) and minimum size. |
0.05
|
sor_k
|
int
|
Statistical outlier removal for the medium-likelihood points. |
50
|
sor_std
|
int
|
Statistical outlier removal for the medium-likelihood points. |
50
|
dilate_dist
|
float
|
Final dilation (m). |
0.03
|
method
|
('passage', 'gbs')
|
|
"passage"
|
Returns:
| Type | Description |
|---|---|
PointCloud
|
The wood points, thinned to |