copc_converter
A fast, memory-efficient converter that turns LAS/LAZ point cloud files into COPC (Cloud-Optimized Point Cloud) files.
Features
- Produces spec-compliant COPC 1.0 files (LAS 1.4, point format 6, 7, or 8 — automatically chosen from input)
- Merges multiple input files into a single COPC output
- Out-of-core processing with a configurable memory budget — handles datasets larger than RAM
- Parallel reading, octree construction, and LAZ compression via rayon
- Preserves WKT CRS from input files
- Optional temporal index for GPS-time-based filtering (spec)
Installation
Requires Rust 1.85+.
From crates.io
From source
This installs the copc_converter binary to ~/.cargo/bin/, which should be on your PATH.
Pre-built binaries
Download pre-built binaries from the GitHub releases page. These are built for broad compatibility and run on any machine.
For best performance, prefer installing from source via cargo install — this automatically compiles with target-cpu=native, optimizing for your specific CPU's instruction set (AVX2, NEON, etc.).
Usage
# Single file
# Directory of LAZ/LAS files
Options
| Flag | Description | Default |
|---|---|---|
--memory-limit |
Max memory budget (16G, 4096M, etc.) |
auto-detected |
--threads |
Max parallel threads | all cores |
--temp-dir |
Directory for intermediate files | system temp |
--temporal-index |
Write a temporal index EVLR for time-based queries | off |
--temporal-stride |
Sampling stride for the temporal index (every n-th point) | 1000 |
--progress |
Progress output format: bar, plain, or json |
bar |
--temp-compression |
Compress scratch temp files: none or lz4 |
none |
--node-storage |
Per-node temp layout: files or packed |
files |
Temp file compression and node storage layout
Large conversions create a lot of scratch data. Two independent knobs shape the temp directory's footprint:
--temp-compressioncontrols the on-disk encoding of each batch ofRawPointrecords.none(default) writes raw bytes;lz4wraps each batch in a self-contained LZ4 frame. LZ4 compresses at >1 GB/s per core so CPU cost is modest, and on network filesystems (EFS/NFS) it often reduces wall time because the bottleneck shifts from I/O to compute.--node-storagecontrols the filesystem layout of per-node point data during build.files(default) writes a separate file per octree node; on very large inputs node counts reach the hundred-thousands, which can exhaust inode budgets on shared scratch filesystems.packedwrites all node data into a handful of append-only pack files (one per worker thread) with an in-memory key→offset index, independent of node count.
Both flags can be combined freely.
Measured on a 168M-point / 701 MB LAZ input, 32 GB budget:
--node-storage |
--temp-compression |
wall | peak inodes | peak temp bytes | output |
|---|---|---|---|---|---|
| files | none | 61.7s | 6 716 | 12 161 MB | 1028 MB |
| files | lz4 | 71.3s | 6 716 | 5 848 MB | 1028 MB |
| packed | none | 66.7s | 76 | 11 894 MB | 1028 MB |
| packed | lz4 | 73.8s | 76 | 5 849 MB | 1028 MB |
LZ4 cuts peak temp bytes by ~52% regardless of storage mode; packed cuts peak inodes by ~99% regardless of compression. Output is byte-identical across all four combinations (within hash-order noise of a few KB). Dead space from pack-file overwrites was not observable on this workload.
Use packed when the scratch filesystem has an inode limit, lz4 when
it is space-constrained, and both together for the most disk-friendly
run on a modest wall-time budget.
Examples
# With temporal index (useful for multi-pass mobile mapping data)
Library usage
The crate exposes a typestate pipeline API that enforces correct step ordering at compile time:
use ;
let files = collect_input_files?;
let config = PipelineConfig ;
scan?
.validate?
.distribute?
.build?
.write?;
Tools
Optional analysis tools are available behind the tools feature:
inspect_copc
Inspect a COPC file's structure, or compare two files side-by-side. Works with local files and HTTP URLs.
# Inspect a single file
# Compare two files
Prints node counts, point distribution, compressed sizes, and compression ratios per octree level. When the file has a temporal index EVLR, also prints GPS time range, per-level temporal coverage, a time histogram, and sample density stats.
preview_chunking
Preview how an input LAS/LAZ dataset would be partitioned during conversion, without actually writing anything:
Prints chunk count, target size, grid resolution, and per-chunk size distribution. Useful for tuning --memory-limit before running a long conversion.
How it works
- Scan — reads headers from all input files in parallel to determine bounds, CRS, point format, and point count.
- Validate — checks that all input files share the same CRS and point format, and selects the appropriate COPC output format (6, 7, or 8).
- Count — first full pass over the input: populates an occupancy grid used by the chunk planner to carve the dataset into thousands of roughly equal-sized chunks via counting sort.
- Distribute — second full pass over the input: streams every point into its chunk's scratch file on disk, bounded by the configured memory budget.
- Build — each chunk's sub-octree is built independently in memory in parallel, then merged at coarse levels up to a single global root, thinning points at each level to produce multi-resolution LODs.
- Write — encodes and compresses nodes in parallel into a single COPC file with a hierarchy EVLR for spatial indexing.
Acknowledgments
The chunked octree build is based on the counting-sort approach described in:
Markus Schütz, Stefan Ohrhallinger, and Michael Wimmer. "Fast Out-of-Core Octree Generation for Massive Point Clouds." Computer Graphics Forum, 2020. doi:10.1111/cgf.14134
License
MIT