LiDAR Processing
LiDAR point cloud processing in WbW-R covers the full pipeline from raw flight-line data through to classified point clouds and derived raster products. All processing tools run in the Whitebox backend; R handles session management, file discovery, and result validation.
Core Concepts
Before processing LiDAR data, understand these foundational terms:
- Return number: Which reflection from a single laser pulse. Pulse 1 is the first (often canopy top); pulse 2–5 capture midstory and ground returns.
- Point classification: ASPRS standard categories — ground (2), low veg (3), medium veg (4), high veg (5), buildings (6), noise (7), overlap (12), and others.
- Intensity: Reflectance value (0–65535) proportional to target brightness. Useful for vegetation density estimation and water detection.
- Ground filtering: Separating terrain points (classification 2) from vegetation and buildings; critical for accurate digital terrain models (DTMs).
- Digital terrain model (DTM): Raster surface of bare earth, computed from ground returns only. Used for hydrology, geomorphometry, and flood modelling.
- Digital surface model (DSM): Raster surface of highest returns (canopy top). Used for building detection and volume calculations.
- Canopy height model (CHM): DSM minus DTM; represents vegetation height above ground. Standard input for tree detection and segmentation.
- Point density: Points per square unit (typically points/m²). Higher density enables finer segmentation; lower density requires smoothing.
- Normalization: Converting raw Z-values to height-above-ground by subtracting DTM, creating a normalized point cloud for structural analysis.
Session Setup and File Discovery
library(whiteboxworkflows)
s <- wbw_session()
setwd('/data/lidar')
# List all LAS/LAZ files in the project folder
las_files <- list.files('.', pattern = '\\.(las|laz)$', full.names = TRUE,
recursive = TRUE)
cat('Found', length(las_files), 'point cloud files.\n')
Reading and Inspecting LiDAR Files
las <- wbw_read_lidar('survey.las')
meta <- las$metadata()
cat('Point count:', meta$number_of_points, '\n')
cat('X range:', meta$min_x, 'to', meta$max_x, '\n')
cat('Y range:', meta$min_y, 'to', meta$max_y, '\n')
cat('Z range:', meta$min_z, 'to', meta$max_z, '\n')
cat('Point density per m²:', meta$point_density, '\n')
Point Cloud Filtering and Outlier Removal
# Remove isolated low and high points
wbw_lidar_elevation_slice(i = 'survey.las',
output = 'survey_sliced.las',
minz = 0.0,
maxz = 200.0,
cls = FALSE)
# Statistical outlier removal
wbw_lidar_remove_outliers(i = 'survey.las',
output = 'survey_clean.las',
radius = 2.0,
elev_diff = 25.0,
use_median = FALSE)
Ground Point Classification
Progressive Morphological Filter
wbw_lidar_ground_point_filter(i = 'survey_clean.las',
output = 'survey_classified.las',
radius = 2.0,
min_elev_diff = 0.15,
max_elev_diff = 1.3,
num_iter = 5,
threshold = 0.15,
slope_threshold = 0.15,
height_threshold = 1.0,
classify = TRUE,
slope = FALSE,
height_diff = TRUE,
filter_return_all = FALSE)
Digital Elevation Model Interpolation
Tin Gridding (TIN Interpolation)
wbw_tin_gridding(i = 'survey_classified.las',
output = 'dtm.tif',
returns = 'last',
resolution = 1.0,
exclude_cls = '1,3,4,5,6,7',
minz = -50.0,
maxz = 250.0)
LiDAR IDW Interpolation
wbw_lidar_idw_interpolation(i = 'survey_classified.las',
output = 'dtm_idw.tif',
parameter = 'elevation',
returns = 'last',
resolution = 1.0,
weight = 1.0,
radius = 2.5,
exclude_cls = '1,3,4,5,6,7')
Normalised Height Above Ground
wbw_normalize_lidar(i = 'survey_classified.las',
ground = 'survey_classified.las',
output = 'survey_normalised.las',
ignore_ground_distance = FALSE)
Canopy Height Model (CHM) and DSM
# First return DSM
wbw_lidar_idw_interpolation(i = 'survey_classified.las',
output = 'dsm.tif',
parameter = 'elevation',
returns = 'first',
resolution = 1.0,
weight = 1.0,
radius = 2.5,
exclude_cls = '7')
# Canopy Height Model = DSM - DTM
wbw_subtract(input1 = 'dsm.tif',
input2 = 'dtm.tif',
output = 'chm.tif')
Point Density and Distribution Analysis
wbw_lidar_point_stats(i = 'survey.las',
resolution = 1.0,
num_points = TRUE,
num_pulses = FALSE)
wbw_lidar_density(i = 'survey.las',
output = 'density.tif',
resolution = 1.0,
returns = 'all',
exclude_cls = '7')
Scan-Angle and Return Analysis
# Filter by scan angle
wbw_filter_lidar_scan_angles(i = 'survey.las',
output = 'survey_nadir.las',
threshold = 15.0)
# Intensity image
wbw_lidar_idw_interpolation(i = 'survey_nadir.las',
output = 'intensity.tif',
parameter = 'intensity',
returns = 'all',
resolution = 1.0,
weight = 1.0,
radius = 2.5)
Tile Management
Working with large surveys requires tiling:
# Tile large dataset
wbw_lidar_tile(i = 'full_survey.las',
width = 500.0,
height = 500.0,
origin_x = 0.0,
origin_y = 0.0)
# Add a buffer overlap to each tile
wbw_lidar_tile_footprint(i = 'tile_000_000.las',
output = 'tile_000_000_footprint.shp',
hull = FALSE)
Segmentation and Vegetation Analysis
# Individual tree segmentation from normalised LiDAR
wbw_individual_tree_detection(i = 'survey_normalised.las',
output = 'tree_tops.shp',
min_search_radius = 1.0,
min_height = 2.0,
max_search_radius = 5.0,
max_height = 40.0)
# Canopy cover (fraction first return above height threshold)
wbw_lidar_segmentation_based_filter(i = 'survey_classified.las',
output = 'survey_seg_filtered.las',
slope_threshold = 15.0,
max_edge_length = 0.5,
classify = TRUE)
WbW-Pro Spotlight: LiDAR Change and Disturbance Analysis
- Problem: Compare repeat LiDAR epochs to detect disturbance in a repeatable way.
- Tool:
lidar_change_and_disturbance_analysis - Typical inputs: Baseline tile set, monitoring tile set, output resolution, minimum change threshold.
- Typical outputs: Change rasters plus summary metrics for affected area, hotspot intensity, and QA review.
result <- s$lidar_change_and_disturbance_analysis(
baseline_tiles = '/data/lidar_epoch_2022/',
monitor_tiles = '/data/lidar_epoch_2025/',
resolution = 1.0,
min_change_m = 0.5,
output_prefix = 'disturbance_2025_vs_2022'
)
print(result)
Note: This workflow requires a session initialized with a valid Pro licence.
Complete LiDAR Processing Workflow
library(whiteboxworkflows)
s <- wbw_session()
setwd('/data/lidar_project')
las_file <- 'raw_survey.las'
# 1. Remove outliers
wbw_lidar_remove_outliers(i = las_file, output = 'clean.las', radius = 2.0, elev_diff = 25.0)
# 2. Ground classification
wbw_lidar_ground_point_filter(i = 'clean.las', output = 'classified.las', radius = 2.0,
min_elev_diff = 0.15, max_elev_diff = 1.3, num_iter = 5,
threshold = 0.15, slope_threshold = 0.15, height_threshold = 1.0,
classify = TRUE, slope = FALSE, height_diff = TRUE,
filter_return_all = FALSE)
# 3. DTM
wbw_tin_gridding(i = 'classified.las', output = 'dtm.tif', returns = 'last',
resolution = 1.0, exclude_cls = '1,3,4,5,6,7')
# 4. DSM and CHM
wbw_lidar_idw_interpolation(i = 'classified.las', output = 'dsm.tif', parameter = 'elevation',
returns = 'first', resolution = 1.0, weight = 1.0, radius = 2.5,
exclude_cls = '7')
wbw_subtract(input1 = 'dsm.tif', input2 = 'dtm.tif', output = 'chm.tif')
# 5. Normalise for vegetation analysis
wbw_normalize_lidar(i = 'classified.las', ground = 'classified.las',
output = 'normalised.las', ignore_ground_distance = FALSE)
# 6. Tree detection
wbw_individual_tree_detection(i = 'normalised.las', output = 'tree_tops.shp',
min_search_radius = 1.0, min_height = 2.0,
max_search_radius = 5.0, max_height = 40.0)
# 7. Point density
wbw_lidar_density(i = 'classified.las', output = 'density.tif',
resolution = 1.0, returns = 'all', exclude_cls = '7')
cat('LiDAR processing complete.\n')
Tips
- Choose your format wisely: LAS is universal and compact; LAZ adds compression and is ideal for archival or transmission. COPC is cloud-optimized and best for remote HTTP range-request access. Use LAZ or LAS for terrestrial/airborne surveys; COPC for cloud-native workflows.
- Always validate classifications: Use
lidar_histogram()andlidar_info()to inspect point distributions by return and classification. Misclassified ground points silently corrupt DTMs and downstream hydrology. - DTM vs. DSM vs. CHM: Generate all three at the same resolution so derivatives align. A common pitfall is mixing DTM and DSM resolution.
- Ground filtering is critical: Outliers and noise (classification 7) should be excluded before gridding. Use
lidar_filter_for_ground()to remove spikes and erratic points. - Normalization enables vegetation analysis: Always normalize point clouds (subtract DTM) before individual tree detection or canopy structural metrics.
- Monitor memory for large surveys: Point clouds are memory-intensive. For datasets > 1 GB, use streaming APIs (
lidar_read_chunked()) rather than loading the entire tile at once. - Coordinate reference systems matter: LAS headers carry CRS as WKT. Verify WKT matches your project CRS before gridding or merging tiles.
- Density and grid resolution: If point density is < 0.5 pts/m², consider upsampling or smoothing the output grid to avoid isolated pits or peaks.