Merging Multiple Readers in One Pipeline

TL;DR: List several readers at the start of the pipeline and follow them with filters.merge so the next stage sees one combined view. If inputs are in different CRSs, give each reader its own filters.reprojection branch (via tag and inputs) to a common out_srs before the merge. Expect the merged view to hold every point at once — size the machine for the sum of the inputs.

# Context and Motivation

This guide is part of PDAL Stage Chaining. Plenty of operations need points from more than one file at once: rasterizing a DTM across a tile boundary, classifying ground with buffer data from neighbouring tiles, combining an airborne flight with a later drone infill, or joining flightline files into one tile. PDAL handles this inside a single pipeline by letting several readers feed one downstream stage. When you understand how views merge, the pattern is simple; when you do not, it produces duplicated points, mismatched coordinates and out-of-memory crashes.

Many readers, one view Three readers on the left, one per tile, each producing its own point view. The views flow into filters.merge, which produces a single view containing all points. The merged view passes to SMRF and then to a writer. A note says memory now holds the sum of all three tiles. readers.las t_0431 readers.las t_0432 readers.las t_0441 filters.merge filters.smrf writers.las memory now holds all three tiles at once

# Prerequisites and Assumptions

  • PDAL 2.x; filters.merge and multi-reader pipelines are long-standing features.
  • Inputs that belong together spatially — adjacent tiles, overlapping flightlines, or a base survey and its infill.
  • Knowledge of each input’s CRS and point format (pdal info --summary for each).

# Step-by-Step Implementation

# Step 1 — List the readers

Several readers at the head of a linear pipeline are all inputs to the next stage. Adding explicit tags makes the graph readable.

# Step 2 — Merge explicitly

Without filters.merge, most filters process each input view separately. A neighbourhood filter such as SMRF then never sees across the seam — which defeats the purpose. filters.merge produces one view so the next stage sees all points together.

# Step 3 — Align CRSs before merging

If inputs differ in CRS, give each its own filters.reprojection to a common out_srs using inputs, then merge the reprojected tags.

# Step 4 — Reconcile schemas

Merged views take the union of dimensions. A dimension present in one input and absent in another is zero-filled for points from the latter. Decide whether that is acceptable before writing.

# Step 5 — Crop back if needed

For buffered processing, crop the merged, processed cloud back to the central tile before writing so neighbours are not duplicated in output.

# Complete Working Example

Merging a base airborne tile in UTM with a drone infill delivered in geographic coordinates, classifying ground across both, and writing only the central extent:

json
{
  "pipeline": [
    { "type": "readers.las", "filename": "base/t_0431.laz", "tag": "base" },
    { "type": "readers.las", "filename": "drone/infill_0431.laz", "tag": "drone_raw" },
    { "type": "filters.reprojection", "inputs": ["drone_raw"],
      "in_srs": "EPSG:6318+5703", "out_srs": "EPSG:6347+5703", "tag": "drone" },
    { "type": "filters.merge", "inputs": ["base", "drone"], "tag": "merged" },
    { "type": "filters.range", "limits": "Classification![7:7],Classification![18:18]" },
    { "type": "filters.smrf", "slope": 0.15, "window": 18, "threshold": 0.5 },
    { "type": "filters.crop", "bounds": "([431000, 432000], [4471000, 4472000])" },
    { "type": "writers.las", "filename": "out/t_0431_merged.laz",
      "minor_version": 4, "dataformat_id": 6, "a_srs": "EPSG:6347+5703",
      "scale_x": 0.01, "scale_y": 0.01, "scale_z": 0.01,
      "offset_x": "auto", "offset_y": "auto", "offset_z": "auto" }
  ]
}

A Python wrapper that builds the reader list from a tile index and checks point accounting:

python
"""Merge a tile with its neighbours, process, and crop back to the tile."""
from __future__ import annotations

import json
from pathlib import Path

import pdal


