Skip to content

PointCloud and Raster

sylva.pointcloud

Numpy-backed point cloud container.

PointCloud dataclass

A set of 3D points with optional per-point attributes.

Every function in Sylva that takes points takes a PointCloud and returns a new one; nothing is modified in place. Coordinates are metres in whatever frame the data came in (scanner, project or projected CRS).

Parameters:

Name Type Description Default
xyz ndarray

(N, 3) array of coordinates, stored as C-contiguous float64.

required
attrs dict[str, ndarray]

Mapping of attribute name to a length-N array (intensity, classification, height, ...). Names follow laspy conventions so LAS files round-trip.

dict()
crs str | None

Coordinate reference system of xyz, or None when unknown: an EPSG code ("EPSG:7855"; an integer is turned into that form), a PROJ string or WKT. sylva.read sets it from LAS/LAZ headers and sylva.write stores it in LAS/LAZ files. It is carried through subsetting, transform, translate, rotate, recentre and concatenate unchanged, so after a local shift it describes the frame the offset returns to; sylva.geo.coords.reproject changes it.

None

Raises:

Type Description
ValueError

If xyz is not (N, 3), an attribute is not length N or crs is not a string, integer or None.

Notes

Attributes Sylva itself writes and reads:

Name Written by Meaning
classification sylva.ground.classify_ground_csf ASPRS codes, 2 = ground
height sylva.ground.normalize_height height above the DTM (m)
tree_id you, from sylva.trees.segment_trees tree number, -1 unassigned
scan_id sylva.registration.merge_scans index of the source scan
nx, ny, nz ray-cloud PLY vector from point to sensor

Indexing with a boolean mask, integer array or slice returns a subset with all attributes: cloud[cloud.z > 1.3].

Examples:

>>> import numpy as np, sylva
>>> cloud = sylva.PointCloud(np.random.rand(100, 3), {"intensity": np.ones(100)})
>>> tall = cloud[cloud.z > 0.5]

x property

x: ndarray

x coordinates, a view into xyz (length N).

y property

y: ndarray

y coordinates, a view into xyz (length N).

z property

z: ndarray

z coordinates, a view into xyz (length N).

bounds property

bounds: tuple[ndarray, ndarray]

Axis-aligned bounding box as (min_xyz, max_xyz), two length-3 arrays.

heights

heights(attr: str = 'height') -> np.ndarray

Height above ground for each point.

Parameters:

Name Type Description Default
attr str

Attribute holding normalised heights.

'height'

Returns:

Type Description
ndarray

attr as float64 if the cloud has it, otherwise z. Falling back to z is only right for clouds that are already height-normalised (e.g. from sylva.ground.flatten).

copy

copy() -> PointCloud

Deep copy.

Returns:

Type Description
PointCloud

A cloud whose coordinates and attribute arrays are independent of this one's.

with_attrs

with_attrs(**attrs: ndarray) -> PointCloud

Add or replace attributes.

Parameters:

Name Type Description Default
**attrs ndarray

name=array pairs, each length N.

{}

Returns:

Type Description
PointCloud

A new cloud sharing the coordinate array; existing attributes are kept unless replaced.

without

without(*names: str) -> PointCloud

Drop attributes.

Parameters:

Name Type Description Default
*names str

Attribute names to remove; names that are absent are ignored.

()

Returns:

Type Description
PointCloud

A new cloud sharing the coordinate array.

where

where(expr: str) -> PointCloud

Points satisfying an attribute expression.

Parameters:

Name Type Description Default
expr str

A condition over the coordinates x, y, z and the attributes, e.g. "height > 2 & classification != 2"; see sylva.geo.masks.expression for the syntax.

required

Returns:

Type Description
PointCloud

The points for which expr is true, with all attributes, in their original order.

Raises:

Type Description
ValueError

On a syntax error or an unknown attribute; the message gives the position.

transform

transform(matrix: ndarray) -> PointCloud

Apply a rigid or affine transform.

Parameters:

Name Type Description Default
matrix ndarray

