Native Python ASPRS LAS read/write library for processing LiDAR point cloud data
—
Variable Length Record (VLR) handling for storing metadata, coordinate reference systems, and custom application data within LAS files. VLRs provide a standardized way to embed additional information beyond the basic LAS specification.
Core VLR implementation for storing arbitrary data with standardized identification.
class VLR:
def __init__(self, user_id, record_id, description="", record_data=b""):
"""
Create Variable Length Record.
Parameters:
- user_id: str - User/organization identifier (up to 16 chars)
- record_id: int - Record type identifier (0-65535)
- description: str - Human-readable description (up to 32 chars)
- record_data: bytes - Binary record data
"""
@property
def user_id(self) -> str:
"""User/organization identifier."""
@property
def record_id(self) -> int:
"""Record type identifier."""
@property
def description(self) -> str:
"""Human-readable description."""
@property
def record_data(self) -> bytes:
"""Binary record data payload."""
def record_data_bytes(self) -> bytes:
"""Get binary representation of record data."""Usage Examples:
import laspy
# Create custom VLR for application metadata
metadata_vlr = laspy.VLR(
user_id="MyCompany",
record_id=1001,
description="Processing metadata",
record_data=b'{"version": "1.0", "algorithm": "ground_filter_v2"}'
)
# Create VLR with structured data
import json
import struct
processing_params = {
"threshold": 2.5,
"iterations": 3,
"algorithm": "progressive_morphology"
}
# Encode as JSON bytes
json_data = json.dumps(processing_params).encode('utf-8')
params_vlr = laspy.VLR(
user_id="ProcessingApp",
record_id=2001,
description="Algorithm parameters",
record_data=json_data
)
# Add VLRs to LAS file
las = laspy.read('input.las')
las.vlrs.append(metadata_vlr)
las.vlrs.append(params_vlr)
las.write('output_with_metadata.las')
print(f"Added {len(las.vlrs)} VLRs to file")Abstract base classes for creating custom VLR types with standardized interfaces.
class IVLR:
"""Abstract interface for Variable Length Records."""
@property
def user_id(self) -> str: ...
@property
def record_id(self) -> int: ...
@property
def description(self) -> Union[str, bytes]: ...
def record_data_bytes(self) -> bytes:
"""Get binary representation of record data."""
class BaseVLR(IVLR):
"""Abstract base implementation for VLRs."""
def __init__(self, user_id, record_id, description=""):
"""
Initialize base VLR.
Parameters:
- user_id: str - User/organization identifier
- record_id: int - Record type identifier
- description: str - Human-readable description
"""
@property
def user_id(self) -> str: ...
@property
def record_id(self) -> int: ...
@property
def description(self) -> Union[str, bytes]: ...Specialized support for GeoTIFF coordinate reference system information.
# GeoTIFF VLR utilities available in laspy.vlrs.geotiff module
from laspy.vlrs import geotiff
# Standard GeoTIFF VLR user IDs and record IDs are predefined
# LASF_Projection user_id with various record_id values:
# 34735 - GeoKeyDirectoryTag
# 34736 - GeoDoubleParamsTag
# 34737 - GeoAsciiParamsTagUsage Examples:
import laspy
from laspy.vlrs import geotiff
# Read and inspect GeoTIFF VLRs
las = laspy.read('georeferenced.las')
for vlr in las.vlrs:
if vlr.user_id == "LASF_Projection":
print(f"Found GeoTIFF VLR: ID={vlr.record_id}, Desc='{vlr.description}'")
if vlr.record_id == 34735: # GeoKeyDirectoryTag
print("This VLR contains GeoTIFF key directory")
elif vlr.record_id == 34736: # GeoDoubleParamsTag
print("This VLR contains GeoTIFF double parameters")
elif vlr.record_id == 34737: # GeoAsciiParamsTag
print("This VLR contains GeoTIFF ASCII parameters")
# Parse coordinate reference system (requires pyproj)
try:
crs = las.header.parse_crs()
if crs:
print(f"Parsed CRS: {crs}")
print(f"CRS Authority: {crs.to_authority()}")
else:
print("No CRS information found")
except ImportError:
print("pyproj not available for CRS parsing")LAS files commonly use these standardized VLR types:
# Coordinate Reference System VLRs
CRS_USER_ID = "LASF_Projection"
GEO_KEY_DIRECTORY = 34735
GEO_DOUBLE_PARAMS = 34736
GEO_ASCII_PARAMS = 34737
WKT_COORDINATE_SYSTEM = 2112
# Extra Bytes VLRs (for custom point dimensions)
EXTRA_BYTES_USER_ID = "LASF_Projection"
EXTRA_BYTES_RECORD_ID = 4
# Classification Lookup VLRs
CLASSIFICATION_USER_ID = "LASF_Projection"
CLASSIFICATION_RECORD_ID = 0
# Flight Line VLRs
FLIGHT_LINE_USER_ID = "LASF_Projection"
FLIGHT_LINE_RECORD_ID = 1
# Histogram VLRs
HISTOGRAM_USER_ID = "LASF_Projection"
HISTOGRAM_RECORD_ID = 2import laspy
import struct
def add_flight_line_info(las_data, flight_lines):
"""Add flight line information as VLR."""
# Pack flight line data
# Format: count (4 bytes) + flight_line_ids (4 bytes each)
data = struct.pack('<I', len(flight_lines)) # Count
for flight_id in flight_lines:
data += struct.pack('<I', flight_id)
flight_vlr = laspy.VLR(
user_id="LASF_Projection",
record_id=1,
description="Flight line information",
record_data=data
)
las_data.vlrs.append(flight_vlr)
return las_data
def add_classification_lookup(las_data, class_lookup):
"""Add classification lookup table as VLR."""
# Create lookup table data
# Format: class_id (1 byte) + description_length (1 byte) + description (variable)
data = b""
for class_id, description in class_lookup.items():
desc_bytes = description.encode('utf-8')
data += struct.pack('<BB', class_id, len(desc_bytes))
data += desc_bytes
class_vlr = laspy.VLR(
user_id="LASF_Projection",
record_id=0,
description="Classification lookup",
record_data=data
)
las_data.vlrs.append(class_vlr)
return las_data
# Usage
las = laspy.read('input.las')
# Add flight line info
flight_lines = [101, 102, 103, 104]
las = add_flight_line_info(las, flight_lines)
# Add classification lookup
classifications = {
0: "Never classified",
1: "Unclassified",
2: "Ground",
3: "Low vegetation",
4: "Medium vegetation",
5: "High vegetation",
6: "Building"
}
las = add_classification_lookup(las, classifications)
las.write('output_with_vlrs.las')import laspy
import json
import struct
from typing import Dict, Any
class JsonVLR(laspy.vlrs.BaseVLR):
"""VLR that stores JSON data."""
def __init__(self, user_id: str, record_id: int, data: Dict[str, Any], description: str = ""):
super().__init__(user_id, record_id, description)
self._data = data
@property
def data(self) -> Dict[str, Any]:
"""Get JSON data."""
return self._data
@data.setter
def data(self, value: Dict[str, Any]):
"""Set JSON data."""
self._data = value
def record_data_bytes(self) -> bytes:
"""Serialize JSON data to bytes."""
return json.dumps(self._data).encode('utf-8')
@classmethod
def from_vlr(cls, vlr: laspy.VLR) -> 'JsonVLR':
"""Create JsonVLR from standard VLR."""
data = json.loads(vlr.record_data.decode('utf-8'))
return cls(vlr.user_id, vlr.record_id, data, vlr.description)
class BinaryStructVLR(laspy.vlrs.BaseVLR):
"""VLR that stores structured binary data."""
def __init__(self, user_id: str, record_id: int, format_string: str, values: tuple, description: str = ""):
super().__init__(user_id, record_id, description)
self.format_string = format_string
self.values = values
def record_data_bytes(self) -> bytes:
"""Pack structured data to bytes."""
return struct.pack(self.format_string, *self.values)
@classmethod
def from_vlr(cls, vlr: laspy.VLR, format_string: str) -> 'BinaryStructVLR':
"""Create BinaryStructVLR from standard VLR."""
values = struct.unpack(format_string, vlr.record_data)
return cls(vlr.user_id, vlr.record_id, format_string, values, vlr.description)
# Usage examples
las = laspy.read('input.las')
# Add JSON metadata
metadata = {
"processing_date": "2024-01-15",
"software_version": "2.1.0",
"quality_metrics": {
"ground_points_percent": 65.2,
"outliers_removed": 1250
}
}
json_vlr = JsonVLR("MyCompany", 1001, metadata, "Processing metadata")
las.vlrs.append(json_vlr)
# Add structured binary data
survey_params = (
12.5, # flight_height (float)
45.0, # scan_angle_max (float)
500000, # pulse_frequency (uint32)
2 # returns_per_pulse (uint16)
)
binary_vlr = BinaryStructVLR(
"SurveyApp", 2001,
'<ffIH', # Little-endian: 2 floats, 1 uint32, 1 uint16
survey_params,
"Survey parameters"
)
las.vlrs.append(binary_vlr)
las.write('output_custom_vlrs.las')import laspy
def inspect_vlrs(las_file):
"""Inspect all VLRs in a LAS file."""
las = laspy.read(las_file)
print(f"File: {las_file}")
print(f"Total VLRs: {len(las.vlrs)}")
if hasattr(las, 'evlrs') and las.evlrs:
print(f"Extended VLRs: {len(las.evlrs)}")
print("\nVLR Details:")
print("-" * 80)
for i, vlr in enumerate(las.vlrs):
print(f"VLR {i+1}:")
print(f" User ID: '{vlr.user_id}'")
print(f" Record ID: {vlr.record_id}")
print(f" Description: '{vlr.description}'")
print(f" Data size: {len(vlr.record_data)} bytes")
# Try to identify known VLR types
if vlr.user_id == "LASF_Projection":
if vlr.record_id == 34735:
print(" Type: GeoTIFF Key Directory")
elif vlr.record_id == 34736:
print(" Type: GeoTIFF Double Parameters")
elif vlr.record_id == 34737:
print(" Type: GeoTIFF ASCII Parameters")
elif vlr.record_id == 2112:
print(" Type: WKT Coordinate System")
elif vlr.record_id == 4:
print(" Type: Extra Bytes Description")
# Show first few bytes of data
if len(vlr.record_data) > 0:
preview = vlr.record_data[:20]
print(f" Data preview: {preview}")
print()
def validate_vlrs(las_file):
"""Validate VLR integrity and standards compliance."""
las = laspy.read(las_file)
issues = []
for i, vlr in enumerate(las.vlrs):
# Check user ID length (16 chars max)
if len(vlr.user_id) > 16:
issues.append(f"VLR {i+1}: User ID too long ({len(vlr.user_id)} > 16)")
# Check description length (32 chars max)
if len(vlr.description) > 32:
issues.append(f"VLR {i+1}: Description too long ({len(vlr.description)} > 32)")
# Check record ID range (0-65535)
if not (0 <= vlr.record_id <= 65535):
issues.append(f"VLR {i+1}: Record ID out of range ({vlr.record_id})")
# Check for reserved record IDs
if vlr.user_id == "LASF_Projection":
reserved_ids = {34735, 34736, 34737, 2112, 4, 0, 1, 2}
if vlr.record_id not in reserved_ids:
issues.append(f"VLR {i+1}: Non-standard record ID for LASF_Projection ({vlr.record_id})")
if issues:
print("VLR Validation Issues:")
for issue in issues:
print(f" - {issue}")
else:
print("All VLRs passed validation")
return len(issues) == 0
# Usage
inspect_vlrs('sample.las')
is_valid = validate_vlrs('sample.las')import laspy
def copy_vlrs_between_files(source_file, target_file, vlr_filter=None):
"""Copy VLRs from source to target file."""
source = laspy.read(source_file)
target = laspy.read(target_file)
copied_count = 0
for vlr in source.vlrs:
# Apply filter if provided
if vlr_filter and not vlr_filter(vlr):
continue
# Check if VLR already exists in target
exists = any(
(existing.user_id == vlr.user_id and existing.record_id == vlr.record_id)
for existing in target.vlrs
)
if not exists:
target.vlrs.append(vlr)
copied_count += 1
target.write(target_file)
print(f"Copied {copied_count} VLRs to {target_file}")
def crs_vlr_filter(vlr):
"""Filter for CRS-related VLRs only."""
return (vlr.user_id == "LASF_Projection" and
vlr.record_id in {34735, 34736, 34737, 2112})
def custom_vlr_filter(vlr):
"""Filter for custom application VLRs only."""
return vlr.user_id not in {"LASF_Projection"}
# Copy only CRS information
copy_vlrs_between_files('source.las', 'target.las', crs_vlr_filter)
# Copy only custom VLRs
copy_vlrs_between_files('source.las', 'target.las', custom_vlr_filter)Install with Tessl CLI
npx tessl i tessl/pypi-laspy