Choosing a Projected CRS for a LiDAR Project

TL;DR: Process in a metric projected CRS with a current datum realization — for example NAD83(2011) / UTM (EPSG:6330–6348) in the US, ETRS89 / UTM (EPSG:25828–25838) in Europe, or the national grid your client uses — combined with the official vertical datum as a compound CRS. Find the UTM zone from the project centroid with pyproj.database.query_utm_crs_info, and keep a project that straddles two zones in one zone rather than splitting it.

# Context and Motivation

This guide is part of Coordinate Reference Systems. Everything that measures distances or areas in LiDAR processing — point spacing, SMRF windows, raster cell sizes, buffer widths, slope — assumes a projected CRS in metres with small distortion. Geographic coordinates in degrees break all of it, and a projection chosen carelessly introduces scale errors or forces data across zone boundaries. The choice is usually easy, but it should be made once, deliberately, at the start of a project, and written into every file as a compound CRS so that heights are as well defined as positions.

Three questions settle most projects A decision tree. First: does the client or regulator mandate a CRS? If yes, use it. If no: is there a national or state grid in routine use for this area? If yes, use that grid with its current datum realization. If no: use the UTM zone containing the project centroid on the current national datum, and pair it with the official vertical datum as a compound CRS. client mandates a CRS? national/state grid in use? UTM zone of centroid current datum + vertical use the mandated CRS as a compound CRS national grid, current datum yes no yes no

# Prerequisites and Assumptions

  • The project boundary, in any CRS, to compute a centroid and extent.
  • pyproj 3.x.
  • Knowledge of the client’s requirements and of the official vertical datum and geoid model for the region.

# Step-by-Step Implementation

# Step 1 — Check for a mandated CRS

Contracts and national programmes often fix the delivery CRS. If so, process in it too, unless it is geographic, in which case process in a projected CRS and reproject once at delivery.

# Step 2 — Find the UTM zone

Query pyproj for UTM CRSs whose area of use contains the project centroid, filtered by datum name.

# Step 3 — Check the extent against the zone

UTM zones are 6° wide. A project that crosses a zone boundary by a few kilometres should stay in the zone containing most of it; scale error just outside a zone edge is small and far better than splitting the project.

# Step 4 — Choose the vertical datum

Use the official vertical datum for the region — NAVD88 (EPSG:5703) in the conterminous US today, EVRF2007 or a national height system in Europe — and record the geoid model used.

# Step 5 — Write the compound code everywhere

EPSG:6344+5703 style codes in writers.las (a_srs), writers.gdal and every output’s metadata.

# Complete Working Example

python
"""Pick a UTM CRS for a project boundary and report scale distortion across it."""
from __future__ import annotations

import geopandas as gpd
from pyproj import CRS, Proj
from pyproj.aoi import AreaOfInterest
from pyproj.database import query_utm_crs_info


def pick_utm(boundary: gpd.GeoDataFrame, datum_name: str = "NAD83(2011)") -> CRS:
    geo = boundary.to_crs("EPSG:4326")
    lon, lat = geo.geometry.union_all().centroid.coords[0]
    infos = query_utm_crs_info(datum_name=datum_name,
                               area_of_interest=AreaOfInterest(lon, lat, lon, lat))
    if not infos:
        raise ValueError(f"no {datum_name} UTM zone found at {lon:.3f}, {lat:.3f}")
    return CRS.from_epsg(infos[0].code)


def scale_report(boundary: gpd.GeoDataFrame, crs: CRS) -> dict:
    geo = boundary.to_crs("EPSG:4326").geometry.union_all()
    minx, miny, maxx, maxy = geo.bounds
    proj = Proj(crs)
    factors = []
    for lon in (minx, (minx + maxx) / 2, maxx):
        for lat in (miny, maxy):
            factors.append(proj.get_factors(lon, lat).meridional_scale)
    return {"crs": crs.name, "epsg": crs.to_epsg(),
            "scale_min": round(min(factors), 6), "scale_max": round(max(factors), 6),
            "max_distortion_ppm": round(max(abs(f - 1) for f in factors) * 1e6, 1)}


if __name__ == "__main__":
    area = gpd.read_file("project/boundary.gpkg")
    utm = pick_utm(area)
    print(scale_report(area, utm))
    compound = f"EPSG:{utm.to_epsg()}+5703"
    print("write outputs with a_srs =", compound, "->", CRS.from_user_input(compound).name)

