0
# VLR Management
1
2
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.
3
4
## Capabilities
5
6
### Variable Length Records
7
8
Core VLR implementation for storing arbitrary data with standardized identification.
9
10
```python { .api }
11
class VLR:
12
def __init__(self, user_id, record_id, description="", record_data=b""):
13
"""
14
Create Variable Length Record.
15
16
Parameters:
17
- user_id: str - User/organization identifier (up to 16 chars)
18
- record_id: int - Record type identifier (0-65535)
19
- description: str - Human-readable description (up to 32 chars)
20
- record_data: bytes - Binary record data
21
"""
22
23
@property
24
def user_id(self) -> str:
25
"""User/organization identifier."""
26
27
@property
28
def record_id(self) -> int:
29
"""Record type identifier."""
30
31
@property
32
def description(self) -> str:
33
"""Human-readable description."""
34
35
@property
36
def record_data(self) -> bytes:
37
"""Binary record data payload."""
38
39
def record_data_bytes(self) -> bytes:
40
"""Get binary representation of record data."""
41
```
42
43
**Usage Examples:**
44
45
```python
46
import laspy
47
48
# Create custom VLR for application metadata
49
metadata_vlr = laspy.VLR(
50
user_id="MyCompany",
51
record_id=1001,
52
description="Processing metadata",
53
record_data=b'{"version": "1.0", "algorithm": "ground_filter_v2"}'
54
)
55
56
# Create VLR with structured data
57
import json
58
import struct
59
60
processing_params = {
61
"threshold": 2.5,
62
"iterations": 3,
63
"algorithm": "progressive_morphology"
64
}
65
66
# Encode as JSON bytes
67
json_data = json.dumps(processing_params).encode('utf-8')
68
69
params_vlr = laspy.VLR(
70
user_id="ProcessingApp",
71
record_id=2001,
72
description="Algorithm parameters",
73
record_data=json_data
74
)
75
76
# Add VLRs to LAS file
77
las = laspy.read('input.las')
78
las.vlrs.append(metadata_vlr)
79
las.vlrs.append(params_vlr)
80
las.write('output_with_metadata.las')
81
82
print(f"Added {len(las.vlrs)} VLRs to file")
83
```
84
85
### VLR Base Classes
86
87
Abstract base classes for creating custom VLR types with standardized interfaces.
88
89
```python { .api }
90
class IVLR:
91
"""Abstract interface for Variable Length Records."""
92
93
@property
94
def user_id(self) -> str: ...
95
96
@property
97
def record_id(self) -> int: ...
98
99
@property
100
def description(self) -> Union[str, bytes]: ...
101
102
def record_data_bytes(self) -> bytes:
103
"""Get binary representation of record data."""
104
105
class BaseVLR(IVLR):
106
"""Abstract base implementation for VLRs."""
107
108
def __init__(self, user_id, record_id, description=""):
109
"""
110
Initialize base VLR.
111
112
Parameters:
113
- user_id: str - User/organization identifier
114
- record_id: int - Record type identifier
115
- description: str - Human-readable description
116
"""
117
118
@property
119
def user_id(self) -> str: ...
120
121
@property
122
def record_id(self) -> int: ...
123
124
@property
125
def description(self) -> Union[str, bytes]: ...
126
```
127
128
### GeoTIFF VLR Support
129
130
Specialized support for GeoTIFF coordinate reference system information.
131
132
```python { .api }
133
# GeoTIFF VLR utilities available in laspy.vlrs.geotiff module
134
from laspy.vlrs import geotiff
135
136
# Standard GeoTIFF VLR user IDs and record IDs are predefined
137
# LASF_Projection user_id with various record_id values:
138
# 34735 - GeoKeyDirectoryTag
139
# 34736 - GeoDoubleParamsTag
140
# 34737 - GeoAsciiParamsTag
141
```
142
143
**Usage Examples:**
144
145
```python
146
import laspy
147
from laspy.vlrs import geotiff
148
149
# Read and inspect GeoTIFF VLRs
150
las = laspy.read('georeferenced.las')
151
152
for vlr in las.vlrs:
153
if vlr.user_id == "LASF_Projection":
154
print(f"Found GeoTIFF VLR: ID={vlr.record_id}, Desc='{vlr.description}'")
155
156
if vlr.record_id == 34735: # GeoKeyDirectoryTag
157
print("This VLR contains GeoTIFF key directory")
158
elif vlr.record_id == 34736: # GeoDoubleParamsTag
159
print("This VLR contains GeoTIFF double parameters")
160
elif vlr.record_id == 34737: # GeoAsciiParamsTag
161
print("This VLR contains GeoTIFF ASCII parameters")
162
163
# Parse coordinate reference system (requires pyproj)
164
try:
165
crs = las.header.parse_crs()
166
if crs:
167
print(f"Parsed CRS: {crs}")
168
print(f"CRS Authority: {crs.to_authority()}")
169
else:
170
print("No CRS information found")
171
except ImportError:
172
print("pyproj not available for CRS parsing")
173
```
174
175
## Standard VLR Types
176
177
### Well-Known VLR Categories
178
179
LAS files commonly use these standardized VLR types:
180
181
```python
182
# Coordinate Reference System VLRs
183
CRS_USER_ID = "LASF_Projection"
184
GEO_KEY_DIRECTORY = 34735
185
GEO_DOUBLE_PARAMS = 34736
186
GEO_ASCII_PARAMS = 34737
187
WKT_COORDINATE_SYSTEM = 2112
188
189
# Extra Bytes VLRs (for custom point dimensions)
190
EXTRA_BYTES_USER_ID = "LASF_Projection"
191
EXTRA_BYTES_RECORD_ID = 4
192
193
# Classification Lookup VLRs
194
CLASSIFICATION_USER_ID = "LASF_Projection"
195
CLASSIFICATION_RECORD_ID = 0
196
197
# Flight Line VLRs
198
FLIGHT_LINE_USER_ID = "LASF_Projection"
199
FLIGHT_LINE_RECORD_ID = 1
200
201
# Histogram VLRs
202
HISTOGRAM_USER_ID = "LASF_Projection"
203
HISTOGRAM_RECORD_ID = 2
204
```
205
206
### Working with Standard VLRs
207
208
```python
209
import laspy
210
import struct
211
212
def add_flight_line_info(las_data, flight_lines):
213
"""Add flight line information as VLR."""
214
215
# Pack flight line data
216
# Format: count (4 bytes) + flight_line_ids (4 bytes each)
217
data = struct.pack('<I', len(flight_lines)) # Count
218
for flight_id in flight_lines:
219
data += struct.pack('<I', flight_id)
220
221
flight_vlr = laspy.VLR(
222
user_id="LASF_Projection",
223
record_id=1,
224
description="Flight line information",
225
record_data=data
226
)
227
228
las_data.vlrs.append(flight_vlr)
229
return las_data
230
231
def add_classification_lookup(las_data, class_lookup):
232
"""Add classification lookup table as VLR."""
233
234
# Create lookup table data
235
# Format: class_id (1 byte) + description_length (1 byte) + description (variable)
236
data = b""
237
for class_id, description in class_lookup.items():
238
desc_bytes = description.encode('utf-8')
239
data += struct.pack('<BB', class_id, len(desc_bytes))
240
data += desc_bytes
241
242
class_vlr = laspy.VLR(
243
user_id="LASF_Projection",
244
record_id=0,
245
description="Classification lookup",
246
record_data=data
247
)
248
249
las_data.vlrs.append(class_vlr)
250
return las_data
251
252
# Usage
253
las = laspy.read('input.las')
254
255
# Add flight line info
256
flight_lines = [101, 102, 103, 104]
257
las = add_flight_line_info(las, flight_lines)
258
259
# Add classification lookup
260
classifications = {
261
0: "Never classified",
262
1: "Unclassified",
263
2: "Ground",
264
3: "Low vegetation",
265
4: "Medium vegetation",
266
5: "High vegetation",
267
6: "Building"
268
}
269
las = add_classification_lookup(las, classifications)
270
271
las.write('output_with_vlrs.las')
272
```
273
274
## Advanced VLR Usage
275
276
### Custom VLR Classes
277
278
```python
279
import laspy
280
import json
281
import struct
282
from typing import Dict, Any
283
284
class JsonVLR(laspy.vlrs.BaseVLR):
285
"""VLR that stores JSON data."""
286
287
def __init__(self, user_id: str, record_id: int, data: Dict[str, Any], description: str = ""):
288
super().__init__(user_id, record_id, description)
289
self._data = data
290
291
@property
292
def data(self) -> Dict[str, Any]:
293
"""Get JSON data."""
294
return self._data
295
296
@data.setter
297
def data(self, value: Dict[str, Any]):
298
"""Set JSON data."""
299
self._data = value
300
301
def record_data_bytes(self) -> bytes:
302
"""Serialize JSON data to bytes."""
303
return json.dumps(self._data).encode('utf-8')
304
305
@classmethod
306
def from_vlr(cls, vlr: laspy.VLR) -> 'JsonVLR':
307
"""Create JsonVLR from standard VLR."""
308
data = json.loads(vlr.record_data.decode('utf-8'))
309
return cls(vlr.user_id, vlr.record_id, data, vlr.description)
310
311
class BinaryStructVLR(laspy.vlrs.BaseVLR):
312
"""VLR that stores structured binary data."""
313
314
def __init__(self, user_id: str, record_id: int, format_string: str, values: tuple, description: str = ""):
315
super().__init__(user_id, record_id, description)
316
self.format_string = format_string
317
self.values = values
318
319
def record_data_bytes(self) -> bytes:
320
"""Pack structured data to bytes."""
321
return struct.pack(self.format_string, *self.values)
322
323
@classmethod
324
def from_vlr(cls, vlr: laspy.VLR, format_string: str) -> 'BinaryStructVLR':
325
"""Create BinaryStructVLR from standard VLR."""
326
values = struct.unpack(format_string, vlr.record_data)
327
return cls(vlr.user_id, vlr.record_id, format_string, values, vlr.description)
328
329
# Usage examples
330
las = laspy.read('input.las')
331
332
# Add JSON metadata
333
metadata = {
334
"processing_date": "2024-01-15",
335
"software_version": "2.1.0",
336
"quality_metrics": {
337
"ground_points_percent": 65.2,
338
"outliers_removed": 1250
339
}
340
}
341
342
json_vlr = JsonVLR("MyCompany", 1001, metadata, "Processing metadata")
343
las.vlrs.append(json_vlr)
344
345
# Add structured binary data
346
survey_params = (
347
12.5, # flight_height (float)
348
45.0, # scan_angle_max (float)
349
500000, # pulse_frequency (uint32)
350
2 # returns_per_pulse (uint16)
351
)
352
353
binary_vlr = BinaryStructVLR(
354
"SurveyApp", 2001,
355
'<ffIH', # Little-endian: 2 floats, 1 uint32, 1 uint16
356
survey_params,
357
"Survey parameters"
358
)
359
las.vlrs.append(binary_vlr)
360
361
las.write('output_custom_vlrs.las')
362
```
363
364
### VLR Inspection and Validation
365
366
```python
367
import laspy
368
369
def inspect_vlrs(las_file):
370
"""Inspect all VLRs in a LAS file."""
371
372
las = laspy.read(las_file)
373
374
print(f"File: {las_file}")
375
print(f"Total VLRs: {len(las.vlrs)}")
376
377
if hasattr(las, 'evlrs') and las.evlrs:
378
print(f"Extended VLRs: {len(las.evlrs)}")
379
380
print("\nVLR Details:")
381
print("-" * 80)
382
383
for i, vlr in enumerate(las.vlrs):
384
print(f"VLR {i+1}:")
385
print(f" User ID: '{vlr.user_id}'")
386
print(f" Record ID: {vlr.record_id}")
387
print(f" Description: '{vlr.description}'")
388
print(f" Data size: {len(vlr.record_data)} bytes")
389
390
# Try to identify known VLR types
391
if vlr.user_id == "LASF_Projection":
392
if vlr.record_id == 34735:
393
print(" Type: GeoTIFF Key Directory")
394
elif vlr.record_id == 34736:
395
print(" Type: GeoTIFF Double Parameters")
396
elif vlr.record_id == 34737:
397
print(" Type: GeoTIFF ASCII Parameters")
398
elif vlr.record_id == 2112:
399
print(" Type: WKT Coordinate System")
400
elif vlr.record_id == 4:
401
print(" Type: Extra Bytes Description")
402
403
# Show first few bytes of data
404
if len(vlr.record_data) > 0:
405
preview = vlr.record_data[:20]
406
print(f" Data preview: {preview}")
407
408
print()
409
410
def validate_vlrs(las_file):
411
"""Validate VLR integrity and standards compliance."""
412
413
las = laspy.read(las_file)
414
issues = []
415
416
for i, vlr in enumerate(las.vlrs):
417
# Check user ID length (16 chars max)
418
if len(vlr.user_id) > 16:
419
issues.append(f"VLR {i+1}: User ID too long ({len(vlr.user_id)} > 16)")
420
421
# Check description length (32 chars max)
422
if len(vlr.description) > 32:
423
issues.append(f"VLR {i+1}: Description too long ({len(vlr.description)} > 32)")
424
425
# Check record ID range (0-65535)
426
if not (0 <= vlr.record_id <= 65535):
427
issues.append(f"VLR {i+1}: Record ID out of range ({vlr.record_id})")
428
429
# Check for reserved record IDs
430
if vlr.user_id == "LASF_Projection":
431
reserved_ids = {34735, 34736, 34737, 2112, 4, 0, 1, 2}
432
if vlr.record_id not in reserved_ids:
433
issues.append(f"VLR {i+1}: Non-standard record ID for LASF_Projection ({vlr.record_id})")
434
435
if issues:
436
print("VLR Validation Issues:")
437
for issue in issues:
438
print(f" - {issue}")
439
else:
440
print("All VLRs passed validation")
441
442
return len(issues) == 0
443
444
# Usage
445
inspect_vlrs('sample.las')
446
is_valid = validate_vlrs('sample.las')
447
```
448
449
### VLR Migration and Conversion
450
451
```python
452
import laspy
453
454
def copy_vlrs_between_files(source_file, target_file, vlr_filter=None):
455
"""Copy VLRs from source to target file."""
456
457
source = laspy.read(source_file)
458
target = laspy.read(target_file)
459
460
copied_count = 0
461
462
for vlr in source.vlrs:
463
# Apply filter if provided
464
if vlr_filter and not vlr_filter(vlr):
465
continue
466
467
# Check if VLR already exists in target
468
exists = any(
469
(existing.user_id == vlr.user_id and existing.record_id == vlr.record_id)
470
for existing in target.vlrs
471
)
472
473
if not exists:
474
target.vlrs.append(vlr)
475
copied_count += 1
476
477
target.write(target_file)
478
print(f"Copied {copied_count} VLRs to {target_file}")
479
480
def crs_vlr_filter(vlr):
481
"""Filter for CRS-related VLRs only."""
482
return (vlr.user_id == "LASF_Projection" and
483
vlr.record_id in {34735, 34736, 34737, 2112})
484
485
def custom_vlr_filter(vlr):
486
"""Filter for custom application VLRs only."""
487
return vlr.user_id not in {"LASF_Projection"}
488
489
# Copy only CRS information
490
copy_vlrs_between_files('source.las', 'target.las', crs_vlr_filter)
491
492
# Copy only custom VLRs
493
copy_vlrs_between_files('source.las', 'target.las', custom_vlr_filter)
494
```