(4, 4) homogeneous matrix acting on column vectors, e.g. a RIEGL SOP from sylva.io.read_matrix_file or the result of sylva.registration.icp.

required

Returns:

Type Description
PointCloud

Transformed copy. Attributes are carried over unchanged, so direction-like attributes (nx, ny, nz) are not rotated.

Raises:

Type Description
ValueError

If matrix is not 4x4.

translate

translate(dx: float, dy: float, dz: float = 0.0) -> PointCloud

Shift every point by a constant offset.

Parameters:

Name Type Description Default
dx float

Offset in the units of the coordinates (m).

required
dy float

Offset in the units of the coordinates (m).

required
dz float

Offset in the units of the coordinates (m).

required

Returns:

Type Description
PointCloud

Shifted copy with the same attributes and crs. Each coordinate gets one addition, so shifting back by the negated offset returns the original to within one rounding, and exactly for whole-metre offsets of millimetre data.

Raises:

Type Description
ValueError

If an offset is not finite.

Examples:

>>> local = cloud.translate(-500_000, -6_900_000)

rotate

rotate(angle_deg: float, axis: str | Iterable[float] = 'z', about: Iterable[float] | None = None) -> PointCloud

Rotate every point about an axis.

Parameters:

Name Type Description Default
angle_deg float

Angle in degrees, positive counter-clockwise when looking down the axis towards the origin (right-handed): 90 about "z" turns +x into +y.

required
axis str | Iterable[float]

"x", "y", "z" or any 3-vector (normalised here).

'z'
about Iterable[float] | None

A point (x, y, z) on the axis; the origin if None. Rotate about the plot centre, or recentre first, to keep projected coordinates from swinging far away.

None

Returns:

Type Description
PointCloud

Rotated copy with the same attributes and crs. Direction-like attributes (nx, ny, nz) are not rotated. Multiples of 90 degrees are exact.

Raises:

Type Description
ValueError

If the angle, axis or centre is not finite, the axis is zero or an unknown name, or about does not have three values.

Examples:

>>> turned = cloud.rotate(30, about=cloud.xyz.mean(axis=0))

recentre

recentre(origin: Iterable[float] | None = None) -> tuple[PointCloud, np.ndarray]

Move the coordinates close to zero.

Projected coordinates in the millions of metres leave float32 (and many viewers and meshing tools) with centimetre precision; local coordinates keep it.

Parameters:

Name Type Description Default
origin Iterable[float] | None

Point that becomes (0, 0, 0). By default the minimum corner of the (finite) points rounded down to whole metres, so the offset is exact and short.

None

Returns:

Name Type Description
cloud PointCloud

Shifted copy with the same attributes and crs.

offset ndarray

Length-3 offset that undoes the shift: cloud.translate(*offset) gives the original coordinates.

Raises:

Type Description
ValueError

If origin is not three finite numbers.

Examples:

>>> local, offset = cloud.recentre()
>>> restored = local.translate(*offset)

concatenate classmethod

concatenate(clouds: Iterable[PointCloud]) -> PointCloud

Stack several clouds into one.

Parameters:

Name Type Description Default
clouds Iterable[PointCloud]

Clouds to merge, in order.

required

Returns:

Type Description
PointCloud

All points; only attributes present in every input are kept. Use sylva.registration.merge_scans to also record which scan each point came from. crs is the inputs' common CRS (clouds without one are taken to share it), or None.

Raises:

Type Description
ValueError

If clouds is empty, or two clouds have different CRSs.

from_array classmethod

from_array(array: ndarray, names: Mapping[int, str] | None = None) -> PointCloud

Build a cloud from a 2D array.

Parameters:

Name Type Description Default
array ndarray

(N, 3 + k) array; the first three columns are x, y, z.

required
names Mapping[int, str] | None

Column index to attribute name, e.g. {3: "intensity"}. Unnamed extra columns are called col3, col4, ...

None

Returns:

Type Description
PointCloud

sylva.raster

Georeferenced 2D grid for DTMs and CHMs.