def merged_pipeline(center: Path, neighbours: list[Path], bounds: str, dst: Path) -> dict:
    readers = [{"type": "readers.las", "filename": str(p), "tag": f"in{i}"}
               for i, p in enumerate([center, *neighbours])]
    return {"pipeline": [
        *readers,
        {"type": "filters.merge", "inputs": [r["tag"] for r in readers]},
        {"type": "filters.range", "limits": "Classification![7:7],Classification![18:18]"},
        {"type": "filters.smrf", "slope": 0.15, "window": 18, "threshold": 0.5},
        {"type": "filters.crop", "bounds": bounds},
        {"type": "writers.las", "filename": str(dst), "minor_version": 4, "dataformat_id": 6,
         "forward": "all"},
    ]}


if __name__ == "__main__":
    spec = merged_pipeline(Path("tiles/t_0431.laz"),
                           [Path("tiles/t_0430.laz"), Path("tiles/t_0432.laz")],
                           "([431000, 432000], [4471000, 4472000])",
                           Path("out/t_0431_ground.laz"))
    p = pdal.Pipeline(json.dumps(spec))
    n = p.execute()
    print(f"{n} points written for the central tile")
Why the explicit merge matters Left: two tiles processed as separate views by SMRF; each tile's ground surface is estimated without the other, producing a step at the shared edge. Right: after filters.merge the two tiles are processed as one view and the ground surface crosses the seam smoothly. separate views merged view step at the seam continuous across the tile edge each tile filtered alone

# Key Parameter Table

Element Setting Why
reader tag in0, in1, … Makes inputs lists explicit and readable
filters.merge inputs = all readers One view for neighbourhood stages
per-input filters.reprojection common out_srs Merge only data in one CRS
filters.crop central bounds Write only the tile you are responsible for
writer forward all Carries header settings from the first input
writer offset_* auto Recomputes offsets for the merged extent

# Verification

  • No duplicates. After cropping, the output count should be close to the central tile’s own count (plus any infill). A count near the sum of all inputs means the crop is missing.
  • Seam continuity. Rasterize a DTM of the output and a neighbour’s output, then check the difference along the shared edge; a step indicates the neighbours were not merged before SMRF.
  • Schema. pdal info --schema on the output lists dimensions from every input; check whether zero-filled dimensions (for example Red, Green, Blue from a colourized drone input) should be dropped.

# Gotchas and Edge Cases

Implicit behaviour differs by stage. Some stages handle multiple input views by processing each separately; writers write them all. Relying on implicit merging makes results depend on stage internals. Always merge explicitly before neighbourhood filters.

Different point formats. Merging PDRF 1 and PDRF 7 inputs yields a view with RGB for all points, zero for those that never had it. Writing PDRF 6 drops RGB; writing PDRF 7 keeps zeros that look like black. Choose the output format deliberately.

Different scales and offsets. PDAL holds coordinates as doubles internally, so merging inputs with different LAS scales is safe; the writer’s scale and offset decide the output precision. Set them explicitly rather than inheriting from whichever file came first.

Merged views cost the sum Horizontal bars of peak memory. One tile alone needs about 3 gigabytes. The tile with its two side neighbours merged needs about 9. The tile with all eight neighbours merged needs about 27, which exceeds a 16 gigabyte worker, shown as a dashed limit line. 1 tile 3 GB tile + 2 neighbours 9 GB tile + 8 neighbours 27 GB 16 GB worker

Memory. A merged view holds all inputs at once. Buffer with a strip of neighbour points rather than whole neighbour tiles — crop each neighbour reader to a 30–50 m buffer before merging — as described in buffered tiling to avoid edge artefacts.

# Frequently Asked Questions

Do I need filters.merge if I list several readers?

For writers, no — they write all incoming views. For neighbourhood filters such as SMRF, outlier removal or HAG, yes: without an explicit merge they may process each input view separately and never see across tile edges.

Can I merge files in different coordinate systems?

Only after reprojecting them to a common CRS. Give each input its own reprojection stage using tags and inputs, then merge the reprojected outputs.

What happens to dimensions that exist in only one input?

The merged view contains the union of dimensions, and points from inputs without a dimension get zero for it. Decide whether to drop such dimensions or choose an output format that makes the zeros harmless.

How do I avoid writing neighbour points into every tile?

Crop the processed merged view back to the central tile’s bounds before the writer. Each tile then contains only its own points, processed with the benefit of neighbour context.