Skip to content

feat(lidar): PDAL-driven LiDAR → conditioned DEM pipeline (P34) #48

Description

@MAfarrag

Context

The USGS 3DEP workflow that produces a Hydro-Flattened DEM goes: raw LAS/LAZ point cloud → classification
(ground, water, buildings) → DEM gridding → hydroflattening (#36) → breakline burning (#38) → publish. Today,
vendors stitch this together with PDAL pipelines + custom Python. Phase 4 wraps PDAL and exposes the full
pipeline as a single declarative config.

Problem / Current Behaviour

Individual steps are available (grid_lidar_points, read_las/write_las, classify_ground,
detect_trees, clip, merge, filter_classes), but composing them into the canonical 3DEP pipeline still
requires custom orchestration. There is no declarative entry point that hands a lidar_to_dem(...) config to
the user and chains hydroflatten + breakline burn + stream burn.

Affected locations

File Symbol Notes
src/digitalrivers/lidar.py grid_lidar_points(...) shipped — point-cloud → DEM grid.
src/digitalrivers/lidar.py read_las, write_las, classify_ground, detect_trees, clip, merge, filter_classes, LasPoints shipped.
digitalrivers.point_cloud.lidar_to_dem(...) (top-level dispatcher) pending.
digitalrivers.point_cloud.LidarPipeline (declarative pipeline) pending.
digitalrivers lidar-to-dem CLI pending.

Motivation Example

# Shipped today.
from digitalrivers.lidar import read_las, classify_ground, filter_classes, grid_lidar_points

pc      = read_las("tile.laz")
classed = classify_ground(pc)
ground  = filter_classes(classed, [2])
dem     = grid_lidar_points(ground, cell_size_m=1.0, method="mean")

# Pending.
# from digitalrivers.point_cloud import lidar_to_dem, LidarPipeline
#
# dem = lidar_to_dem(
#     las_paths=["tile1.laz", "tile2.laz"],
#     cell_size=1.0,
#     classifications=[2],
#     method="mean",
# )
#
# pipeline = LidarPipeline(
#     las_paths=[...],
#     steps=[
#         {"type": "filters.range", "limits": "Classification[2:2]"},
#         {"type": "writers.gdal", "resolution": 1.0, "output_type": "mean"},
#         {"type": "digital-rivers.hydroflatten", "waterbodies_from": "Classification[9:9]"},
#         {"type": "digital-rivers.burn_streams", "streams": "streams.gpkg", "method": "fill_burn"},
#         {"type": "digital-rivers.raise_walls", "lines": "levees.gpkg", "height_attr": "crest_m"},
#     ],
# )
# dem = pipeline.execute()

Proposed Solution

from digitalrivers.point_cloud import lidar_to_dem, LidarPipeline

def lidar_to_dem(
    las_paths: list[str],
    *,
    cell_size: float,
    classifications: list[int] | None = None,
    method: "Literal['mean','idw','tin']" = "mean",
    hydroflatten_from_class: int | None = 9,
    streams: "gpd.GeoDataFrame | None" = None,
    walls: "gpd.GeoDataFrame | None" = None,
) -> "DEM": ...


class LidarPipeline:
    def __init__(self, las_paths: list[str], steps: list[dict]): ...
    def execute(self) -> "DEM": ...
    def to_json(self) -> str: ...
    @classmethod
    def from_json(cls, j: str) -> "LidarPipeline": ...

Built-in pipelines:

CLI: digitalrivers lidar-to-dem --config pipeline.json --out conditioned.tif.

References

  • PDAL: https://pdal.io/
  • USGS 3DEP Lidar Base Specification 2025.
  • ASPRS LAS 1.4 specification.
  • PDAL Python bindings — pdal.Pipeline, pdal.Reader.las(...), filters.smrf / filters.range /
    filters.dbscan, writers.gdal.

Out of Scope

  • LAS 1.5 / E57 readers — handled via PDAL when those formats land in PDAL itself.
  • GPU-accelerated point-cloud processing — Phase 5+.

Effort Estimate

Size: L (6 days)
Rationale: PDAL's Python API is mature; bulk is wiring the declarative pipeline + writing
digital-rivers.* extension stages + the CLI.

Definition of Done

  • grid_lidar_points(...) ships.
  • LAS read / write (read_las, write_las) ships.
  • Classification helpers (classify_ground, detect_trees, filter_classes) ship.
  • Geometry helpers (clip, merge) ship.
  • Pending: lidar_to_dem(...) top-level dispatcher.
  • Pending: LidarPipeline declarative pipeline runner + JSON round-trip.
  • Pending: On a USGS 3DEP sample tile, lidar_to_dem(...) produces a DEM within RMSE 0.5 m of the
    published 3DEP DEM.
  • Pending: Hydroflatten polygons derived from class-9 points match user-supplied polygons on a
    published reference tile within IoU >= 0.85.
  • Pending: CLI digitalrivers lidar-to-dem --config pipeline.json --out conditioned.tif.

Status: Partial on branch feat/phase-4. Shipped: point-cloud gridding + classification + IO. Pending:
declarative pipeline runner + CLI. See tests/test_lidar.py, tests/test_lidar_e2e.py.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphase-4Phase 4 — continental scale & maintenance

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions