feat: add transform_crs() to reproject point clouds#331
Open
agrigoriev wants to merge 6 commits into
Open
Conversation
608ae60 to
3fb4f25
Compare
Add a new pipeline stage transform_crs() that reprojects the point cloud from its current CRS to a target CRS (EPSG code or WKT). Unlike set_crs(), which only assigns a CRS without moving the coordinates, transform_crs() actually reprojects the data with PROJ/GDAL. - Reproject X/Y with a batched OGRCoordinateTransformation; Z is preserved (matches gdaltransform/sf/terra behaviour for 2D targets). - Adapt the scale factors and offsets to the target CRS to avoid precision loss or 32-bit integer overflow when units change (metres <-> degrees). - Propagate the target CRS to downstream stages and writers, and reproject the coverage/chunk extents so rasterization and tiled/parallel runs are sized in the target CRS. - Thread-safe under concurrent-files: each cloned stage builds its own coordinate transformation. - Wired through the C++ core, parser, api layer, R and Python bindings. - Add tests (tests/testthat/test-transform_crs.R) and documentation.
…nates - Handle non-INT32 X/Y storage (PCD float/double) without corrupting coordinates; write the coordinate directly for float/double, keep the scaled-integer path for LAS, and reject unsupported types. - set_chunk: scale the tile buffer to the target CRS units so a downstream halo isn't a source-unit buffer against a target-unit resolution; warn and continue instead of aborting the run when a chunk extent can't be reprojected. - Robust reference-offset selection (centre -> corners -> 0) so the stage no longer fails when the bbox centroid falls outside the transform domain. - Drop and warn on points outside the transform domain or beyond the int32 range instead of silently wrapping to a garbage location. - Reset the bounding box when all points are dropped (no inverted extent). - write_las: reproject the catalog bbox to the output CRS before sizing the merged COPC octree (z preserved); no-op when the CRS is unchanged. - Delete copy-assignment (owns a raw OGRCoordinateTransformation). - Docs: Z is preserved, not recomputed; vertical transforms out of scope. - Extract a shared reproject_bbox() helper in CRS. Adds tests for PCD float reprojection, buffer-unit scaling and the reproject -> COPC octree path.
…rrectly to LAS PCD (and other non-LAS) sources store X/Y as raw float/double. The in-memory schema is kept at identity scale/offset so every in-memory accessor (get_x, AttributeAccessor, the kd-tree) reads the reprojected coordinate directly. But write_las() quantizes to LAS int32 using the schema scale/offset, which for a float/double source is the default 1.0 -- collapsing sub-unit (e.g. lon/lat) coordinates to 0 on write. transform_crs now records the target-appropriate scale/offset on the header for every storage type (the schema is still updated only for INT32, to keep in-memory float/double decoding consistent). The reference offset is computed for all types. The LAS writer reads the scale/offset from the header for float/double coordinate axes, and from the schema for INT32 (preserving the existing LAS and transform_with paths). Adds a PCD -> reproject -> write_las round-trip test; the previous PCD test stopped at an in-memory callback and missed the writer path.
For a projected->projected transform, the previous code reused the source schema's X/Y scale factor. That is correct for an INT32 (LAS) source, where the schema scale is the real quantization step, but wrong for a float/double source (e.g. PCD) whose schema scale is a placeholder (typically 1.0). Since the LAS writer now reads the header scale/offset for float/double coordinate axes, that 1.0 reached the writer and quantized projected coordinates to whole units (~0.3 m error). Reuse the schema scale only when both X and Y are INT32 and the source is projected; otherwise choose a target-appropriate scale (1e-7 for a geographic target, 1 cm for a projected target). Adds a projected->projected PCD -> write_las round-trip test.
The merged-COPC octree-sizing path (write_las(experimental_writer=TRUE)) lives in the pre-devel COPC writer rework, which is not present on devel. This test was added alongside the writelas integration in the original PR; that integration was dropped when rebasing onto devel, so drop its test too. The full test (and implementation) remain on the pre-devel integration branch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new pipeline stage
transform_crs()that reprojects the point cloud from its current CRS to a target CRS (EPSG code or WKT string). This complementsset_crs(): whereset_crs()only assigns a CRS without touching the coordinates,transform_crs()actually reprojects the data with PROJ/GDAL.Behaviour
OGRCoordinateTransformation; Z is preserved as-is, matchinggdaltransform/sf/terrafor 2D targets (avoids surprising sub-metre datum-driven height shifts).Implementation
New stage
LASRtransformcrs(src/LASRstages/transformcrs.{cpp,h}), wired through the C++ core (parser factory + first-pass CRS propagation), theapilayer, the Rcpp module, the R wrapper (transform_crs()), and the Python bindings. AddsCRS::is_geographic().Tests
tests/testthat/test-transform_crs.R(7 tests) — verified output againstgdaltransformground truth to storage precision, Z preservation, EPSG + WKT inputs, distinction fromset_crs(), invalid-target errors, identity transform, and the reproject→rasterize extent path. Existing CRS/rasterize/transform_with/pipeline suites still pass.