Skip to content

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

TypeREPROJECT_LAS
Acceptsinput_las: las
Producesoutput_las: las
Paramssource_epsg (string), target_epsg (string)

Inputs

SlotTypeDescription
input_laslasSource point cloud dataset

Outputs

SlotTypeDescription
output_laslasReprojected (or CRS-corrected) point cloud dataset

Parameters

NameTypeDescription
source_epsgstringCRS of the input dataset (e.g. "EPSG:2271" or "EPSG:26915+EPSG:5703")
target_epsgstringTarget 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:

  1. Downloads input_las and runs pdal info --metadata on it.
  2. Runs projinfo against the file's embedded spatial reference. A parse failure is captured rather than aborting the step.
  3. Fetches PROJJSON definitions for every EPSG/ESRI code in source_epsg and target_epsg (horizontal, plus vertical if compound) from https://epsg.io/{code}.json.
  4. Evaluates whether the embedded CRS can be trusted (shouldAssign):
    • shouldAssign = true if 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 = true if projinfo couldn't parse the embedded SRS at all (missing or malformed CRS metadata).
  5. Decides whether to proceed:
    • Skip — only when the embedded CRS is trustworthy (shouldAssign = false) and source_epsg equals target_epsg, or either is empty. The original artifact (same URI, hash, and metadata) is returned unchanged as output_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_epsg and target_epsg are both present and differ).

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_epsg equal or empty → skip; return the input unchanged (see Passthrough behavior).
  • Otherwise, if source_epsg equals target_epsgassignment: the embedded CRS was untrustworthy, so target_epsg is stamped onto the file as-is, with no coordinate transform.
  • Otherwise (source_epsg differs from target_epsg) → reprojection: a full coordinate transform from source_epsg to target_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:

BranchCompound?Pipeline
AssignmentNoLAS → writers.copc (a_srs = target horizontal PROJJSON)
AssignmentYesLAS → writers.copc (a_srs = target CompoundCRS PROJJSON)
ReprojectionNoLAS → filters.reprojection → writers.copc
ReprojectionYesLAS → 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