A typical result for a county near the central meridian:

text
{'crs': 'NAD83(2011) / UTM zone 15N', 'epsg': 6344, 'scale_min': 0.999602, 'scale_max': 0.999701, 'max_distortion_ppm': 398.0}
write outputs with a_srs = EPSG:6344+5703 -> NAD83(2011) / UTM zone 15N + NAVD88 height

A scale factor of 0.9996 at the central meridian is by design; 400 ppm is 4 cm per 100 m, negligible for point spacing and cell sizes but worth knowing for survey-grade distance comparisons.

Scale across one UTM zone A curve of the UTM point scale factor across a 6 degree zone at mid-latitudes. It is 0.9996 at the central meridian, rises to exactly 1 about 180 kilometres either side, and reaches about 1.0010 at the zone edges. A project straddling the edge by a small distance sees only a slight further increase. 0.9996 at central meridian scale = 1 ≈ 1.0010 at zone edge −3°+3°

# Key Parameter Table

Region Horizontal (projected) Vertical Compound example
Conterminous US NAD83(2011) / UTM, EPSG:6330–6348 NAVD88 height, EPSG:5703 EPSG:6347+5703
US, state plane NAD83(2011) / State Plane (m or ftUS) NAVD88 (m or ftUS) EPSG:6539+6360
Europe ETRS89 / UTM, EPSG:25828–25838 EVRF2007 height, EPSG:5621 EPSG:25832+5621
Great Britain OSGB36 / British National Grid, EPSG:27700 ODN height, EPSG:5701 EPSG:7405 (predefined)
Global fallback WGS 84 / UTM, EPSG:326xx / 327xx EGM2008 height, EPSG:3855 EPSG:32633+3855

Confirm each code in your PROJ database before use; national agencies periodically publish new realizations.

# Verification

  • Centroid lands in the zone. Transform the centroid to the chosen CRS; the easting should fall well within 166,000–834,000 m for UTM.
  • Round trip. Transform a few boundary vertices to the CRS and back; errors should be sub-millimetre.
  • Vertical present. CRS.from_user_input(compound).sub_crs_list has two members, projected and vertical.

# Gotchas and Edge Cases

Old datum realizations. Plain NAD83 (EPSG:26915 etc.) and NAD83(2011) differ by up to a metre or more in places. Choose the realization that matches your control survey, and use it consistently.

Feet in state plane. Many state plane CRSs have both metre and US-survey-foot variants. Using a feet CRS for processing complicates every metric parameter; process in metres and convert at delivery if the client wants feet, as in reprojecting State Plane feet to metres.

Projects spanning two zones. Splitting a project across zones creates a seam in every product. Keep one zone, accept the slightly larger scale factor beyond the edge, or use a custom transverse Mercator centred on the project for very wide areas.

Straddling a zone boundary Two adjacent UTM zones separated by a vertical boundary line. A project area crosses the boundary, with most of it in zone 15 and a small part in zone 16. The recommended choice keeps the whole project in zone 15, with slightly higher scale distortion in the small part beyond the edge, instead of splitting it and creating a seam. UTM zone 15 UTM zone 16 project, processed in zone 15 small overhang: keep, do not split

Geographic processing. Some public data is distributed in EPSG:4326. Reproject to a projected CRS before any metric processing; filters that take distances in metres interpret degrees as metres without complaint.

# Frequently Asked Questions

Which UTM zone should I use?

The zone containing the project centroid, which pyproj can find with query_utm_crs_info. If the project crosses a zone boundary, keep it in the zone containing most of the area rather than splitting it.

Should I use UTM or a national grid?

Use what your client and local users work in. National and state grids are designed to minimize distortion locally and match existing mapping; UTM is a sound default when no local grid is standard.

Why use a compound CRS?

A compound CRS declares both the horizontal system and the vertical datum. Without the vertical part, heights are ambiguous, and datum or geoid mismatches become impossible to detect or correct later.

Can I process LiDAR in latitude and longitude?

Not sensibly. Point spacing, window sizes, raster resolution and slopes all assume metric coordinates. Reproject to a projected CRS first and, if needed, back to geographic at delivery.