Upgrading LAS 1.2 Files to LAS 1.4

TL;DR: Rewrite with writers.las using "minor_version": 4 and "dataformat_id": 6 (or 7 if the source is PDRF 2 or 3 with colour), set a_srs so a WKT CRS record is written, and make sure GPS time is adjusted standard time before writing — LAS 1.4 formats 6–10 require it. Then verify point counts, class histograms and coordinates are unchanged.

# Context and Motivation

This guide is part of LAS/LAZ File Structure. Archives are full of LAS 1.2 data in formats 1 and 3: the standard for airborne deliveries for a decade. Modern specifications and tools expect LAS 1.4 with formats 6–10, for good reasons: the 8-bit classification makes room for classes 19–22 and beyond, the overlap flag replaces the old habit of abusing class 12, WKT replaces GeoTIFF keys for CRS, and extra bytes carry custom attributes cleanly. Upgrading is mostly mechanical, but four details — class mapping, flags, GPS time and CRS — decide whether the result is a correct LAS 1.4 file or just a relabelled 1.2 file.

Four things to get right Four rows mapping LAS 1.2 to LAS 1.4. Point format 1 or 3 becomes 6 or 7. Class 12 used for overlap becomes the original class plus the overlap flag. GPS week time becomes adjusted standard GPS time. GeoTIFF key CRS records become a WKT CRS record. LAS 1.2 LAS 1.4 point format 1 or 3 point format 6 or 7 class 12 = overlap real class + overlap flag GPS week time adjusted standard GPS time GeoTIFF keys CRS WKT CRS record

# Prerequisites and Assumptions

  • PDAL 2.x, or laspy 2.x, and pyproj.
  • Knowledge of the source’s quirks: whether class 12 was used for overlap, whether GPS time is week time or adjusted standard time (global encoding bit 0), and what CRS it is really in.
  • For week-time data, the GPS week of the flight, from the flight log or acquisition dates.

# Step-by-Step Implementation

# Step 1 — Inspect the source

pdal info --metadata gives minor_version, dataformat_id, global_encoding (bit 0 = 1 means adjusted standard time) and the CRS; pdal info --stats --enumerate Classification lists classes.

# Step 2 — Resolve overlap class 12

If class 12 marks overlap, you cannot recover the true class from the file. Keep it as 1 (unclassified) with the overlap flag set, or reclassify those points, then set the flag.

# Step 3 — Convert GPS time

If bit 0 of global encoding is 0, GPS time is seconds of the week. Convert with the flight’s GPS week, as described in converting GPS week time to adjusted standard time.

# Step 4 — Write LAS 1.4

writers.las with minor_version: 4, dataformat_id: 6 (or 7 with RGB), a_srs set to the correct compound CRS, and forward to carry other header settings.

# Step 5 — Verify

Counts, class histogram (apart from deliberate 12 → 1 changes), coordinate ranges and GPS time ranges must match expectations.

# Complete Working Example

A PDAL pipeline for a source that used class 12 for overlap and already has adjusted standard time:

json
{
  "pipeline": [
    { "type": "readers.las", "filename": "legacy/t_0431_12.las" },
    { "type": "filters.assign", "value": [
        "Overlap = 1 WHERE Classification == 12",
        "Classification = 1 WHERE Classification == 12"
    ]},
    { "type": "writers.las", "filename": "upgraded/t_0431.laz",
      "minor_version": 4, "dataformat_id": 6,
      "a_srs": "EPSG:26915+5703",
      "global_encoding": 17,
      "forward": "scale_x,scale_y,scale_z,offset_x,offset_y,offset_z,system_id,software_id" }
  ]
}

global_encoding: 17 sets bit 0 (adjusted standard GPS time) and bit 4 (WKT CRS), both expected for PDRF 6–10. The assignment order matters: set the flag while the class is still 12, then change the class.

A laspy version with a GPS-week conversion, for sources in week time:

python
"""Upgrade LAS 1.2 (PDRF 1/3) to LAS 1.4 (PDRF 6/7) with laspy."""
from __future__ import annotations

import laspy
import numpy as np
from pyproj import CRS

SECONDS_PER_WEEK = 604_800


