The georeferenced point cloud (PointCloud)#

A point cloud represents 2D point geometries of georeferenced coordinates associated with a main 1D data array, and optionally auxiliary data.

Although a subtype of Vector, point clouds have a very different nature than other vectors and are ubiquitous in geospatial analysis, requiring their own object type. For numerical operations, a point cloud manipulation is facilitated by its own arithmetic to manipulate its main data array, as well as a specific interface with rasters (interpolation or reduction to same coordinates, or point gridding), or other point clouds (pairwise distance matching). For geometric operations, point clouds interface in specific ways with other vector-types (zonal statistics, geometric masking).

GeoUtils aims to support these features, as well the reading and writing of point clouds both from vector-type files (e.g., ESRI shapefile, geopackage, geoparquet) usually used for sparse point clouds, and from point-cloud-type files (e.g., LAS, LAZ, COPC) usually used for dense point clouds.

Warning

Support for LAS files is still preliminary and loads all data in memory for most operations. We are working on adding operations with chunked reading.

Below, a summary of the PointCloud object and its methods.

Object definition and attributes#

A PointCloud is a Vector is a vector of 2D point geometries associated to numeric values from a main data column, and can also contain auxiliary data columns.

It inherits the main Vector attribute ds containing the geodataframe, and adds another main attribute data_column that identifies the name of the main data associated to the point geometries.

Additionally, new attributes such as point_count and new methods specific to point clouds are detailed further below.

Generic vector attributes and methods are inherited through the Vector object, such as bounds, crs, reproject() and crop().

Tip

The complete list of Vector attributes and methods can be found in the Vector section of the API.

Open and save#

A PointCloud is opened by instantiating the class with a str, a pathlib.Path, a geopandas.GeoDataFrame, a geopandas.GeoSeries or a shapely.Geometry.

import geoutils
import numpy as np

# Instantiate a point cloud from a filename on disk
filename_dem = geoutils.examples.get_path("coromandel_lidar")
pc = geoutils.PointCloud(filename_dem, data_column="Z")
pc
PointCloud(
  ds=              Z                         geometry
       0       816.078  POINT (1838933.295 5887999.962)
       1       818.823  POINT (1838930.054 5887999.966)
       2       811.059  POINT (1838933.316 5887999.967)
       3       818.151  POINT (1838928.638 5887999.961)
       4       817.599  POINT (1838926.098 5887999.995)
       ...         ...                              ...
       584343  827.438  POINT (1838913.663 5888000.872)
       584344  825.099  POINT (1838912.806 5888000.715)
       584345  825.419  POINT (1838912.889 5888001.284)
       584346  825.204   POINT (1838912.775 5888000.72)
       584347  822.421  POINT (1838911.847 5888000.565)
       
       [584348 rows x 2 columns]
  crs=COMPD_CS["NZGD2000 / New Zealand Transverse Mercator 2000 + NZVD2016 height",PROJCS["NZGD2000 / New Zealand Transverse Mercator 2000",GEOGCS["NZGD2000",DATUM["New_Zealand_Geodetic_Datum_2000",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6167"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4167"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",173],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",1600000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Northing",NORTH],AXIS["Easting",EAST],AUTHORITY["EPSG","2193"]],VERT_CS["NZVD2016 height",VERT_DATUM["New Zealand Vertical Datum 2016",2005,AUTHORITY["EPSG","1169"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Gravity-related height",UP],AUTHORITY["EPSG","7839"]]]
  bounds=BoundingBox(left=np.float64(1838792.524), bottom=np.float64(5887910.586), right=np.float64(1838937.061), top=np.float64(5888036.092)))

Create from arrays or tuples#

A PointCloud is created from three 1D arrays, from a Nx3 or 3xN array, or from an iterable of 3-tuples by calling the class methods from_xyz(), from_array() or from_tuples() respectively.

# From three 1D arrays
pc1 = geoutils.PointCloud.from_xyz(x=np.array([1, 4]), y=np.array([2, 5]), z=np.array([3, 6]), crs=4326)
# From a Nx3 array
pc2 = geoutils.PointCloud.from_array(np.array([[1, 2, 3], [4, 5, 6]]), crs=4326)
# From a iterable of 3-tuples
pc3 = geoutils.PointCloud.from_tuples([(1, 2, 3), (4, 5, 6)], crs=4326)

Gridding to a raster#

Gridding a PointCloud into a specific grid can be done through the grid() function, that applies a gridding scheme according to a given methods (inverse-distance weighting, delauney triangle interpolation).

# Grid the point cloud on a 100x100 grid on its extent
coords = (np.linspace(pc.bounds.left, pc.bounds.right, 100), np.linspace(pc.bounds.bottom, pc.bounds.top, 100))
rst = pc.grid(grid_coords=coords)

Arithmetic#

A PointCloud can be applied any pythonic arithmetic operation (+, -, /, //, *, **, %) with another PointCloud, ndarray or number. It will output one or two PointClouds. NumPy coercion rules apply for dtype. The operation is applied to the data_column of the point cloud.

# Add 1 and divide point cloud by 2
(pc1 + 1)/2
PointCloud(
  ds=     z     geometry
       0  2.0  POINT (1 2)
       1  3.5  POINT (4 5)
  crs=EPSG:4326
  bounds=BoundingBox(left=np.float64(1.0), bottom=np.float64(2.0), right=np.float64(4.0), top=np.float64(5.0)))

A PointCloud can also be applied any pythonic logical comparison operation (==, !=, >=, >, <=, <) with another PointCloud, ndarray or number. It will cast to a boolean PointCloud.

# What are point cloud pixels are larger than 20?
pc1 > 20
PointCloud(
  ds=       z     geometry
       0  False  POINT (1 2)
       1  False  POINT (4 5)
  crs=EPSG:4326
  bounds=BoundingBox(left=np.float64(1.0), bottom=np.float64(2.0), right=np.float64(4.0), top=np.float64(5.0)))

See Support of pythonic operators for more details.

Array interface#

A PointCloud can be applied any NumPy universal functions and most mathematical, logical or masked-array functions with another PointCloud, ndarray or number. The operation is applied to the data_column of the point cloud.

# Compute the element-wise square-root
np.sqrt(pc1)
PointCloud(
  ds=          z     geometry
       0  1.732051  POINT (1 2)
       1  2.449490  POINT (4 5)
  crs=EPSG:4326
  bounds=BoundingBox(left=np.float64(1.0), bottom=np.float64(2.0), right=np.float64(4.0), top=np.float64(5.0)))

Logical comparison functions will cast to a boolean PointCloud.

# Is the pointcloud close to another one within tolerance?

np.isclose(pc1, pc1+0.05, atol=0.1)
PointCloud(
  ds=      z     geometry
       0  True  POINT (1 2)
       1  True  POINT (4 5)
  crs=EPSG:4326
  bounds=BoundingBox(left=np.float64(1.0), bottom=np.float64(2.0), right=np.float64(4.0), top=np.float64(5.0)))

See Masked-array NumPy interface for more details.

Statistics#

Statistics of a point cloud, optionally subsetting to an inlier mask, can be computed using get_stats().

# Get mean, max and STD of the point cloud
pc.get_stats(["mean", "max", "std"])
{'mean': np.float64(819.6583450461027),
 'max': np.float64(849.053),
 'std': np.float64(16.355949189397542)}

A point cloud can also be quickly subsampled using subsample(), which considers only valid values, and returns either a point cloud or an array:

# Get 500 random points in the point cloud
pc_sub = pc.subsample(500)

See Statistics for more details.