Skip to content

sylva.quality

sylva.quality

Point-cloud quality measured on tree stems.

Between 1 and 3 m a stem is the one surface in a forest scan whose shape is known well enough to measure the scanner against. Each stem is cut into thin slices and a circle fitted to every slice from all scans together; every point's radial residual is then read per scan position::

from sylva import quality

q = quality.stem_noise(cloud, scan_id=quality.scan_ids_from_origins(origins))
q.summary()      # sigma_total, sigma_within, sigma_local, registration, tails
q.scans          # per scan: tx, ty (horizontal offset), sigma_within, sigma_local

What each spread contains:

sigma_total all points about the shared circle, as the cloud stands: range noise, bark, the stem's departure from a circle and misregistration together. A whole-stem circle cannot get below a few millimetres here even for a perfect scanner (about 7 mm on synthetic stems). sigma_within one scan's points about their own median in the slice: range noise, bark and stem shape over the arc that scan saw; misregistration removed. sigma_local what remains after removing a smooth curve along that arc (constant, first and second harmonics of the angle): range noise and bark only, the measure closest to the scanner's own noise. tx, ty the horizontal offset of each scan, from sum (r - t . n)^2 over its points on every stem (n the outward normal; Huber 1964 weights), refined by moving the scans back and refitting; relative to the mean of all scans, since a common shift is invisible. Vertical misregistration does not show on vertical stems.

StemNoise dataclass

Result of stem_noise; tables are dicts of equal-length arrays.

Attributes:

Name Type Description
slices dict

Per stem slice: stem, height, cx, cy, radius, n_points, sigma (after moving scans back), sigma_first (as the cloud stands), arc (degrees seen) and tail_fraction.

scan_slices dict

Per scan and slice: scan, slice, n_points, median_residual, sigma_within, sigma_local.

scans dict

Per scan: scan, n_points, n_slices, tx, ty (horizontal offset, m), sigma_within, sigma_local.

residual ndarray

Radial residual of each input point (m); NaN where not used.

summary

summary(min_scan_slices: int = 1) -> dict

Plot-level quality figures, in metres.

Parameters:

Name Type Description Default
min_scan_slices int

Scans measured in fewer stem slices than this are left out of the registration figures. A scan that saw only a few stem points (an outer position looking away from the plot) gets an offset fitted to almost nothing, which would otherwise set registration_max.

1

Returns:

Type Description
dict

n_stems, n_slices, n_scans; point-weighted medians sigma_total, sigma_corrected, sigma_within, sigma_local; tail_fraction; and with several scans registration_rms, registration_max and worst_scan over the n_scans_registered scans with enough slices. Only the counts are present if no slice qualified.

Notes

On the TERN CUP 2022 plot sigma_local was 6.7 mm and registration_rms 4.7 mm; two halves of the stems agreed to 1.1 mm. Compare scans and dates with the same settings, not with a fixed threshold.

scan_ids_from_origins

scan_ids_from_origins(origins, tolerance: float = 0.05) -> np.ndarray

Scan position of each point, from its sensor origin.

Parameters:

Name Type Description Default
origins

(N, 3) sensor position of each point, e.g. shots.origin[shots.shot_of_echo()] or a ray cloud's end point plus nx, ny, nz.

required
tolerance float

Origins on the same tolerance (m) grid cell are one position.

0.05

Returns:

Type Description
ndarray

int64 id per point, 0 .. n-1 in sorted order of the origins.

stem_noise

stem_noise(cloud: PointCloud, scan_id=None, stems=None, height_attr: str = 'height', iterations: int = 3, **params) -> StemNoise

Stem-based noise and registration of cloud.

scan_id: per-point scan position (array, the name of an attribute, or None for a single scan; see scan_ids_from_origins). stems: stem positions as (n, 2) xy, a list of sylva.trees.Tree, or None to detect them. Heights come from height_attr if the cloud has it, else from the lowest point. params: height_min=1, height_max=3, step=0.25, thickness=0.1 (slices, m above ground), min_radius=0.05, max_radius=1, min_arc=270 (deg a slice's circle must see), min_inlier_fraction=0.5, cut_min=0.05 and cut_fraction=0.3 (points further than max(cut_min, cut_fraction * r) from the circle are not stem), min_scan_points=30. iterations of offset refinement.

Parameters:

Name Type Description Default
cloud PointCloud

Registered multi-scan cloud (or one scan) covering 1-3 m of the stems.

required
scan_id

Scan position per point, as above.

None
stems

Stem positions, as above.

None
height_attr str

Attribute holding height above ground.

'height'
iterations int

Rounds of moving the scans back and refitting.

3
**params

The slice and fit settings above.

{}

Returns:

Type Description
StemNoise

Call StemNoise.summary for plot-level numbers.

Notes

Needs stems that are roughly round and seen from most sides: at least min_arc degrees of each slice, so single scans of isolated trees usually qualify only with a lower min_arc.