def upgrade(src: str, dst: str, crs: str, gps_week: int | None = None) -> None:
    las = laspy.read(src)
    target = 7 if las.header.point_format.id in (2, 3, 5) else 6
    new = laspy.convert(las, point_format_id=target, file_version="1.4")

    overlap = np.asarray(las.classification) == 12
    new.overlap = overlap.astype(np.uint8)
    new.classification = np.where(overlap, 1, np.asarray(las.classification)).astype(np.uint8)

    week_time = (int(las.header.global_encoding.gps_time_type) == 0)
    if week_time:
        if gps_week is None:
            raise ValueError("source uses GPS week time; supply the flight's GPS week")
        new.gps_time = gps_week * SECONDS_PER_WEEK + np.asarray(las.gps_time) - 1e9
    new.header.global_encoding.gps_time_type = laspy.header.GpsTimeType.STANDARD
    new.header.add_crs(CRS.from_user_input(crs))
    new.write(dst)


if __name__ == "__main__":
    upgrade("legacy/t_0431_12.las", "upgraded/t_0431.laz", "EPSG:26915+5703", gps_week=1987)
What the class histogram should show Paired bars for classes 1, 2, 5, 6 and 12, before and after upgrading. Classes 2, 5 and 6 are identical. Class 12 goes from 3.1 million to zero, and class 1 grows by exactly 3.1 million, with the overlap flag now set on those points. Any other change is a conversion error. 125612 → 0 after +3.1 M 1.2 1.4 classification

# Key Parameter Table

Setting Value Why
minor_version 4 LAS 1.4 header
dataformat_id 6, 7 or 8 6 from PDRF 1, 7 from PDRF 3, 8 if NIR exists
a_srs compound EPSG Writes a WKT CRS record
global_encoding 17 Adjusted standard time + WKT bits
forward scales, offsets, IDs Keeps precision and provenance fields
class 12 handling overlap flag + class 1 LAS 1.4 reserves 12 and uses the flag

# Verification

  • Counts identical. Source and output point counts match exactly.
  • Histogram diff. Only class 12 → 1 moves; every other class count is unchanged.
  • GPS time plausible. Adjusted standard time for flights in the 2010s and 2020s falls roughly between 0 and a few hundred million seconds; week time falls between 0 and 604,800. Check which range the output is in.
  • CRS present. pdal info --metadata shows a WKT CRS on the output.

# Gotchas and Edge Cases

Class 12 was not always overlap. Some vendors used 12 for other purposes. Confirm with the delivery documentation before converting it to an overlap flag.

Unknown GPS week. Week time cannot be converted without the week. If the flight date is unknown, the GPS times can still be kept consistent within the file, but they cannot be aligned with trajectories or other flights. Document the limitation.

CRS from GeoTIFF keys. Some 1.2 files carry incomplete GeoTIFF keys — horizontal only, or a user-defined projection. Resolve the true CRS from the delivery metadata, and set it explicitly rather than trusting the automatic translation.

Telling the two time types apart Two number lines. GPS week time runs from 0 to 604,800 seconds and resets every week. Adjusted standard GPS time is seconds since the GPS epoch minus one billion; for flights around 2018 it is roughly 200 to 250 million. A value like 385,000 is week time; a value like 218,000,000 is adjusted standard time. week time 0 604,800 s 385,000 adjusted standard 0 (2011) ≈ 500 M s 218,000,000 (2018)

Laspy convert drops fields. Converting PDRF 3 to PDRF 6 drops RGB. Choose 7 to keep colour; laspy will not warn.

# Frequently Asked Questions

How do I convert LAS 1.2 to LAS 1.4?

Rewrite the file with PDAL’s writers.las using minor_version 4 and a LAS 1.4 point format such as 6 or 7, or with laspy’s convert function. Handle class 12 overlap, GPS time type and the CRS explicitly while doing so.

Why does LAS 1.4 need adjusted standard GPS time?

Point formats 6 to 10 are defined with adjusted standard GPS time, a continuous timescale, so points from different weeks and flights can be ordered and matched to trajectories. Week time resets every week and is ambiguous without the week number.

What happens to class 12 overlap points?

In LAS 1.4, overlap is a flag rather than a class. Set the overlap flag on those points and assign them a real class, or class 1 if the true class is unknown.

Do I lose anything by upgrading?

Not if the target format holds every source field: 6 for format 1, 7 for format 3. Choosing a format without colour for a coloured source drops RGB silently, so check before converting.