REPROJECT_LAS
Reprojects a LiDAR point cloud from one coordinate reference system (CRS) to another — or, when the source dataset's own embedded CRS metadata turns out to be invalid, assigns the target CRS to the file without transforming any coordinates. The workflow retrieves PROJJSON projection definitions from epsg.io and builds a PDAL pipeline using filters.reprojection (for a true reprojection) or writers.copc's a_srs alone (for a CRS assignment). Both simple horizontal-only and compound horizontal+vertical CRS are supported on either path.
The resulting dataset is always written as a COPC (Cloud Optimized Point Cloud) file with appropriate scaling and offsets and returned as a LAS-compatible artifact.
Typical use: normalizing datasets into a common coordinate system before further processing, tiling, or visualization — and, as a safety net, correcting datasets whose embedded CRS metadata is missing or corrupt even when no coordinate transform was actually requested. Usually the first step in any pipeline that works with data in a non-standard or unverified CRS.
Contract
| Type | REPROJECT_LAS |
| Accepts | input_las: las |
| Produces | output_las: las |
| Params | source_epsg (string), target_epsg (string) |
Inputs
| Slot | Type | Description |
|---|---|---|
input_las | las | Source point cloud dataset |
Outputs
| Slot | Type | Description |
|---|---|---|
output_las | las | Reprojected (or CRS-corrected) point cloud dataset |
Parameters
| Name | Type | Description |
|---|---|---|
source_epsg | string | CRS of the input dataset (e.g. "EPSG:2271" or "EPSG:26915+EPSG:5703") |
target_epsg | string | Target CRS to transform into, or to assign (e.g. "EPSG:3857") |
Both parameters accept standard EPSG codes, ESRI codes, and compound CRS strings (horizontal+vertical). Both must be present at job submission (param_keys) even when no reprojection is intended — see Passthrough behavior below, since an empty/equal pair no longer guarantees a zero-cost no-op the way it used to.
Passthrough behavior
Passthrough is no longer a pure parameter comparison. The executor always downloads the input file and inspects its embedded CRS metadata before deciding whether a transform is actually needed:
- Downloads
input_lasand runspdal info --metadataon it. - Runs
projinfoagainst the file's embedded spatial reference. A parse failure is captured rather than aborting the step. - Fetches PROJJSON definitions for every EPSG/ESRI code in
source_epsgandtarget_epsg(horizontal, plus vertical if compound) fromhttps://epsg.io/{code}.json. - Evaluates whether the embedded CRS can be trusted (
shouldAssign):shouldAssign = trueif the embedded SRS claims to be geographic but the file's max X coordinate is greater than 180 — the data clearly isn't in lat/lon degrees despite being tagged as such.shouldAssign = trueifprojinfocouldn't parse the embedded SRS at all (missing or malformed CRS metadata).
- Decides whether to proceed:
- Skip — only when the embedded CRS is trustworthy (
shouldAssign = false) andsource_epsgequalstarget_epsg, or either is empty. The original artifact (same URI, hash, and metadata) is returned unchanged asoutput_las; no PDAL pipeline runs and nothing is re-uploaded. - Proceed — when the embedded CRS is untrustworthy (
shouldAssign = true), or a real reprojection is requested (source_epsgandtarget_epsgare both present and differ).
- Skip — only when the embedded CRS is trustworthy (
Because the download, metadata extraction, and epsg.io lookups now happen unconditionally before this decision, REPROJECT_LAS is no longer a zero-I/O no-op even in the pure-skip case — only the transform/assignment PDAL run and re-upload are skipped. Pipelines can still include it unconditionally, but "equal or empty EPSG params" no longer means "free."
What it does internally
1. Download and inspect
Downloads input_las from MinIO and runs pdal info --metadata to capture its embedded CRS and bounding box.
2. Validate the embedded CRS
Runs projinfo against the embedded spatial reference, and separately checks whether a declared-geographic CRS is consistent with the file's actual coordinate values (max X > 180 is a red flag). Either failure marks the embedded CRS as untrustworthy.
3. Fetch CRS definitions
source_epsg and target_epsg are split on + to separate horizontal and vertical components, supporting compound CRS such as EPSG:26915+EPSG:5703. Each component's PROJJSON definition is fetched from https://epsg.io/{code}.json.
4. Decide: skip, assign, or reproject
- Embedded CRS trustworthy and
source_epsg/target_epsgequal or empty → skip; return the input unchanged (see Passthrough behavior). - Otherwise, if
source_epsgequalstarget_epsg→ assignment: the embedded CRS was untrustworthy, sotarget_epsgis stamped onto the file as-is, with no coordinate transform. - Otherwise (
source_epsgdiffers fromtarget_epsg) → reprojection: a full coordinate transform fromsource_epsgtotarget_epsg.
5. Build the PDAL pipeline
Four pipeline variants are used depending on the branch chosen and whether the relevant CRS is simple or compound:
| Branch | Compound? | Pipeline |
|---|---|---|
| Assignment | No | LAS → writers.copc (a_srs = target horizontal PROJJSON) |
| Assignment | Yes | LAS → writers.copc (a_srs = target CompoundCRS PROJJSON) |
| Reprojection | No | LAS → filters.reprojection → writers.copc |
| Reprojection | Yes | LAS → filters.reprojection (CompoundCRS in/out) → writers.copc (a_srs = target CompoundCRS PROJJSON) |
The compound variant is only used when the relevant EPSG string(s) contain +; for reprojection this requires both source_epsg and target_epsg to be compound, otherwise only the horizontal component of each is used. Assignment pipelines never include filters.reprojection — coordinates are left untouched, only the CRS tag changes. Every variant writes via writers.copc with scale_x/y/z = 0.001 and offset = auto.
6. Run and validate
The pipeline JSON is written to disk and executed with pdal pipeline. Any output on stderr fails the step — the job is stopped with a status: failed error containing the captured output.
7. Upload and return
On success, the output file is uploaded to MinIO, pdal info is run on it to capture fresh metadata, the file is hashed (md5sum), and the new artifact (uri, hash, metadata) is returned as output_las.
Recipe usage
json
{
"id": "reproject",
"type": "REPROJECT_LAS",
"inputs": { "input_las": "job:input_las" },
"outputs": { "output_las": "step:reproject.output_las" },
"param_keys": ["source_epsg", "target_epsg"]
}Job params:
json
"params": {
"reproject": {
"source_epsg": "EPSG:2271",
"target_epsg": "EPSG:3857"
}
}Artifact storage path
artifacts/job_{id}/reproject/output.las