Skip to content

About

Point-cloud processing engine: ground classification, DEM gridding, IDW, volumes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Important

Consolidated into SurveySuite
This engine has been consolidated into the unified SurveySuite monorepo. Active development, bug fixes, releases, and unified testing now live in survey-suite. This repository is preserved as an archive.

survey-pointcloud

Point-cloud processing engine for surveying: LiDAR-style point modelling, voxel thinning, grid-minimum ground classification, DEM gridding (minimum/maximum/IDW), and grid-based cut/fill volumes.

Pure Python, zero dependencies, no UI imports, no file I/O. Part of the survey-suite engine collection.

Installation

pip install -e .

Requires Python ≥ 3.9. No third-party dependencies.

Quick start

from pointcloud import (
    PointCloud, classify_ground_grid, grid_minimum, volume_grid, GROUND,
)

# Load points however you like -- tuples, lists, numpy rows, laspy records.
pts = [(x, y, z) for x, y, z in read_my_points()]

cloud = PointCloud(pts, crs="EPSG:32618")
print(cloud.bounds())       # (min_x, min_y, min_z, max_x, max_y, max_z)
print(cloud.describe())     # count, z stats, classification histogram

# Classify ground (ASPRS class 2), grid a DTM, compute stockpile volume.
ground = PointCloud(classify_ground_grid(pts, cell_size=2.0, threshold=0.15))
dtm = grid_minimum(ground.points, origin_x=0, origin_y=0,
                   pixel_size=1.0, width=200, height=200)
cut, fill, net = volume_grid(dtm, base_elevation=100.0, cell_area=1.0)
print(f"cut={cut:.1f} m3  fill={fill:.1f} m3  net={net:.1f} m3")

API reference

Point model — pointcloud.point

  • Point(x, y, z, intensity=0.0, classification=1, return_number=1, number_of_returns=1) — immutable-by-convention dataclass for a single LiDAR-style point.
  • xyz(p) — extract (x, y, z) from any point-like: Point, tuple/list, object with .x/.y/.z attributes, or numpy row.
  • as_point(p) — coerce a point-like to a canonical Point.
  • ASPRS LAS classification constants: CREATED (0), UNCLASSIFIED (1), GROUND (2), LOW_VEGETATION (3), MEDIUM_VEGETATION (4), HIGH_VEGETATION (5), BUILDING (6), NOISE (7), WATER (9), BRIDGE_DECK (17), plus CLASSIFICATION_NAMES mapping codes to labels.

Cloud container — pointcloud.cloud

  • PointCloud(points, crs="") — coerces inputs to Point on construction.
  • .bounds() → (min_x, min_y, min_z, max_x, max_y, max_z)
  • .describe() → CloudStats(count, z min/max/mean, per-class counts)
  • .crop(min_x, min_y, max_x, max_y) → new cloud in a bounding box
  • .filter_classification([GROUND]) → new cloud of matching classes
  • .thin(voxel_size, keep="lowest"|"first") → voxel-decimated copy
  • .chunks(n) → iterate in n-sized chunks (bounded memory)
  • voxel_thin(points, voxel_size, keep="lowest") — standalone, single-pass O(n) decimation; "lowest" keeps the lowest-z point per cubic voxel.
  • iter_chunks(points, n) — yield successive n-sized chunks.

Ground classification — pointcloud.classify

  • classify_ground_grid(points, cell_size, threshold, ground_class=GROUND) — two-pass O(n) morphological filter: per-cell minimum z becomes the local ground estimate; points within threshold above it are labelled ground. Returns a new list (inputs are never mutated); non-ground points keep their original classification. Best on gentle terrain; see Limitations.

DEM gridding — pointcloud.dem

All functions return plain row-major list[list[float]] grids; empty or out-of-bounds cells are nan nodata.

  • grid_minimum(points, origin_x, origin_y, pixel_size, width, height) — lowest z per cell → DTM-style bare-earth surface.
  • grid_maximum(...) — highest z per cell → DSM-style surface.
  • interpolate_idw(points, origin_x, origin_y, pixel_size, width, height, power=2.0, radius=None) — inverse-distance-weighted interpolation. A point at (or ~at) a cell centre pins that cell. With radius=None every point influences every cell; otherwise cells with no neighbours inside radius are nodata.

