CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-laspy

Native Python ASPRS LAS read/write library for processing LiDAR point cloud data

Pending
Overview
Eval results
Files

vlr.mddocs/

VLR Management

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.

Capabilities

Variable Length Records

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")

VLR Base Classes

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]: ...

GeoTIFF VLR Support

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 - GeoAsciiParamsTag

Usage 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")

Standard VLR Types

Well-Known VLR Categories

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 = 2

Working with Standard VLRs

import 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')

Advanced VLR Usage

Custom VLR Classes

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')

VLR Inspection and Validation

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')

VLR Migration and Conversion

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

docs

compression.md

copc.md

core-io.md

data-containers.md

index.md

io-handlers.md

point-data.md

vlr.md

tile.json