Reading a LAS/LAZ header, and what COPC adds

A real LAS header, byte by byte, and what the same cloud gains when it is rewritten as COPC.

LAS is a binary format with no field names in it: every value in the public header sits at a fixed byte offset counted from the start of the file, and a reader that does not know the offsets cannot find anything. LAZ is the same file with the points compressed by LASzip.

Two real clouds run through this page: a handheld GeoSLAM scan of a chapel on the Washington University campus, 72,244,026 points with no coordinate system in the file, and a crop of USGS 3DEP airborne lidar over Greenwood Cemetery in St. Louis, 947,352 points, published both as plain LAZ and as COPC.

The public header block

The first 112 bytes of the chapel scan as xxd prints them; the notes below give offsets in decimal.

00000000: 4c 41 53 46 00 00 00 00 00 00 00 00 00 00 00 00  LASF............
00000010: 00 00 00 00 00 00 00 00 01 02 6c 69 62 4c 41 53  ..........libLAS
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00 00 00 47 65 6f 53 4c 41  ..........GeoSLA
00000040: 4d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  M...............
00000050: 00 00 00 00 00 00 00 00 00 00 0f 01 e7 07 e3 00  ................
00000060: 47 01 00 00 01 00 00 00 81 1c 00 3a 5b 4e 04 00  G..........:[N..
0
4c 41 53 46 — the four characters LASF. Any other four bytes and the file is not LAS.
6
Global Encoding, a 16-bit field. Zero here. Bit 4 is the one that matters when you go looking for a coordinate system: set means the CRS is stored as WKT, clear means GeoTIFF keys.
24
01 02 — version 1.2, major then minor, and it governs how the rest of the header is laid out.
26, 58
System Identifier and Generating Software, 32 characters each, padded with nulls: libLAS and GeoSLAM. Free text, and the closest thing to provenance the header carries.
90, 92
0f 01 and e7 07 — day 271 of 2023. Every multi-byte integer in LAS is little-endian, low byte first.
94
e3 00 = 227, the size of the public header block and so the offset of the first VLR. LAS 1.4 files carry 375. Read the field rather than inferring it from the version.
96
47 01 00 00 = 327, where point records begin. Everything between the header and this offset is variable length records.
100
01 00 00 00 — one VLR.
104
81 = 129, the point data record format. Mask off the two high bits, which LAZ uses to mark compression, and the format is 1; taken at face value the byte names a format that does not exist.
105
1c 00 = 28 bytes per point record, the minimum size of format 1. A larger number means extra bytes are appended to every point.
107
3a 5b 4e 04 = 72,244,026 points, the legacy 32-bit count; in a LAS 1.4 file with point format 6 or above it is zero and the real count lives in a 64-bit field at offset 247.

The last 96 bytes of the header are twelve doubles, decoded here:

Offset Field Value
131, 139, 147 X, Y, Z scale factor 0.0001
155, 163, 171 X, Y, Z offset 0
179, 187 Max X, Min X 73.7761, −100.4331
195, 203 Max Y, Min Y 71.1349, −99.1124
211, 219 Max Z, Min Z 49.0239, −3.213

Scale, offset, and the 32-bit limit

X, Y and Z in a point record are signed 32-bit integers, not floating-point numbers. The header's scale and offset turn them into file coordinates: coordinate = (record value × scale) + offset, at the precision the scale claims — 0.0001 in the chapel scan, 0.001 in the Greenwood LAZ.

A signed 32-bit integer stops at 2,147,483,647, and the offset exists because of that ceiling. The northern edge of the Greenwood crop is at 4,286,002.909 m in UTM zone 15N; at a 0.001 m scale with no offset that is 4,286,002,909 counts, twice what the field holds. The published LAZ sets the Y offset to 4,285,496 m, so the point is stored as 506,909 counts.

Ask PDAL for a 0.001 m scale with no offset and it declines, naming the first point to overflow rather than the largest:

Unable to convert scaled value (4285603749) to int32 for dimension 'Y'

The published COPC keeps the offset at zero and coarsens the scale to 0.01 m, which fits and quantizes every coordinate to the nearest centimeter. The check is two-sided about the offset, and it runs per axis: every coordinate has to satisfy −2,147,483,648 ≤ (coordinate − offset) ÷ scale ≤ 2,147,483,647, so the axis minimum and the axis maximum both have to be tested. Dividing the extent by the scale says the data would fit somewhere, not that the offset puts it there.

Point data record formats

The format number at offset 104 decides which attributes each point carries, and therefore how wide it is.

File LAS Format Bytes Points
class_chapel.laz GeoSLAM handheld scan, campus chapel 1.2 1 28 72,244,026
rigid.las a second file from the same 2023 scans, written by PDAL 1.9.1 1.2 3 34 112,722,784
greenwood…2017.laz USGS 3DEP airborne lidar, cropped 1.4 6 30 947,352

Offsets 235 and 243 give the position and number of extended variable length records, which sit after the point data and carry an 8-byte length field. That is where the COPC hierarchy lives.

Where the coordinate reference system lives

The header has no field for it. The coordinate system is stored in a variable length record — an ordinary VLR before the points, or an extended one after them.

A VLR is a 54-byte header — two reserved bytes, a 16-character user ID, a 2-byte record ID, a 2-byte payload length, a 32-character description — and then its payload, which is what the Bytes column counts.

At User ID Record Bytes Description
375 LASF_Projection 2112 905 OGC Transformation Record
1334 liblas 2112 905 OGR variant of OpenGIS WKT SRS
2293 laszip encoded 22204 40 http://laszip.org

LASF_Projection record 2112 is the OGC WKT coordinate system record, and its 905 bytes name a compound CRS: EPSG:6344, NAD83(2011) / UTM zone 15N, plus EPSG:5703, NAVD88 height. Global Encoding reads 16, bit 4 set, so flag and record agree, as they must: LAS 1.4 makes a clear WKT bit an error for point formats 6 through 10, to be fixed at the source rather than read around.

The chapel scan has one VLR, the LASzip descriptor. The dump starts at byte 224, so the record begins with bb aa at 227:

000000e0: b4 09 c0 bb aa 6c 61 73 7a 69 70 20 65 6e 63 6f  .....laszip enco
000000f0: 64 65 64 00 00 bc 56 2e 00 68 74 74 70 3a 2f 2f  ded...V..http://
00000100: 6c 61 73 7a 69 70 2e 6f 72 67 00 00 00 00 00 00  laszip.org......

So there is no CRS in the file, and the Global Encoding is zero. A handheld SLAM scanner builds its cloud in a frame the walk defines, and the file records neither where that frame's origin ended up nor — since units are part of a CRS — what the numbers are in. Ask for the transform to a mapping frame when a scanner-local file arrives without one.

The same cloud rewritten as COPC

A Cloud Optimized Point Cloud is a LAZ 1.4 file with two extra records and the points put in a different order.

Two schematic byte layouts. A plain LAZ file is a header, VLRs and one run of compressed point records, with nothing recording which chunk covers which part of the site. The COPC rewrite of the same cloud, again 947,352 points, adds a copc info VLR near the front, which gives the byte offset of a hierarchy record at the end of the file, and that hierarchy gives the byte range of a single octree node — 1,641 plus 860 plus 182,215 bytes fetched for a whole-site overview.
Schematic, not to scale: the header and VLRs are 1,641 bytes of a 4,990,292-byte file. Segment order and the three fetched ranges are real.

The specification requires point format 6, 7 or 8, and an info record as the first VLR, which in a LAS 1.4 file means byte 375. The dump below starts at 368:

00000170: 00 00 00 00 00 00 00 00 00 63 6f 70 63 00 00 00  .........copc...
00000180: 00 00 00 00 00 00 00 00 00 01 00 a0 00 43 4f 50  .............COP
00000190: 43 20 69 6e 66 6f 20 56 4c 52 00 00 00 00 00 00  C info VLR......

User ID copc, record ID 1, a 160-byte payload describing an octree: the center and half-size of the root cube, the point spacing at the root, and the byte offset and size of the hierarchy — the pair that makes remote reading work.

At User ID Record Bytes Description
375 copc 1 160 COPC info VLR
589 laszip encoded 22204 40 lazperf variant
683 LASF_Projection 2112 904

The hierarchy is the second addition, an extended VLR at byte 4,989,432 with user ID copc and record ID 1000. Its 800-byte payload holds 25 entries of 32 bytes each: a node key — level, then x, y, z, so 0-0-0-0 is the root and each level down splits the cube in eight — the offset and length of that node's chunk, and its point count. The first four:

Key Offset Bytes Points
0-0-0-0 4,807,078 182,215 19,776
1-0-0-0 3,540,517 180,232 26,330
2-1-0-0 1,649 301,357 78,878
2-1-1-0 303,006 283,230 64,662

The 25 point counts sum to 947,352, the count in the header, and that agreement is the check worth running. Otherwise the COPC holds the same points in the same point format 6 under the same CRS.

The two headers state identical bounds, to the millimeter, because the COPC's were carried over from the LAZ unchanged. The points behind them are not: the LAZ decodes to its header bounds exactly, while the COPC decodes to 735,472.07 to 736,256.83, 4,285,407.86 to 4,286,002.91, and 163.74 to 208.74 — up to three millimeters outside its own declared box on four of its six faces, and inside it on the other two. Nothing recomputed the bounds when the scale changed.

The plain LAZ is 5,014,321 bytes and the COPC 4,990,292. The index itself is 1,074 bytes, a 214-byte info VLR and an 860-byte hierarchy record; the 24 KB gap measures point order, scale and a dropped 905-byte liblas copy of the WKT, not the index.

One region by HTTP range request

The plain LAZ declares a fixed 50,000 points per chunk, so a reader can seek to a chunk boundary but nothing tells it which chunk covers which part of the site. The COPC sets the same field to the variable-chunk sentinel, one chunk per octree node, and the hierarchy says where each one is, so asking for a region becomes a byte range:

curl -r 4807078-4989292 -o node.bin \
  https://bradleylab-public.s3.us-east-2.amazonaws.com/pointclouds/\
greenwood_cemetery_usgs3dep_2017.copc.laz

HTTP/1.1 206 Partial Content
Content-Range: bytes 4807078-4989292/4990292
Content-Length: 182215

Three requests give a whole-site overview: bytes 0–1,640 for the header and VLRs, bytes 4,989,432–4,990,291 for the hierarchy, and the 182,215 bytes of the root node — 184,716 bytes of 4,990,292, or 3.7 percent of the file, for 19,776 points across the entire cemetery. PDAL does the same through its COPC reader, which takes the URL directly and writes the same 19,776 points:

pdal translate --driver readers.copc \
  https://bradleylab-public.s3.us-east-2.amazonaws.com/pointclouds/\
greenwood_cemetery_usgs3dep_2017.copc.laz overview.las \
  --readers.copc.resolution=10

The chunks here are encoded by lazperf rather than by LASzip itself, recorded above as lazperf variant; the lab's publishing notes have that combination breaking CloudCompare and ArcGIS Pro for another lab file, in point format 7, in June 2026 — which is why the plain LAZ is published alongside.

Checks to run on a file you receive

  1. The version at bytes 24 and 25, then the header size at 94 and 95, read rather than inferred.
  2. The point count from the 64-bit field at 247 when the point format is 6 or above, the 32-bit field at 107 otherwise.
  3. A CRS record in the VLRs and the extended VLRs both, and Global Encoding bit 4 agreeing with it.
  4. Both ends of each axis against the integer limit, not the extent.
  5. The header bounds against the points.
  6. For a COPC, the first VLR copc at byte 375, and the hierarchy's point counts summing to the header's count.

Specifications: ASPRS LAS 1.4 R16, which defines the header offsets and point formats used here, and the COPC 1.0 specification. The Greenwood files are public: the COPC, the plain LAZ, and a manifest recording the source tiles and the crop. Point data are USGS 3DEP, project MO_Saint_Louis_Lidar_2017_B17, flown 2017-02-20, cropped without reclassification, filtering, thinning or reprojection.

Point cloud processing at the lab

The lab collects terrestrial, mobile and airborne lidar and processes it through version-controlled pipelines. Deliverables are cloud-native by default: COPC alongside plain LAZ, published to an open catalog.