Multi-platform point cloud analysis — registration, segmentation, meshing, and feature extraction
Project description
occulus
Multi-platform point cloud analysis — registration, segmentation, surface reconstruction, and feature extraction for aerial, terrestrial, and UAV LiDAR.
Point cloud tooling in Python is fragmented. laspy reads files. Open3D visualizes them. PDAL pipelines transform them. But nothing treats acquisition platform as a first-class concept — and platform changes everything about how you process the data. Aerial LiDAR at 8 pts/m² from a nadir perspective requires fundamentally different ground classification parameters than a terrestrial scan at 50,000 pts/m² from a tripod. UAV photogrammetric point clouds have different noise characteristics than UAV LiDAR. Today, you adjust every parameter manually, every time.
occulus is the point cloud analysis library that knows where your data came from and adapts accordingly. One API. Platform-aware defaults. C++ performance where it matters. NumPy arrays in, NumPy arrays out.
Why occulus?
The problem
You have a terrestrial laser scan of a bridge abutment and an aerial LiDAR survey of the surrounding floodplain. Both are point clouds. Both are stored as LAS files. But they need completely different processing:
- The aerial data needs CSF ground classification at 2m cloth resolution, tree segmentation, and a canopy height model.
- The terrestrial data needs multi-scan registration (ICP), plane detection for the concrete surfaces, and a Poisson mesh for the structural assessment.
In today's Python ecosystem, you'd use laspy to read both, then branch into completely different code paths — Open3D for the TLS registration, some custom NumPy code for the aerial metrics, maybe PDAL for the ground classification. No library understands that these are different kinds of point clouds with different processing needs.
The solution
import occulus
# Read with platform awareness — returns the right subtype
aerial = occulus.read("survey.laz", platform="aerial")
scan = occulus.read("bridge_scan.laz", platform="terrestrial")
# Same function, platform-aware defaults
# Aerial: CSF cloth resolution 2.0m
# Terrestrial: CSF cloth resolution 0.5m (warns about perspective limitations)
aerial_ground = occulus.segmentation.classify_ground(aerial)
scan_ground = occulus.segmentation.classify_ground(scan)
# Platform-specific operations
trees = occulus.segmentation.segment_trees(aerial) # aerial/UAV workflow
planes = occulus.features.detect_planes(scan) # TLS/survey workflow
# Universal operations work on any platform
normals = occulus.normals.estimate_normals(scan)
mesh = occulus.mesh.poisson_reconstruct(scan)
The API doesn't force you to think about platform differences — it handles them. But when you need to override the defaults, every parameter is exposed.
Features
Platform-aware type system
from occulus.types import PointCloud, AerialCloud, TerrestrialCloud, UAVCloud
# Base type works for any data
cloud = PointCloud(xyz_array)
# Platform subtypes carry acquisition metadata and provide smart defaults
aerial = AerialCloud(xyz, intensity=intensity, return_number=returns)
tls = TerrestrialCloud(xyz, scan_positions=[pos1, pos2, pos3])
uav = UAVCloud(xyz, is_photogrammetric=True)
Each subtype knows its acquisition context:
| Platform | Typical density | Perspective | Key metadata | Default ground params |
|---|---|---|---|---|
AerialCloud |
2–25 pts/m² | Nadir (overhead) | Return number, flight altitude | CSF 2.0m cloth |
TerrestrialCloud |
1,000–100,000 pts/m² | Horizontal | Scan positions, scan angles | CSF 0.5m cloth |
UAVCloud |
20–500 pts/m² | Oblique | Flight trajectory, SfM vs LiDAR flag | CSF 1.5m cloth |
Registration
Align multiple scans into a common coordinate system.
from occulus.registration import icp, coarse_align, align_scans
# ICP — auto-selects point-to-point or point-to-plane based on normals
result = icp(source, target, max_correspondence_distance=0.5)
print(result.transformation) # 4x4 rigid transform
print(result.fitness) # fraction of matched points
print(result.inlier_rmse) # alignment quality
# Coarse alignment first (FPFH features + RANSAC), then ICP refinement
coarse = coarse_align(source, target, voxel_size=0.5)
refined = icp(source, target, init_transform=coarse.transformation)
# Multi-scan alignment — aligns N scans to a common frame
# For TerrestrialCloud inputs, uses scan positions as initial guesses
aligned = align_scans([scan1, scan2, scan3, scan4])
Segmentation
Separate ground from non-ground, extract individual objects, delineate tree crowns.
from occulus.segmentation import classify_ground, segment_objects, segment_trees
# Ground classification — platform-aware defaults
ground = classify_ground(cloud) # algorithm and params selected by platform
print(f"{ground.n_ground} ground points, {ground.n_nonground} non-ground")
# Force a specific algorithm
ground = classify_ground(cloud, algorithm="pmf", max_distance=1.0)
# Object segmentation — DBSCAN, region growing, or connected components
objects = segment_objects(non_ground_cloud, method="dbscan", eps=0.5)
print(f"Found {objects.n_segments} objects")
# Tree segmentation — watershed or point-based methods
trees = segment_trees(aerial_cloud, method="watershed", min_height=2.0)
Surface reconstruction
Build meshes from point clouds for visualization, volume computation, and structural analysis.
from occulus.mesh import poisson_reconstruct, ball_pivot, alpha_shape
# Poisson — watertight, requires normals
cloud = occulus.normals.estimate_normals(cloud)
mesh = poisson_reconstruct(cloud, depth=10)
print(f"{mesh.n_vertices} vertices, {mesh.n_faces} faces")
# Ball Pivoting — handles boundaries and holes naturally
mesh = ball_pivot(cloud, radii=[0.05, 0.1, 0.2])
# Alpha shape — concavity-respecting boundary
mesh = alpha_shape(cloud, alpha=0.5)
# All mesh results have the same interface
mesh.to_open3d() # Open3D interop
Feature extraction
Detect geometric primitives — planes, cylinders, edges — from structured scenes.
from occulus.features import detect_planes, detect_cylinders, detect_edges
# RANSAC plane detection — iteratively finds the N largest planes
planes = detect_planes(cloud, distance_threshold=0.02, max_planes=5)
for p in planes:
print(f"Plane: {p.n_inliers} points, RMSE={p.rmse:.4f}m, normal={p.normal}")
# Cylinder detection — pipes, poles, tree trunks
cylinders = detect_cylinders(cloud, radius_range=(0.05, 0.5))
for c in cylinders:
print(f"Cylinder: r={c.radius:.3f}m, {c.n_inliers} points")
# Edge detection — boundaries, creases, corners
edges = detect_edges(cloud, k_neighbors=30, angle_threshold=60)
Filtering
Clean, downsample, and crop point clouds.
from occulus.filters import (
voxel_downsample, statistical_outlier, radius_outlier, crop, random_downsample
)
# Voxel grid downsampling
small = voxel_downsample(cloud, voxel_size=0.1)
# Statistical Outlier Removal (SOR)
clean, inlier_mask = statistical_outlier(cloud, k_neighbors=20, std_ratio=2.0)
# Radius outlier removal
clean, mask = radius_outlier(cloud, radius=0.5, min_neighbors=5)
# Spatial crop
roi = crop(cloud, bounds=(xmin, ymin, zmin, xmax, ymax, zmax))
# Random subsample
subset = random_downsample(cloud, fraction=0.1, seed=42)
Normal estimation
Compute and orient surface normals.
from occulus.normals import estimate_normals, orient_normals
# PCA-based normal estimation
cloud = estimate_normals(cloud, k_neighbors=30)
# Orient normals toward a known viewpoint (TLS scanner position)
cloud = orient_normals(cloud, viewpoint=scan_position.as_array())
# Or orient via minimum spanning tree (when viewpoint is unknown)
cloud = orient_normals(cloud, method="mst")
Metrics
Summary statistics, canopy height models, density analysis.
from occulus.metrics import compute_metrics, canopy_height_model, point_density_map
# Full summary statistics
stats = compute_metrics(cloud)
print(f"Points: {stats.n_points:,}")
print(f"Density: {stats.point_density:.1f} pts/m²")
print(f"Z range: {stats.z_min:.1f}–{stats.z_max:.1f}m")
print(f"Median elevation: {stats.z_percentiles[50]:.1f}m")
# Canopy Height Model — aerial/UAV workflow
chm, x_edges, y_edges = canopy_height_model(aerial_cloud, resolution=1.0)
# Point density map
density, x_edges, y_edges = point_density_map(cloud, resolution=2.0)
I/O
Read and write LAS, LAZ, PLY, PCD, and XYZ formats.
import occulus
# Read with platform hint — returns the right subtype
cloud = occulus.read("scan.laz", platform="terrestrial")
cloud = occulus.read("survey.las", platform="aerial")
cloud = occulus.read("model.ply") # platform="unknown" by default
# Subsample on read
cloud = occulus.read("huge_dataset.laz", subsample=0.1) # keep 10%
# Write
occulus.write(cloud, "output.laz") # compressed
occulus.write(cloud, "output.las") # uncompressed
occulus.write(cloud, "output.ply") # PLY format
occulus.write(cloud, "points.xyz") # delimited text
Visualization
Optional Open3D integration for interactive viewing.
from occulus.viz import show, show_registration, show_segmentation
# Quick view
show(cloud)
show(cloud1, cloud2) # multiple clouds
# Visualize registration result
show_registration(source, target, icp_result)
# Visualize segmentation with colored labels
show_segmentation(cloud, segmentation_result)
Architecture
C++ core, Python API
Performance-critical algorithms (ICP, k-d tree queries, RANSAC, CSF, normal estimation) are implemented in C++ and exposed via pybind11. The Python layer provides the API, type system, and orchestration.
occulus/
├── src/occulus/ ← Python API (types, dispatch, orchestration)
│ ├── types.py ← PointCloud, AerialCloud, TerrestrialCloud, UAVCloud
│ ├── io/ ← LAS/LAZ/PLY/PCD/XYZ readers and writers
│ ├── registration/ ← ICP, coarse alignment, multi-scan fusion
│ ├── segmentation/ ← Ground classification, object/tree segmentation
│ ├── mesh/ ← Poisson, BPA, alpha shape reconstruction
│ ├── features/ ← RANSAC plane/cylinder, edge detection
│ ├── metrics/ ← Statistics, CHM, density maps
│ ├── filters/ ← Voxel downsample, SOR, radius outlier, crop
│ ├── normals/ ← PCA normal estimation, orientation
│ ├── viz/ ← Open3D visualization helpers
│ └── _cpp/ ← pybind11 bindings to C++ core
└── cpp/ ← C++ source (k-d tree, ICP, RANSAC, CSF, meshing)
NumPy-native
Every function takes NumPy arrays and returns NumPy arrays. No proprietary object formats to learn. The PointCloud class is a thin wrapper around ndarray attributes — you can always access cloud.xyz as a raw (N, 3) float64 array.
Open3D interop
Convert between occulus and Open3D objects in both directions:
o3d_cloud = cloud.to_open3d() # occulus → Open3D
cloud = PointCloud.from_open3d(pcd) # Open3D → occulus
mesh.to_open3d() # mesh → Open3D TriangleMesh
Install
pip install occulus
Optional extras:
pip install occulus[las] # LAS/LAZ support via laspy
pip install occulus[viz] # Open3D visualization + matplotlib
pip install occulus[all] # Everything
Requirements
- Python 3.11+
- NumPy 1.24+
- A C++ compiler for building from source (prebuilt wheels provided for major platforms)
Companion: parallax (R)
occulus has a companion R package — parallax — providing the same capabilities with R-native idioms. Same concepts, same algorithm names, independent implementations optimized for each ecosystem.
| occulus (Python) | parallax (R) | |
|---|---|---|
| Core | C++ via pybind11 | Rust via extendr |
| Returns | NumPy arrays | S3/S7 classes, tibbles |
| Interop | Open3D, laspy | sf, terra, lidR |
| Registry | PyPI | CRAN |
Both packages share test fixtures, documentation structure, and API concepts. A workflow learned in one transfers directly to the other.
Real-World Output Gallery
All outputs generated from real USGS 3DEP, KY From Above, and OpenTopography LiDAR data — WCAG 2.1 AA compliant with alt-text metadata.
Coal Mine Terrain — Full Toolkit Demo (KY From Above)
Colorado Front Range — Rocky Mountains
Slope and Aspect Analysis — Eastern Kentucky
Kentucky Ground Classification (CSF)
KY From Above — Bluegrass Region Terrain Survey
Houston, Texas — Urban Density Analysis
Arizona — Sonoran Desert Basin and Range
CSF vs PMF Ground Classification Comparison
Oregon Pacific Coastline — Cliff, Beach, Forest
Louisiana — Mississippi River Delta Wetlands
Utah Canyonlands — Colorado Plateau
Use cases
Environmental monitoring
- Pre/post mining land disturbance analysis from aerial LiDAR
- Floodplain mapping from UAV surveys
- Canopy height and forest inventory from aerial/UAV platforms
- Stream channel morphology from terrestrial scanning
Surveying and engineering
- Multi-scan TLS registration for as-built documentation
- Bridge and structure inspection from terrestrial point clouds
- Volumetric computation from mesh reconstruction
- Plane detection for building facade analysis
Research
- Custom segmentation algorithm development with platform-aware test data
- Multi-epoch change detection workflows
- Cross-platform point cloud comparison (aerial vs. terrestrial vs. UAV)
- Reproducible processing pipelines with explicit parameter tracking
Development
git clone https://github.com/chrislyonsKY/occulus.git
cd occulus
pip install -e ".[dev]"
# Tests
pytest # unit tests (mocked I/O)
pytest -m integration # integration tests (real files)
# Quality
ruff check src/ tests/ # lint
ruff format src/ tests/ # format
mypy src/ # type check
Contributing
See CONTRIBUTING.md for development standards, git conventions, and the architecture overview.
AI-assisted development
This project uses structured AI development infrastructure. See ai-dev/ for architecture docs, agent configurations, decision records, and guardrails that guide both human and AI contributors.
License
GPL-3.0 — see LICENSE.
Author
Chris Lyons — GIS Developer, Kentucky Energy & Environment Cabinet
- GitHub: @chrislyonsKY
- Email: chris.lyons@ky.gov
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file occulus-1.0.0.tar.gz.
File metadata
- Download URL: occulus-1.0.0.tar.gz
- Upload date:
- Size: 59.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
16e8d1d417ec6d433d4f65fd2bc71c50b8a0f61efb764c296618f3a9ccb3d0a2
|
|
| MD5 |
2eba33edbd00a72726c73be123c7362d
|
|
| BLAKE2b-256 |
561c9745b1a62213051e6e75a88c701800f7a6cfca83d70fba1ec60a0c95b051
|
Provenance
The following attestation bundles were made for occulus-1.0.0.tar.gz:
Publisher:
publish.yml on chrislyonsKY/occulus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
occulus-1.0.0.tar.gz -
Subject digest:
16e8d1d417ec6d433d4f65fd2bc71c50b8a0f61efb764c296618f3a9ccb3d0a2 - Sigstore transparency entry: 1102727020
- Sigstore integration time:
-
Permalink:
chrislyonsKY/occulus@9db4ed97a027cf09814b4d19620ef6e5bde75f25 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/chrislyonsKY
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9db4ed97a027cf09814b4d19620ef6e5bde75f25 -
Trigger Event:
release
-
Statement type:
File details
Details for the file occulus-1.0.0-py3-none-any.whl.
File metadata
- Download URL: occulus-1.0.0-py3-none-any.whl
- Upload date:
- Size: 72.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f62f8665bf49a2fb2a09176c366b5f33cdecac79071417570c09e79f73ad74b
|
|
| MD5 |
f817a734e2ea2dbabc5a0987c5e62724
|
|
| BLAKE2b-256 |
ce681edb355a23d3728b116dc59978cc7ab05a2d91c97c9b17acf1ae6d27ae6f
|
Provenance
The following attestation bundles were made for occulus-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on chrislyonsKY/occulus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
occulus-1.0.0-py3-none-any.whl -
Subject digest:
5f62f8665bf49a2fb2a09176c366b5f33cdecac79071417570c09e79f73ad74b - Sigstore transparency entry: 1102727074
- Sigstore integration time:
-
Permalink:
chrislyonsKY/occulus@9db4ed97a027cf09814b4d19620ef6e5bde75f25 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/chrislyonsKY
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9db4ed97a027cf09814b4d19620ef6e5bde75f25 -
Trigger Event:
release
-
Statement type: