0
# Compression Support
1
2
LAZ compression backend management with support for multiple compression libraries and selective field decompression for efficient processing. Laspy provides flexible compression options to optimize file size and processing performance.
3
4
## Capabilities
5
6
### LAZ Backend Management
7
8
Multiple compression backend support with automatic detection and fallback capabilities.
9
10
```python { .api }
11
class LazBackend(Enum):
12
LazrsParallel = 0 # Multi-threaded lazrs backend
13
Lazrs = 1 # Single-threaded lazrs backend
14
Laszip = 2 # LASzip backend
15
16
@classmethod
17
def detect_available(cls) -> Tuple[LazBackend, ...]:
18
"""
19
Detect available compression backends on system.
20
21
Returns:
22
Tuple[LazBackend, ...]: Available backends in priority order
23
"""
24
25
def is_available(self) -> bool:
26
"""
27
Check if this backend is available.
28
29
Returns:
30
bool: True if backend can be used
31
"""
32
33
@property
34
def supports_append(self) -> bool:
35
"""
36
Check if backend supports append operations.
37
38
Returns:
39
bool: True if append is supported
40
"""
41
42
def create_reader(self, source, header, decompression_selection=None):
43
"""Create point reader using this backend."""
44
45
def create_writer(self, dest, header):
46
"""Create point writer using this backend."""
47
48
def create_appender(self, dest, header):
49
"""Create point appender using this backend (if supported)."""
50
```
51
52
**Usage Examples:**
53
54
```python
55
import laspy
56
from laspy import LazBackend
57
58
# Check available backends
59
available = LazBackend.detect_available()
60
print(f"Available backends: {[b.name for b in available]}")
61
62
# Check specific backend availability
63
if LazBackend.LazrsParallel.is_available():
64
print("Multi-threaded lazrs available")
65
backend = LazBackend.LazrsParallel
66
elif LazBackend.Lazrs.is_available():
67
print("Single-threaded lazrs available")
68
backend = LazBackend.Lazrs
69
elif LazBackend.Laszip.is_available():
70
print("LASzip available")
71
backend = LazBackend.Laszip
72
else:
73
print("No LAZ backends available")
74
backend = None
75
76
# Use specific backend for reading
77
if backend:
78
las = laspy.read('compressed.laz', laz_backend=backend)
79
print(f"Read {len(las.points)} points using {backend.name}")
80
81
# Check append support
82
for backend in available:
83
if backend.supports_append:
84
print(f"{backend.name} supports append operations")
85
else:
86
print(f"{backend.name} does not support append operations")
87
```
88
89
### Selective Decompression
90
91
Fine-grained control over which fields to decompress for memory and performance optimization.
92
93
```python { .api }
94
class DecompressionSelection(IntFlag):
95
XY_RETURNS_CHANNEL = ... # X, Y coordinates and return information
96
Z = ... # Z coordinate
97
CLASSIFICATION = ... # Classification field
98
FLAGS = ... # Various flag fields
99
INTENSITY = ... # Intensity values
100
SCAN_ANGLE = ... # Scan angle
101
USER_DATA = ... # User data field
102
POINT_SOURCE_ID = ... # Point source ID
103
GPS_TIME = ... # GPS timestamp (if present)
104
RGB = ... # RGB color information (if present)
105
NIR = ... # Near-infrared (if present)
106
WAVEPACKET = ... # Wavepacket data (if present)
107
ALL_EXTRA_BYTES = ... # All extra byte dimensions
108
109
@classmethod
110
def all(cls) -> DecompressionSelection:
111
"""
112
Select all available fields for decompression.
113
114
Returns:
115
DecompressionSelection: All fields selected
116
"""
117
118
@classmethod
119
def base(cls) -> DecompressionSelection:
120
"""
121
Select base essential fields (XYZ, returns, classification).
122
123
Returns:
124
DecompressionSelection: Essential fields only
125
"""
126
127
@classmethod
128
def xy_returns_channel(cls) -> DecompressionSelection:
129
"""
130
Select only XY coordinates and return information.
131
132
Returns:
133
DecompressionSelection: Minimal coordinate fields
134
"""
135
136
def decompress_xy_returns_channel(self) -> DecompressionSelection:
137
"""Enable XY coordinates and return information decompression."""
138
139
def decompress_z(self) -> DecompressionSelection:
140
"""Enable Z coordinate decompression."""
141
142
def decompress_classification(self) -> DecompressionSelection:
143
"""Enable classification field decompression."""
144
145
def decompress_flags(self) -> DecompressionSelection:
146
"""Enable flag fields decompression."""
147
148
def decompress_intensity(self) -> DecompressionSelection:
149
"""Enable intensity decompression."""
150
151
def decompress_scan_angle(self) -> DecompressionSelection:
152
"""Enable scan angle decompression."""
153
154
def decompress_user_data(self) -> DecompressionSelection:
155
"""Enable user data decompression."""
156
157
def decompress_point_source_id(self) -> DecompressionSelection:
158
"""Enable point source ID decompression."""
159
160
def decompress_gps_time(self) -> DecompressionSelection:
161
"""Enable GPS time decompression."""
162
163
def decompress_rgb(self) -> DecompressionSelection:
164
"""Enable RGB color decompression."""
165
166
def decompress_nir(self) -> DecompressionSelection:
167
"""Enable near-infrared decompression."""
168
169
def decompress_wavepacket(self) -> DecompressionSelection:
170
"""Enable wavepacket data decompression."""
171
172
def decompress_all_extra_bytes(self) -> DecompressionSelection:
173
"""Enable all extra bytes decompression."""
174
175
def skip_xy_returns_channel(self) -> DecompressionSelection:
176
"""Disable XY coordinates and return information decompression."""
177
178
def skip_z(self) -> DecompressionSelection:
179
"""Disable Z coordinate decompression."""
180
181
def skip_classification(self) -> DecompressionSelection:
182
"""Disable classification field decompression."""
183
184
def skip_flags(self) -> DecompressionSelection:
185
"""Disable flag fields decompression."""
186
187
def skip_intensity(self) -> DecompressionSelection:
188
"""Disable intensity decompression."""
189
190
def skip_scan_angle(self) -> DecompressionSelection:
191
"""Disable scan angle decompression."""
192
193
def skip_user_data(self) -> DecompressionSelection:
194
"""Disable user data decompression."""
195
196
def skip_point_source_id(self) -> DecompressionSelection:
197
"""Disable point source ID decompression."""
198
199
def skip_gps_time(self) -> DecompressionSelection:
200
"""Disable GPS time decompression."""
201
202
def skip_rgb(self) -> DecompressionSelection:
203
"""Disable RGB color decompression."""
204
205
def skip_nir(self) -> DecompressionSelection:
206
"""Disable near-infrared decompression."""
207
208
def skip_wavepacket(self) -> DecompressionSelection:
209
"""Disable wavepacket data decompression."""
210
211
def skip_all_extra_bytes(self) -> DecompressionSelection:
212
"""Disable all extra bytes decompression."""
213
214
def is_set_xy_returns_channel(self) -> bool:
215
"""Check if XY coordinates and returns are selected."""
216
217
def is_set_z(self) -> bool:
218
"""Check if Z coordinate is selected."""
219
220
def is_set_classification(self) -> bool:
221
"""Check if classification field is selected."""
222
223
def is_set_flags(self) -> bool:
224
"""Check if flag fields are selected."""
225
226
def is_set_intensity(self) -> bool:
227
"""Check if intensity is selected."""
228
229
def is_set_scan_angle(self) -> bool:
230
"""Check if scan angle is selected."""
231
232
def is_set_user_data(self) -> bool:
233
"""Check if user data is selected."""
234
235
def is_set_point_source_id(self) -> bool:
236
"""Check if point source ID is selected."""
237
238
def is_set_gps_time(self) -> bool:
239
"""Check if GPS time is selected."""
240
241
def is_set_rgb(self) -> bool:
242
"""Check if RGB color is selected."""
243
244
def is_set_nir(self) -> bool:
245
"""Check if near-infrared is selected."""
246
247
def is_set_wavepacket(self) -> bool:
248
"""Check if wavepacket data is selected."""
249
250
def is_set_all_extra_bytes(self) -> bool:
251
"""Check if all extra bytes are selected."""
252
253
def is_set(self, flag) -> bool:
254
"""
255
Check if specific flag is set.
256
257
Parameters:
258
- flag: DecompressionSelection flag to check
259
260
Returns:
261
bool: True if flag is set
262
"""
263
264
def to_lazrs(self):
265
"""Convert to lazrs-specific decompression selection."""
266
267
def to_laszip(self) -> int:
268
"""Convert to LASzip-compatible integer selection."""
269
```
270
271
**Usage Examples:**
272
273
```python
274
import laspy
275
from laspy import DecompressionSelection
276
277
# Read only essential fields to save memory
278
selection = DecompressionSelection.base() # XYZ, returns, classification
279
las = laspy.read('large.laz', decompression_selection=selection)
280
print(f"Loaded {len(las.points)} points with base fields only")
281
282
# Custom field selection for specific analysis
283
selection = (DecompressionSelection.xy_returns_channel()
284
.decompress_z()
285
.decompress_intensity()
286
.decompress_rgb())
287
288
# Skip expensive fields like GPS time and extra bytes
289
las = laspy.read('detailed.laz', decompression_selection=selection)
290
291
# Check what fields are available
292
if selection.is_set_rgb():
293
print("RGB data will be decompressed")
294
# Access RGB data
295
colors = las.points[['red', 'green', 'blue']]
296
297
if not selection.is_set_gps_time():
298
print("GPS time will be skipped (saves memory)")
299
300
# Progressive decompression - start minimal, add fields as needed
301
selection = DecompressionSelection.xy_returns_channel()
302
303
# Read with minimal fields first
304
las = laspy.read('data.laz', decompression_selection=selection)
305
print(f"Initial load: XY coordinates only")
306
307
# If analysis needs Z data, reload with Z
308
if analysis_needs_height:
309
selection = selection.decompress_z()
310
las = laspy.read('data.laz', decompression_selection=selection)
311
print("Reloaded with Z coordinate")
312
313
# For color analysis, add RGB
314
if analysis_needs_color:
315
selection = selection.decompress_rgb()
316
las = laspy.read('data.laz', decompression_selection=selection)
317
print("Reloaded with RGB data")
318
```
319
320
### Point Format Compression Utilities
321
322
Utilities for working with compressed point formats and format conversion.
323
324
```python { .api }
325
def is_point_format_compressed(point_format_id: int) -> bool:
326
"""
327
Check if point format uses compression.
328
329
Parameters:
330
- point_format_id: int - Point format ID to check
331
332
Returns:
333
bool: True if point format is compressed
334
"""
335
336
def compressed_id_to_uncompressed(point_format_id: int) -> int:
337
"""
338
Convert compressed point format ID to uncompressed equivalent.
339
340
Parameters:
341
- point_format_id: int - Compressed point format ID
342
343
Returns:
344
int: Uncompressed point format ID
345
"""
346
347
def uncompressed_id_to_compressed(point_format_id: int) -> int:
348
"""
349
Convert uncompressed point format ID to compressed equivalent.
350
351
Parameters:
352
- point_format_id: int - Uncompressed point format ID
353
354
Returns:
355
int: Compressed point format ID
356
"""
357
```
358
359
**Usage Examples:**
360
361
```python
362
import laspy
363
from laspy.compression import (
364
is_point_format_compressed,
365
compressed_id_to_uncompressed,
366
uncompressed_id_to_compressed
367
)
368
369
# Check point format compression status
370
for fmt_id in range(11): # Point formats 0-10
371
is_compressed = is_point_format_compressed(fmt_id)
372
print(f"Point format {fmt_id}: {'Compressed' if is_compressed else 'Uncompressed'}")
373
374
# Convert between compressed and uncompressed IDs
375
uncompressed_fmt = 3 # Standard point format 3
376
compressed_fmt = uncompressed_id_to_compressed(uncompressed_fmt)
377
print(f"Point format {uncompressed_fmt} compressed equivalent: {compressed_fmt}")
378
379
back_to_uncompressed = compressed_id_to_uncompressed(compressed_fmt)
380
print(f"Point format {compressed_fmt} uncompressed equivalent: {back_to_uncompressed}")
381
382
# Example: Force compression when writing
383
las = laspy.read('input.las')
384
current_fmt = las.header.point_format.id
385
386
if not is_point_format_compressed(current_fmt):
387
# Convert to compressed equivalent for writing
388
compressed_fmt = uncompressed_id_to_compressed(current_fmt)
389
converted = laspy.convert(las, point_format_id=compressed_fmt)
390
converted.write('compressed_output.laz', do_compress=True)
391
print(f"Converted format {current_fmt} to {compressed_fmt} and compressed")
392
```
393
394
## Advanced Compression Usage
395
396
### Backend Performance Comparison
397
398
```python
399
import laspy
400
import time
401
from laspy import LazBackend
402
403
def benchmark_backends(laz_file, chunk_size=100000):
404
"""Compare performance of different LAZ backends."""
405
406
available_backends = LazBackend.detect_available()
407
results = {}
408
409
for backend in available_backends:
410
print(f"Testing {backend.name}...")
411
412
start_time = time.time()
413
total_points = 0
414
415
try:
416
with laspy.open(laz_file, laz_backend=backend) as reader:
417
for chunk in reader.chunk_iterator(chunk_size):
418
total_points += len(chunk)
419
420
elapsed = time.time() - start_time
421
throughput = total_points / elapsed
422
423
results[backend.name] = {
424
'elapsed': elapsed,
425
'points': total_points,
426
'throughput': throughput
427
}
428
429
print(f" {total_points:,} points in {elapsed:.2f}s ({throughput:,.0f} points/sec)")
430
431
except Exception as e:
432
print(f" Failed: {e}")
433
results[backend.name] = {'error': str(e)}
434
435
return results
436
437
# Run benchmark
438
results = benchmark_backends('large_file.laz')
439
440
# Find fastest backend
441
fastest = None
442
best_throughput = 0
443
444
for backend_name, result in results.items():
445
if 'throughput' in result and result['throughput'] > best_throughput:
446
best_throughput = result['throughput']
447
fastest = backend_name
448
449
if fastest:
450
print(f"\nFastest backend: {fastest} ({best_throughput:,.0f} points/sec)")
451
```
452
453
### Memory-Optimized Streaming
454
455
```python
456
import laspy
457
from laspy import DecompressionSelection, LazBackend
458
459
def memory_efficient_processing(laz_file, output_file, processing_func):
460
"""Process large LAZ files with minimal memory usage."""
461
462
# Use most efficient backend
463
backend = LazBackend.detect_available()[0]
464
465
# Minimal field selection
466
selection = DecompressionSelection.base()
467
468
print(f"Processing {laz_file} with {backend.name}")
469
print(f"Decompression selection: base fields only")
470
471
with laspy.open(laz_file, laz_backend=backend, decompression_selection=selection) as reader:
472
header = reader.header.copy()
473
474
with laspy.open(output_file, mode='w', header=header,
475
laz_backend=backend, do_compress=True) as writer:
476
477
total_processed = 0
478
chunk_size = 50000 # Small chunks for low memory usage
479
480
for chunk in reader.chunk_iterator(chunk_size):
481
# Process chunk
482
processed_chunk = processing_func(chunk)
483
484
# Write immediately to minimize memory usage
485
if len(processed_chunk) > 0:
486
writer.write_points(processed_chunk)
487
total_processed += len(processed_chunk)
488
489
# Progress indication
490
if total_processed % 500000 == 0:
491
print(f"Processed {total_processed:,} points")
492
493
print(f"Completed: {total_processed:,} points processed")
494
495
def ground_filter(points):
496
"""Simple ground filtering example."""
497
if len(points) == 0:
498
return points
499
500
# Keep only ground and low vegetation
501
mask = (points.classification == 2) | (points.classification == 3)
502
return points[mask]
503
504
# Use memory-efficient processing
505
memory_efficient_processing('input.laz', 'filtered.laz', ground_filter)
506
```
507
508
### Compression Quality Analysis
509
510
```python
511
import laspy
512
import os
513
from laspy import LazBackend
514
515
def analyze_compression_ratio(input_las, backends=None):
516
"""Analyze compression ratios for different backends."""
517
518
if backends is None:
519
backends = LazBackend.detect_available()
520
521
# Get original file size
522
original_size = os.path.getsize(input_las)
523
print(f"Original LAS file: {original_size:,} bytes")
524
525
las = laspy.read(input_las)
526
527
results = {}
528
529
for backend in backends:
530
try:
531
# Write compressed file with this backend
532
temp_file = f"temp_{backend.name.lower()}.laz"
533
534
with laspy.open(temp_file, mode='w', header=las.header,
535
laz_backend=backend, do_compress=True) as writer:
536
writer.write_points(las.points)
537
538
# Check compressed size
539
compressed_size = os.path.getsize(temp_file)
540
ratio = original_size / compressed_size
541
542
results[backend.name] = {
543
'compressed_size': compressed_size,
544
'compression_ratio': ratio,
545
'space_saved': 1 - (compressed_size / original_size)
546
}
547
548
print(f"{backend.name}:")
549
print(f" Compressed size: {compressed_size:,} bytes")
550
print(f" Compression ratio: {ratio:.1f}:1")
551
print(f" Space saved: {results[backend.name]['space_saved']:.1%}")
552
553
# Clean up temp file
554
os.remove(temp_file)
555
556
except Exception as e:
557
print(f"{backend.name}: Failed - {e}")
558
results[backend.name] = {'error': str(e)}
559
560
return results
561
562
# Analyze different backends
563
compression_results = analyze_compression_ratio('sample.las')
564
565
# Find best compression
566
best_ratio = 0
567
best_backend = None
568
569
for backend_name, result in compression_results.items():
570
if 'compression_ratio' in result and result['compression_ratio'] > best_ratio:
571
best_ratio = result['compression_ratio']
572
best_backend = backend_name
573
574
if best_backend:
575
print(f"\nBest compression: {best_backend} ({best_ratio:.1f}:1)")
576
```
577
578
### Selective Field Processing
579
580
```python
581
import laspy
582
from laspy import DecompressionSelection
583
584
def process_fields_selectively(laz_file):
585
"""Demonstrate selective field processing for different analyses."""
586
587
# Analysis 1: Terrain analysis (needs XYZ + classification)
588
print("Terrain analysis...")
589
selection = (DecompressionSelection.xy_returns_channel()
590
.decompress_z()
591
.decompress_classification())
592
593
with laspy.open(laz_file, decompression_selection=selection) as reader:
594
total_ground = 0
595
for chunk in reader.chunk_iterator(100000):
596
ground_points = chunk[chunk.classification == 2]
597
total_ground += len(ground_points)
598
599
print(f"Found {total_ground:,} ground points")
600
601
# Analysis 2: Intensity analysis (needs XYZ + intensity)
602
print("Intensity analysis...")
603
selection = (DecompressionSelection.base()
604
.decompress_intensity())
605
606
with laspy.open(laz_file, decompression_selection=selection) as reader:
607
intensity_stats = []
608
for chunk in reader.chunk_iterator(100000):
609
if hasattr(chunk, 'intensity'):
610
intensity_stats.extend(chunk.intensity)
611
612
if intensity_stats:
613
import numpy as np
614
mean_intensity = np.mean(intensity_stats)
615
print(f"Mean intensity: {mean_intensity:.1f}")
616
617
# Analysis 3: Color analysis (needs RGB)
618
print("Color analysis...")
619
selection = (DecompressionSelection.xy_returns_channel()
620
.decompress_rgb())
621
622
with laspy.open(laz_file, decompression_selection=selection) as reader:
623
has_color = False
624
for chunk in reader.chunk_iterator(100000):
625
if hasattr(chunk, 'red'):
626
has_color = True
627
break
628
629
if has_color:
630
print("File contains RGB color information")
631
else:
632
print("File does not contain RGB color")
633
634
# Run selective analyses
635
process_fields_selectively('sample.laz')
636
```