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 charactersLASF. 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:
libLASandGeoSLAM. Free text, and the closest thing to provenance the header carries. - 90, 92
0f 01ande7 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.
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
- The version at bytes 24 and 25, then the header size at 94 and 95, read rather than inferred.
- 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.
- A CRS record in the VLRs and the extended VLRs both, and Global Encoding bit 4 agreeing with it.
- Both ends of each axis against the integer limit, not the extent.
- The header bounds against the points.
-
For a COPC, the first VLR
copcat 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.