Raster dataclass

A regular 2D grid in map coordinates, used for DTMs and CHMs.

data[row, col] covers x from xmin + col * resolution and y from ymin + row * resolution, so row 0 is the southern edge (the array is upside down compared with an image). to_geotiff and to_ascii_grid flip it to the usual north-up order on export.

Parameters:

Name Type Description Default
data ndarray

(rows, cols) values; NaN marks cells with no data.

required
xmin float

Coordinates of the south-west corner of the grid (not of the first cell centre).

required
ymin float

Coordinates of the south-west corner of the grid (not of the first cell centre).

required
resolution float

Cell size in the units of x and y (m).

required
crs str | None

Optional CRS string ("EPSG:28355") written by to_geotiff and, as a .prj, by to_ascii_grid. Sylva never reprojects.

None

shape property

shape: tuple[int, int]

(rows, cols) of data.

xmax property

xmax: float

Eastern edge of the grid.

ymax property

ymax: float

Northern edge of the grid.

cell_centers

cell_centers() -> tuple[np.ndarray, np.ndarray]

Cell-centre coordinates.

Returns:

Type Description
X, Y : numpy.ndarray

Two (rows, cols) arrays, aligned with data.

cell_index

cell_index(x, y) -> tuple[np.ndarray, np.ndarray]

Cell containing each coordinate.

Parameters:

Name Type Description Default
x

Coordinates (scalars or arrays of the same shape).

required
y

Coordinates (scalars or arrays of the same shape).

required

Returns:

Type Description
row, col : numpy.ndarray

int64 indices. They are not clipped: points outside the grid get negative or too-large indices, so check against shape.

sample

sample(x, y) -> np.ndarray

Interpolate the grid at arbitrary coordinates.

Parameters:

Name Type Description Default
x

Coordinates; flattened to 1D.

required
y

Coordinates; flattened to 1D.

required

Returns:

Type Description
ndarray

Bilinear interpolation between cell centres, one value per coordinate. Coordinates beyond the outer cell centres take the edge value; a NaN neighbour makes the result NaN, so call fill_nearest first if the grid has holes.

fill_nearest

fill_nearest() -> Raster

Fill NaN cells from the nearest valid cell.

Returns:

Type Description
Raster

A copy with every NaN replaced; the input is unchanged.

to_geotiff

to_geotiff(path: str | Path, crs: str | None = None) -> None

Write a single-band float32 GeoTIFF, north-up, nodata = NaN.

Written by Sylva itself (uncompressed, in strips; BigTIFF from 4 GB), so no GDAL or rasterio is needed.

Parameters:

Name Type Description Default
path str | Path

Output file.

required
crs str | None

CRS to write (an EPSG code such as "EPSG:28356", or WKT); defaults to crs. With neither, the file has a geotransform but no CRS. A CRS with an EPSG code goes in the file's GeoKeys; one without (custom WKT) goes in a GDAL <path>.aux.xml beside it, which GDAL-based software reads.

None

Raises:

Type Description
ValueError

For an empty raster or a CRS that cannot be read.

to_ascii_grid

to_ascii_grid(path: str | Path, nodata: float = -9999.0, crs: str | None = None) -> None

Write an ESRI ASCII grid (.asc), north-up, with its CRS in a .prj beside it.

Parameters:

Name Type Description Default
path str | Path

Output file.

required
nodata float

Value written for NaN cells (NODATA_value in the header).

-9999.0
crs str | None

CRS to write, as WKT, to the .prj beside it (dtm.asc -> dtm.prj); defaults to crs. With neither, no .prj is written.

None

from_ascii_grid classmethod

from_ascii_grid(path: str | Path) -> Raster

Read an ESRI ASCII grid written by to_ascii_grid or GIS software.

Parameters:

Name Type Description Default
path str | Path

.asc file. NODATA_value cells become NaN.

required

Returns:

Type Description
Raster

Grid with row 0 at the south; crs from the .prj beside it (EPSG:<code> when its WKT names one), else None.