laspy vs PDAL: When to Use Which
TL;DR: Use laspy when the job is reading, editing or writing LAS/LAZ fields in NumPy — headers, attributes, arrays from other tools — and you want a light pip install. Use PDAL when the job involves spatial algorithms (ground filtering, outliers, height above ground, rasterization), reprojection, many formats, streaming through multi-stage pipelines, or reproducible JSON pipelines in batch. For most production work, use both: PDAL for the heavy stages, laspy or NumPy for the custom steps in between.
# Context and Motivation
This guide is part of laspy and NumPy Workflows for LAS Data. Teams starting with Python LiDAR processing often ask which library to standardize on, and the question is framed as a choice when it is really a division of labour. The two tools have different centres of gravity. laspy is a file-format library: it knows everything about LAS and LAZ and nothing about algorithms. PDAL is a processing framework: it knows dozens of formats and a large catalogue of filters, and exposes them through pipelines. Picking the right one per task — and knowing how to pass data between them — saves both effort and memory.
# Prerequisites and Assumptions
- A task defined well enough to know whether it needs spatial neighbourhoods, reprojection or rasters.
- An environment policy: pip-only environments favour laspy; conda or containers make PDAL easy.
- Awareness that both read LAZ; PDAL additionally reads COPC, EPT, E57, PLY, text and many more formats.
# Step-by-Step Implementation
# Step 1 — Classify the task
Is it file-level (headers, fields, format conversion), point-wise (arithmetic on attributes), or spatial (neighbours, surfaces, rasters)?
# Step 2 — Check the environment constraints
Can you install PDAL with its GDAL and PROJ dependencies here? In a Lambda function or a minimal container, laspy may be the only realistic option.
# Step 3 — Consider data size and memory
laspy reads whole files or chunks you manage yourself; PDAL can stream through a multi-stage pipeline with bounded memory.
# Step 4 — Consider reproducibility and review
PDAL pipelines are JSON documents a reviewer can read and CI can validate; laspy logic lives in Python code.
# Step 5 — Combine at the array boundary
Where a task needs both, pass NumPy arrays between them rather than writing intermediate files.
# Complete Working Example
A workflow that uses each tool for what it does best: PDAL computes ground and height above ground, NumPy applies a custom per-point rule, and laspy writes the result with a new extra dimension.
"""PDAL for spatial stages, NumPy for a custom rule, laspy for the final file."""
from __future__ import annotations
import json
import laspy
import numpy as np
import pdal
from pyproj import CRS
SRC = "tiles/t_0431.laz"
# 1. PDAL: noise removal, ground classification, height above ground (spatial work).
p = pdal.Pipeline(json.dumps({"pipeline": [
SRC,
{"type": "filters.range", "limits": "Classification![7:7],Classification![18:18]"},
{"type": "filters.smrf", "slope": 0.15, "window": 18, "threshold": 0.5},
{"type": "filters.hag_nn", "count": 2},
]}))
p.execute()
a = p.arrays[0]
crs_wkt = p.metadata["metadata"]["readers.las"]["srs"]["compoundwkt"]
# 2. NumPy: a project-specific rule PDAL has no stage for.
risk = np.clip((a["HeightAboveGround"] - 10.0) / 20.0, 0.0, 1.0).astype(np.float32)
risk[a["Classification"] == 6] = 0.0 # buildings are not fall-risk trees
# 3. laspy: write a LAS 1.4 file with the new dimension.
header = laspy.LasHeader(point_format=6, version="1.4")
header.scales = np.array([0.01, 0.01, 0.01])
header.offsets = np.floor([a["X"].min(), a["Y"].min(), a["Z"].min()])
header.add_extra_dim(laspy.ExtraBytesParams(name="fall_risk", type=np.float32))
header.add_crs(CRS.from_wkt(crs_wkt))
las = laspy.LasData(header)
las.x, las.y, las.z = a["X"], a["Y"], a["Z"]
las.intensity = a["Intensity"]
las.classification = a["Classification"]
las.return_number, las.number_of_returns = a["ReturnNumber"], a["NumberOfReturns"]
las.gps_time = a["GpsTime"]
las.fall_risk = risk
las.write("out/t_0431_fall_risk.laz")
print(f"{len(las.points):,} points written; {np.mean(risk > 0.5):.1%} high risk")The last step could equally be a PDAL writers.las fed with the modified array; laspy is used here to show the hand-off in both directions. The metadata key path for the CRS can differ slightly between PDAL versions, so read it defensively in production code.
# Key Parameter Table
| Task | laspy | PDAL | Prefer |
|---|---|---|---|
| Read header, VLRs | direct | pdal info --metadata |
laspy |
| Change a field for all points | NumPy assignment | filters.assign |
either |
| Ground classification | — | filters.smrf, filters.pmf, filters.csf |
PDAL |
| Height above ground | — | filters.hag_nn |
PDAL |
| Reprojection | via pyproj on arrays | filters.reprojection |
PDAL |
| Rasterize to GeoTIFF | — | writers.gdal |
PDAL |
| COPC spatial query | CopcReader.query |
readers.copc |
either |
| Minimal serverless install | pip only | heavy | laspy |
| Reviewable, versioned pipelines | code | JSON | PDAL |
| Large files, many stages | manual chunking | streaming engine | PDAL |
# Verification
When a workflow switches tools mid-stream, verify at each boundary:
- Counts. The array length leaving PDAL equals the number of points laspy writes.
- Coordinates. Round-trip error at most half the output scale.
- CRS. The output file’s CRS matches the input’s; laspy does not carry it automatically from PDAL arrays.
# Gotchas and Edge Cases
Field naming differs. PDAL uses Classification, ReturnNumber, GpsTime; laspy uses classification, return_number, gps_time. Map names explicitly at the boundary.
CRS does not travel with arrays. A NumPy array has no spatial reference. Carry it separately, as the example does from PDAL’s metadata, and set it on the laspy header or PDAL writer.
Dependency conflicts. A pip-installed pyproj next to a conda-installed PDAL can load two different PROJ libraries. Install both from one channel in a single environment.
Reimplementing PDAL in NumPy. Writing your own ground filter or rasterizer in NumPy is tempting and nearly always slower and less robust than the equivalent PDAL stage. Reserve custom code for rules PDAL does not have.
# Frequently Asked Questions
Is laspy or PDAL better for Python LiDAR processing?
Neither is better in general. laspy excels at reading, editing and writing LAS and LAZ data as NumPy arrays with a light install. PDAL excels at spatial processing, reprojection, rasterization and multi-format, multi-stage pipelines. Most production workflows use both.
Can PDAL and laspy read the same files?
Yes. Both read LAS and LAZ, and both can query COPC. PDAL also reads many other point cloud formats.
Which is faster?
For reading and writing LAS or LAZ, they are broadly comparable, with laspy’s parallel LAZ backend competitive. For spatial algorithms, PDAL is far faster because laspy has none and NumPy reimplementations are usually slower.
How do I pass data from PDAL to laspy?
Execute the PDAL pipeline and take pipeline.arrays, then assign the fields to a laspy LasData object, mapping PDAL’s CamelCase names to laspy’s snake_case names and setting the CRS explicitly.
# Related
- laspy and NumPy Workflows for LAS Data — laspy in depth
- PDAL Pipeline Architecture and Execution — PDAL in depth
- Passing NumPy Arrays into a PDAL Pipeline — laspy or NumPy data into PDAL
- Writing a LAS File from NumPy Arrays — the laspy writing step
- Reading COPC in Python with laspy — the cloud-native overlap