Volumes — pointcloud.volumes

  • volume_grid(grid, base_elevation, cell_area) → (cut, fill, net) of a surface against a flat base (borrow-pit / stockpile method). Nodata cells skipped.
  • volume_between(upper, lower, cell_area) → (cut, fill, net) between two co-registered grids, e.g. pre- vs. post-construction DEMs.

Interoperability

This engine is designed to sit between data capture and analysis:

  • Point inputs are duck-typed. Pass plain (x, y, z) tuples, lists, numpy rows, or laspy point records directly — no conversion layer needed. as_point() exists only when you want canonical Point objects.
  • Classification codes follow the ASPRS LAS standard, so classified output round-trips with real LiDAR tooling (laspy, PDAL, CloudCompare).
  • DEM outputs feed survey-raster directly — a grid from grid_minimum/interpolate_idw is already the row-major 2D structure a Raster wraps, so the standard pipeline is: classify → grid → Raster(grid, transform, crs) → indices, stats, hillshade.
  • Coordinates flow to survey-geodesy for datum shifts, projections, and UTM conversion before or after processing; keep one CRS string on the PointCloud so provenance travels with the data.
  • No file I/O by design. LAS/LAZ readers (laspy) and writers live in a thin adapter outside the engine; the engine only ever sees point sequences and grids.

Scaling

  • Single-pass algorithms where it counts. voxel_thin, classify_ground_grid, and min/max gridding are all O(n) with no spatial index — a 10 M-point cloud thins and classifies in one streaming pass.
  • Thin before you interpolate. IDW is O(cells × points) brute force in 0.1.0; run voxel_thin first and it becomes cheap. A KD-tree/indexed neighbour search is the planned upgrade (see Roadmap).
  • Tile large areas. Grids are plain lists — process a site in tiles with crop() + per-tile grid_minimum(), or stream points through iter_chunks() / cloud.chunks(n) for bounded-memory pipelines.
  • Grid-first workflows. The standard LiDAR scaling strategy is rasterize early: classify and grid once, then do all downstream analysis (volumes, differencing) on grids, which are orders of magnitude smaller than the source cloud.
  • Duck-typed points mean zero-copy numpy use. Pass numpy structured arrays or memory-mapped point buffers straight in; only as_point copies, and only where you ask for it.

Limitations and roadmap

  • classify_ground_grid is a first-order filter: it assumes terrain is locally smooth within cell_size. Steep terrain, dense low vegetation, and bridges over ground will misclassify. Progressive morphological or surface-based filters are planned.
  • interpolate_idw is brute force; a spatial index (KD-tree) will make it scale to dense clouds.
  • Planned: TIN construction and TIN-based volumes, normal estimation, intensity normalization, height-above-ground normalization, and laspy/PDAL I/O adapters (outside the engine).

Testing

python -m unittest discover -s tests

32 tests covering the point model, thinning/chunking, ground classification on a synthetic scene, DEM gridding (min/max/IDW against hand-computed values), and volumes. All pass.

Project structure

survey-pointcloud/
├── src/pointcloud/
│   ├── __init__.py     # public API
│   ├── point.py        # Point model, ASPRS codes, duck-typed accessors
│   ├── cloud.py        # PointCloud, voxel_thin, iter_chunks, stats
│   ├── classify.py     # grid-minimum ground classification
│   ├── dem.py          # grid_minimum / grid_maximum / interpolate_idw
│   └── volumes.py      # volume_grid / volume_between (cut/fill)
├── tests/test_pointcloud.py
├── pyproject.toml
├── CHANGELOG.md
└── LICENSE

License

MIT — see LICENSE.

About

Point-cloud processing engine: ground classification, DEM gridding, IDW, volumes

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages