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.
# Prerequisites and Assumptions
- PDAL 2.x;
filters.mergeand 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 --summaryfor 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:
{
"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:
"""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")# 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 --schemaon the output lists dimensions from every input; check whether zero-filled dimensions (for exampleRed,Green,Bluefrom 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.
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.
# Related
- PDAL Stage Chaining — views, stages and execution order
- Branching a PDAL Pipeline with Tags — one input, many outputs
- Buffered Tiling to Avoid Edge Artefacts — the reason most merges exist
- Merging Processed Tiles into One LAZ — merging as a final delivery step
- Diagnosing PDAL Out-of-Memory Failures — when merged views get too big