Remote Sensing Analysis
Remote sensing workflows in WbW-R cover multispectral and hyperspectral image analysis: spectral index computation, image enhancement, principal component analysis, image segmentation, unsupervised and supervised classification, accuracy assessment, and change detection. All computation runs in the Whitebox backend.
Sensor Bundle First: When working with Sentinel-2, Landsat, PlanetScope, or SAR product folders, open the scene as a sensor bundle using
wbw_sensor_bundle_from_path(). Bundles provide automatic metadata discovery, key-based band access, and one-call true/false-colour composites without hardcoding file names.
Core Concepts
Remote sensing image analysis requires familiarity with these core ideas:
- Spectral bands: Distinct wavelength ranges (e.g. blue 450–510 nm, red 620–750 nm, NIR 750–900 nm). Different materials reflect different bands, enabling material discrimination.
- Spectral indices: Normalized ratios of bands that isolate phenomena. NDVI (Normalized Difference Vegetation Index) uses NIR and red to measure greenness; NDWI uses NIR and SWIR for water content.
- Spatial resolution: Pixel size in meters (Sentinel-2: 10 m for visible/NIR, 20 m for SWIR; Landsat: 30 m; SAR: 5–30 m depending on mode).
- Temporal resolution: Revisit interval (Sentinel-2: 5 days; Landsat: 16 days; PlanetScope: daily or higher).
- Atmospheric effects: Raw radiance is affected by aerosols, water vapour, and ozone. Surface reflectance products are atmospherically corrected; still require relative normalization for multi-date analysis.
- Cloud masking: Cloud and cloud shadow pixels must be identified and excluded before classification or change detection using quality/QA bands.
- Supervised classification: Training polygons with known labels teach a model to assign classes to unlabelled pixels. Training quality directly controls classification accuracy.
- Unsupervised classification: Clustering algorithms (K-means, ISODATA) discover spectral clusters without training labels; useful for exploratory analysis.
- Change detection: Comparing indices or classifications across dates to identify land-use changes, disturbance, or phenological shifts.
Session Setup
library(whiteboxworkflows)
s <- wbw_session()
setwd('/data/remote_sensing')
Sensor Bundles — Cross-Family Scene Ingestion
wbw_sensor_bundle_from_path() wraps a product folder and returns a bundle object with a consistent interface across sensor families.
Opening a Bundle
# Works with Sentinel-2, Landsat 8/9, PlanetScope, SPOT, SAR, etc.
bundle <- wbw_sensor_bundle_from_path(
'/data/S2B_MSIL2A_20240601T105619_N0510_R094_T32TPT.SAFE',
session = s
)
# Or from a zip archive — WbW extracts it automatically
bundle <- wbw_sensor_bundle_from_path('/data/LC09_L2SP_017030_20240610.tar', session = s)
Discovering Available Layers
meta <- bundle$metadata()
cat('Family: ', meta$family, '\n')
cat('Mission: ', meta$mission, '\n')
cat('Tile: ', meta$tile_id, '\n')
cat('Level: ', meta$processing_level, '\n')
cat('Cloud %: ', meta$cloud_cover_percent, '\n')
cat('Datetime: ', meta$acquisition_datetime_utc, '\n')
print(bundle$list_band_keys()) # e.g. c('B02', 'B03', 'B04', 'B08', ...)
print(bundle$list_measurement_keys()) # e.g. c('ndvi', 'ndwi')
print(bundle$list_qa_keys()) # e.g. c('SCL', 'QA_PIXEL')
print(bundle$list_aux_keys()) # e.g. c('AOT', 'WVP')
print(bundle$list_asset_keys())
Reading Bands by Key
# Keys follow sensor-native naming — no need to memorise file paths
blue <- bundle$read_band('B02') # Sentinel-2 blue
green <- bundle$read_band('B03')
red <- bundle$read_band('B04')
nir <- bundle$read_band('B08')
swir1 <- bundle$read_band('B11')
swir2 <- bundle$read_band('B12')
# Landsat 9 uses different key names
# blue <- bundle$read_band('SR_B2')
# nir <- bundle$read_band('SR_B5')
# QA / cloud mask
scl <- bundle$read_qa_layer('SCL') # Sentinel-2 scene classification
Quick-Look Composites
# True-colour GeoTIFF (auto-enhanced by default)
bundle$write_true_colour(output_path = 'true_colour.tif')
# False-colour (NIR-Red-Green) composite
bundle$write_false_colour(output_path = 'false_colour.tif')
Cross-Family NDVI Pattern
compute_ndvi_from_bundle <- function(bundle_path, output_path, session = NULL) {
b <- wbw_sensor_bundle_from_path(bundle_path, session = session)
meta <- b$metadata()
if ('ndvi' %in% b$list_measurement_keys()) {
ndvi_r <- b$read_measurement('ndvi')
wbw_write_raster(ndvi_r, output_path)
} else {
# Fall back to band-based NDVI
if (meta$family == 'sentinel2') {
red_r <- b$read_band('B04')
nir_r <- b$read_band('B08')
} else {
red_r <- b$read_band('SR_B4')
nir_r <- b$read_band('SR_B5')
}
wbw_normalized_difference_index(input1 = nir_r$file_path(), input2 = red_r$file_path(),
output = output_path)
}
}
compute_ndvi_from_bundle('/data/S2B_MSIL2A_20240601.SAFE', 'ndvi_s2.tif', s)
compute_ndvi_from_bundle('/data/LC09_L2SP_017030.tar', 'ndvi_l9.tif', s)
Cloud and No-Data Masking
# Sentinel-2 SCL: 3=cloud shadow, 8=med cloud, 9=high cloud, 10=thin cirrus
scl <- bundle$read_qa_layer('SCL')
wbw_raster_calculator(output = 'red_cloud_free.tif',
statement = paste0("if('", scl$file_path(), "' == 3 or '", scl$file_path(), "' == 8 ",
"or '", scl$file_path(), "' == 9 or '", scl$file_path(), "' == 10, ",
"nodata, '", red$file_path(), "')"))
# Landsat Collection 2 QA_PIXEL — use bitwise expressions via raster_calculator
# qa <- bundle$read_qa_layer('QA_PIXEL')
Reading Multi-Band Imagery (Individual Files)
When bundles are not available (e.g., reprojected mosaics, custom composites, legacy archives), load individual band files in the conventional way:
b2 <- wbw_read_raster('LC08_B2_blue.tif')
b3 <- wbw_read_raster('LC08_B3_green.tif')
b4 <- wbw_read_raster('LC08_B4_red.tif')
b5 <- wbw_read_raster('LC08_B5_nir.tif')
b6 <- wbw_read_raster('LC08_B6_swir1.tif')
b7 <- wbw_read_raster('LC08_B7_swir2.tif')
# Resample if bands have different resolutions
wbw_resample(inputs = b6$file_path(),
output = 'b6_10m.tif',
cell_size = 0.0,
base = b4$file_path(),
method = 'bilinear')
Spectral Indices
Normalised Difference Vegetation Index (NDVI)
$$NDVI = \frac{NIR - Red}{NIR + Red}$$
# Using bands read from a bundle
wbw_normalized_difference_index(input1 = nir$file_path(), # NIR
input2 = red$file_path(), # Red
output = 'ndvi.tif')
Common Water, Snow, and Urban Indices
# Define index triplets: (output name, numerator band, denominator band)
indices <- list(
list(name = 'ndwi', b1 = green, b2 = nir), # open water; Green/NIR
list(name = 'mndwi', b1 = green, b2 = swir1), # urban water; Green/SWIR1
list(name = 'nbr', b1 = nir, b2 = swir2), # fire severity; NIR/SWIR2
list(name = 'ndsi', b1 = green, b2 = swir1), # snow/ice; Green/SWIR1
list(name = 'ndbi', b1 = swir1, b2 = nir) # built-up; SWIR1/NIR
)
for (idx in indices) {
wbw_normalized_difference_index(input1 = idx$b1$file_path(),
input2 = idx$b2$file_path(),
output = paste0(idx$name, '.tif'))
}
EVI (Enhanced Vegetation Index)
$$EVI = 2.5 \cdot \frac{NIR - Red}{NIR + 6 \cdot Red - 7.5 \cdot Blue + 1}$$
wbw_raster_calculator(output = 'evi.tif',
statement = paste0("2.5 * ('", nir$file_path(), "' - '", red$file_path(), "') / (",
"'", nir$file_path(), "' + 6.0 * '", red$file_path(), "' - 7.5 * '",
blue$file_path(), "' + 1.0)"))
Image Enhancement
Percentage Linear Stretch
wbw_percentage_contrast_stretch(i = b4$file_path(),
output = 'b4_stretched.tif',
clip = 1.0,
tail = 'both',
num_tones = 256)
Standard Deviation Stretch
wbw_standard_deviation_contrast_stretch(i = b4$file_path(),
output = 'b4_sd_stretch.tif',
stdev = 2.0,
num_tones = 256)
Gamma Correction
wbw_gamma_correction(i = b4$file_path(),
output = 'b4_gamma.tif',
gamma = 0.55)
Histogram Matching
wbw_histogram_matching(i = 'image_target.tif',
histo_file = 'reference_histogram.html',
output = 'image_matched.tif')
IHS Colour Space and Pan-Sharpening
wbw_rgb_to_ihs(intensity = b4$file_path(),
hue = b3$file_path(),
saturation = b2$file_path(),
output = 'ihs.tif')
# Pan-sharpen
wbw_ihs_to_rgb(intensity = 'pan_band.tif',
hue = 'ihs_hue.tif',
saturation = 'ihs_sat.tif',
output = 'pansharpened.tif')
Spatial Filtering
# Gaussian smoothing
wbw_gaussian_filter(i = b5$file_path(), output = 'b5_gauss.tif', sigma = 2.0)
# Edge detection — Canny
wbw_canny_edge_detection(i = b4$file_path(), output = 'edges.tif', sigma = 0.5,
low_threshold = 0.05, high_threshold = 0.15, add_back = FALSE)
# General-purpose GLCM texture (multiband output)
wbw_glcm_texture(input = b5$file_path(),
window_size = 9,
distance = 1,
angles = '0,45,90,135',
features = 'contrast,homogeneity,entropy',
direction_aggregation = 'mean',
levels = 32,
output = 'glcm_texture.tif')
Principal Component Analysis
# Supply a comma-separated list of band files
all_bands <- paste(c(b2$file_path(), b3$file_path(), b4$file_path(),
b5$file_path(), b6$file_path(), b7$file_path()),
collapse = ';')
wbw_principal_component_analysis(inputs = all_bands,
output = 'pca_loadings.html',
num_comp = 6,
standardised = FALSE)
# PC scores are written as pc1.tif, pc2.tif, ... in the working directory
Image Segmentation
wbw_image_segmentation(inputs = all_bands,
output = 'segments.tif',
threshold = 30.0,
steps = 10,
min_area = 10)
Object-Based Image Analysis (OBIA) Baseline
Phase 1 open-core OBIA tools are available through standard wbw_<tool>(...)
wrappers and can be discovered as a grouped set:
obia_tools <- wbw_tools_in_remote_sensing_obia(session = s)
print(obia_tools$id)
# 1) Segment imagery into compact object candidates
wbw_segment_slic_superpixels(inputs = all_bands,
region_size = 18,
compactness = 12.0,
output = 'segments_slic.tif')
# 2) Merge undersized regions
wbw_segments_merge_small_regions(segments = 'segments_slic.tif',
min_size = 12,
method = 'longest',
output = 'segments_clean.tif')
# 3) Extract spectral/shape/texture object features
wbw_object_features_spectral_basic(segments = 'segments_clean.tif',
inputs = all_bands,
output = 'object_features_spectral.csv')
wbw_object_features_shape_basic(segments = 'segments_clean.tif',
output = 'object_features_shape.csv')
wbw_object_features_texture_glcm_basic(segments = 'segments_clean.tif',
input = b5$file_path(),
levels = 16,
output = 'object_features_texture.csv')
# 4) Train/apply object-level RF model
wbw_classify_objects_random_forest(features = 'object_features_all.csv',
training = 'training_segments.csv',
output = 'object_predictions.csv')
# 5) Evaluate object-level accuracy
wbw_evaluate_object_classification_accuracy(predictions = 'object_predictions.csv',
reference = 'validation_segments.csv',
output = 'object_accuracy.json')
# Optional one-call baseline pipeline
wbw_obia_pipeline_basic(inputs = all_bands,
training = 'training_segments.csv',
output_prefix = 'obia_field01',
segment_method = 'slic')
Advanced OBIA Capabilities (Open Tier)
The OBIA surface now includes advanced open-tier tools. You can list them from
the grouped discovery helper and run each directly with wbw_<tool>(...) wrappers
(or with wbw_run_tool(...) for dynamic tool-id dispatch).
Segmentation and scale control:
segment_watershed_markerssegment_multiresolution_hierarchicalsegment_scale_parameter_optimizersegments_split_low_cohesion
Object conversion and interoperability:
segments_to_polygonspolygons_to_segments
Advanced features:
object_features_context_neighborsobject_features_topology_relations
Advanced classification:
classify_objects_svmclassify_objects_ensemble_proclassify_objects_rules_basicclassify_objects_rules_hierarchicalobject_class_probability_mapsobject_uncertainty_diagnostics_pro
Hierarchy and propagation:
build_object_hierarchy_multiscalepropagate_labels_across_hierarchy
Post-processing and quality:
objects_enforce_min_mapping_unitobjects_boundary_refinement_proevaluate_segmentation_quality_pro
Workflow operations:
obia_batch_orchestrator_proobia_audit_report_pro
# 1) Build multiscale hierarchy products
wbw_segment_multiresolution_hierarchical(inputs = all_bands,
coarse_k = 900.0,
fine_k = 280.0,
output_prefix = 'site01_hier')
# 2) Context and topology features for difficult class boundaries
wbw_object_features_context_neighbors(segments = 'site01_hier_segments_fine.tif',
output = 'site01_context.csv')
wbw_object_features_topology_relations(segments = 'site01_hier_segments_fine.tif',
output = 'site01_topology.csv')
# 3) Ensemble and rule-hierarchical object classification
wbw_classify_objects_ensemble_pro(features = 'site01_features_all.csv',
training = 'site01_training_segments.csv',
output = 'site01_pred_ensemble.csv')
wbw_classify_objects_rules_hierarchical(features = 'site01_features_all.csv',
rules = 'site01_rules.csv',
output = 'site01_pred_rules.csv')
# 4) Probability maps and uncertainty diagnostics
wbw_object_class_probability_maps(predictions = 'site01_pred_ensemble.csv',
output = 'site01_probabilities.csv')
wbw_object_uncertainty_diagnostics_pro(probabilities = 'site01_probabilities.csv',
low_conf_threshold = 0.7,
output = 'site01_uncertainty.json')
# Batch orchestration for multi-scene runs
wbw_obia_batch_orchestrator_pro(jobs = list(
list(
inputs = list('s1_red.tif', 's1_green.tif', 's1_nir.tif'),
training = 's1_training.csv',
output_prefix = 'prod/s1',
segment_method = 'graph'
),
list(
inputs = list('s2_red.tif', 's2_green.tif', 's2_nir.tif'),
training = 's2_training.csv',
output_prefix = 'prod/s2',
segment_method = 'slic'
)
),
output = 'prod/obia_batch_report.json')
wbw_obia_audit_report_pro(artifacts = list(
'prod/s1_object_predictions.csv',
'prod/s2_object_predictions.csv',
'prod/obia_batch_report.json'
),
output = 'prod/obia_audit.json')
Use segments_to_polygons and polygons_to_segments when analysts need to
edit object boundaries in vector format and then return those objects back into
raster-segment workflows.
Unsupervised Classification
KMeans
wbw_modified_k_means_clustering(inputs = all_bands,
output = 'kmeans.tif',
out_html = 'kmeans_report.html',
start_clusters = 5,
end_clusters = 10,
max_iterations = 25,
class_change = 2.0)
DBSCAN
wbw_dbscan(inputs = all_bands,
output = 'dbscan.tif',
search_dist = 2.5,
min_points = 10)
Supervised Classification
Random Forest
# 1. Fit the model
wbw_random_forest_classification_fit(inputs = all_bands,
training = 'training_polygons.shp',
field = 'CLASS_ID',
output = 'rf_model.bin',
n_trees = 100,
min_leaf_size = 1,
max_features = 0.0)
# 2. Apply the model
wbw_random_forest_classification_predict(inputs = all_bands,
model = 'rf_model.bin',
output = 'rf_classification.tif')
Support Vector Machine
wbw_svm_classification(inputs = all_bands,
training = 'training_polygons.shp',
field = 'CLASS_ID',
output = 'svm_classification.tif',
c = 200.0,
gamma = 50.0,
cost = 10.0,
tolerance = 0.1,
test_proportion = 0.2)
K-Nearest Neighbours
wbw_knn_classification(inputs = all_bands,
training = 'training_polygons.shp',
field = 'CLASS_ID',
output = 'knn_classification.tif',
k = 5,
scaling = TRUE,
test_proportion = 0.2)
Accuracy Assessment
wbw_kappa_index(i1 = 'rf_classification.tif',
i2 = 'reference_classification.tif',
output = 'accuracy_report.html')
Change Detection
Image Differencing
wbw_change_vector_analysis(date1 = paste(c('t1_b2.tif','t1_b3.tif','t1_b4.tif','t1_b5.tif'), collapse=';'),
date2 = paste(c('t2_b2.tif','t2_b3.tif','t2_b4.tif','t2_b5.tif'), collapse=';'),
magnitude = 'cva_magnitude.tif',
direction = 'cva_direction.tif')
Additional Change, Thermal, and Spectral Sprint Tools
The newest remote sensing tools are available through session tool execution, which is useful while dedicated R helper wrappers are still catching up:
# Image difference change detection
img_diff <- s$run_tool('image_difference_change_detection', list(
t1_inputs = c('t1_b2.tif', 't1_b3.tif', 't1_b4.tif'),
t2_inputs = c('t2_b2.tif', 't2_b3.tif', 't2_b4.tif'),
mode = 'magnitude',
threshold_sigma = 2.0,
output = 'img_diff_mag.tif',
output_signed = 'img_diff_signed.tif',
output_mask = 'img_diff_mask.tif'
))
# Post-classification change with class remapping
post_class <- s$run_tool('post_classification_change', list(
t1_classified = 'lulc_t1.tif',
t2_classified = 'lulc_t2.tif',
transition_scale = 1000,
t1_class_remap = list('11' = 1, '12' = 1),
t2_class_remap = list('41' = 4, '42' = 4),
output = 'post_class_transition.tif'
))
# PCA-based change detection
pca_change <- s$run_tool('pca_based_change_detection', list(
t1_inputs = c('t1_b2.tif', 't1_b3.tif', 't1_b4.tif'),
t2_inputs = c('t2_b2.tif', 't2_b3.tif', 't2_b4.tif'),
component = 1,
threshold_sigma = 2.0,
output = 'pca_change_pc1.tif',
output_mask = 'pca_change_mask.tif',
output_report = 'pca_change_report.json'
))
# Dark object subtraction and DN->TOA reflectance
dos <- s$run_tool('dark_object_subtraction', list(
inputs = c('B02.tif', 'B03.tif', 'B04.tif', 'B08.tif'),
percentile = 1.0,
output = 'dos_stack.tif',
output_diagnostic_offsets = 'dos_offsets.tif'
))
toa <- s$run_tool('dn_to_toa_reflectance', list(
inputs = c('B02.tif', 'B03.tif', 'B04.tif', 'B08.tif'),
sensor_bundle_root = '/data/LC09_L1TP_017030_20240420_20240426_02_T1',
output = 'toa_stack.tif'
))
# Thermal emissivity and LST
emiss <- s$run_tool('ndvi_based_emissivity', list(
red_input = 'B04.tif',
nir_input = 'B08.tif',
output = 'emissivity.tif'
))
lst_sc <- s$run_tool('land_surface_temperature_single_channel', list(
thermal_input = 'B10.tif',
sensor_bundle_root = '/data/LC09_L1TP_017030_20240420_20240426_02_T1',
output = 'lst_single_channel.tif'
))
lst_sw <- s$run_tool('land_surface_temperature_split_window', list(
thermal1_input = 'B10.tif',
thermal2_input = 'B11.tif',
emissivity_mean_constant = 0.98,
emissivity_delta_constant = 0.0,
output = 'lst_split_window.tif'
))
# Spectral analytics
sam <- s$run_tool('spectral_angle_mapper', list(
inputs = c('B02.tif', 'B03.tif', 'B04.tif', 'B08.tif'),
endmembers = list(
list(name = 'water', values = c(0.03, 0.02, 0.01, 0.00)),
list(name = 'veg', values = c(0.05, 0.10, 0.06, 0.40))
),
output = 'sam_classes.tif',
output_angle = 'sam_angle.tif'
))
cont <- s$run_tool('continuum_removal', list(
inputs = c('hyp_b1.tif', 'hyp_b2.tif', 'hyp_b3.tif'),
wavelengths = c(450.0, 550.0, 650.0),
output = 'continuum_removed.tif'
))
unmix <- s$run_tool('linear_spectral_unmixing', list(
inputs = c('B02.tif', 'B03.tif', 'B04.tif', 'B08.tif'),
endmembers = list(
list(name = 'soil', values = c(0.18, 0.20, 0.22, 0.24)),
list(name = 'veg', values = c(0.05, 0.09, 0.06, 0.40))
),
output = 'unmix_frac.tif',
output_residual = 'unmix_residual.tif'
))
mnf <- s$run_tool('minimum_noise_fraction', list(
inputs = c('B02.tif', 'B03.tif', 'B04.tif', 'B08.tif'),
num_components = 3,
output = 'mnf_components.tif',
output_inverse = 'mnf_inverse.tif'
))
lib_match <- s$run_tool('spectral_library_matching', list(
inputs = c('B02.tif', 'B03.tif', 'B04.tif', 'B08.tif'),
metric = 'sam',
library = list(
list(name = 'water', values = c(0.03, 0.02, 0.01, 0.00)),
list(name = 'soil', values = c(0.18, 0.20, 0.22, 0.24))
),
output = 'lib_class.tif',
output_score = 'lib_score.tif'
))
# PolSAR decomposition tools
cp <- s$run_tool('cloude_pottier_decomposition', list(
inputs = c('t11.tif', 't22.tif', 't33.tif'),
matrix_format = 'diag3',
output = 'cloude_pottier_haa.tif'
))
fd <- s$run_tool('freeman_durden_decomposition', list(
inputs = c('c11.tif', 'c22.tif', 'c33.tif'),
matrix_format = 'diag3',
output = 'freeman_durden.tif',
output_clip_mask = 'freeman_clip.tif'
))
WbW-Pro Spotlight: In-Season Crop Stress Intervention Planning
- Problem: Turn in-season crop stress signals into actionable intervention priorities.
- Tool:
in_season_crop_stress_intervention_planning - Typical inputs: NDVI raster, canopy-temperature raster, soil-moisture raster.
- Typical outputs: Intervention-priority and intervention-class products with summary reporting.
result <- s$in_season_crop_stress_intervention_planning(
ndvi = 'ndvi_latest.tif',
canopy_temperature = 'lst_latest.tif',
soil_moisture = 'soil_moisture_latest.tif',
output_prefix = 'field_07_stress'
)
print(result)
Note: This workflow requires a session initialized with a valid Pro licence.
Complete Remote Sensing Workflow
The following end-to-end example uses a sensor bundle for ingestion and random forest for land-cover mapping:
library(whiteboxworkflows)
s <- wbw_session()
# 1. Open scene as a bundle — works with S2, Landsat 9, PlanetScope, etc.
bundle <- wbw_sensor_bundle_from_path(
'/data/S2B_MSIL2A_20240601T105619_N0510_R094_T32TPT.SAFE',
session = s
)
meta <- bundle$metadata()
cat(sprintf('Family: %s, tile: %s, clouds: %.1f%%\n',
meta$family, meta$tile_id, meta$cloud_cover_percent))
if (meta$cloud_cover_percent > 20) {
stop('Too cloudy for classification')
}
# 2. Read bands by key (family-agnostic)
blue <- bundle$read_band('B02')
green <- bundle$read_band('B03')
red <- bundle$read_band('B04')
nir <- bundle$read_band('B08')
wbw_resample(inputs = bundle$read_band('B11')$file_path(), output = 'swir1_10m.tif',
cell_size = 0.0, base = red$file_path(), method = 'bilinear')
wbw_resample(inputs = bundle$read_band('B12')$file_path(), output = 'swir2_10m.tif',
cell_size = 0.0, base = red$file_path(), method = 'bilinear')
swir1 <- wbw_read_raster('swir1_10m.tif')
swir2 <- wbw_read_raster('swir2_10m.tif')
bands <- list(blue, green, red, nir, swir1, swir2)
band_paths <- paste(sapply(bands, function(b) b$file_path()), collapse = ';')
# 3. Mask clouds using SCL
scl <- bundle$read_qa_layer('SCL')
# 4. Spectral indices
wbw_normalized_difference_index(input1 = nir$file_path(), input2 = red$file_path(),
output = 'ndvi.tif')
wbw_normalized_difference_index(input1 = green$file_path(), input2 = swir1$file_path(),
output = 'mndwi.tif')
wbw_normalized_difference_index(input1 = nir$file_path(), input2 = swir2$file_path(),
output = 'nbr.tif')
ndvi <- wbw_read_raster('ndvi.tif')
mndwi <- wbw_read_raster('mndwi.tif')
nbr <- wbw_read_raster('nbr.tif')
# 5. PCA decorrelation
wbw_principal_component_analysis(inputs = band_paths,
output = 'pca_loadings.html',
num_comp = 4,
standardised = TRUE)
pc_paths <- paste(paste0('pc', 1:4, '.tif'), collapse = ';')
# 6. Feature stack = bands + indices + PCA scores
feature_paths <- paste(c(band_paths, 'ndvi.tif', 'mndwi.tif', 'nbr.tif', pc_paths),
collapse = ';')
# 7. Train random forest
wbw_random_forest_classification_fit(inputs = feature_paths,
training = 'training_polygons.shp',
field = 'CLASS',
output = 'rf_model.bin',
n_trees = 500,
test_proportion = 0.25)
# 8. Predict
wbw_random_forest_classification_predict(inputs = feature_paths,
model = 'rf_model.bin',
output = 'lulc.tif')
# 9. Generalise
wbw_generalize_classified_raster(i = 'lulc.tif', output = 'lulc_clean.tif', min_size = 9)
# 10. Accuracy assessment
wbw_kappa_index(i1 = 'lulc_clean.tif', i2 = 'validation_reference.tif',
output = 'accuracy.html')
# 11. Quick-look composite for QC
bundle$write_true_colour(output_path = 'quicklook_truecolour.tif')
cat('Remote sensing workflow complete.\n')
Tips
- Use bundles for scene-level ingestion:
wbw_sensor_bundle_from_path()eliminates hardcoded band filenames and works identically across Sentinel-2, Landsat, PlanetScope, and other supported families. - Screen cloud cover before processing: Check bundle metadata
cloud_cover_percentbefore loading all bands to skip unusable scenes early in a batch workflow. - Atmospheric correction is iterative: Raw digital numbers and even surface reflectance products may still carry illumination and atmospheric artefacts. Histogram matching across scenes is a simple relative normalisation that works well when no reference spectrum is available.
- Align band resolution before stacking: Sentinel-2 delivers 10 m (Blue, Green, Red, NIR) and 20 m (Red Edge, SWIR) bands. Upscale the 20 m bands to 10 m using bilinear resampling before combining them in a feature stack.
- Always mask clouds and cloud shadows: Use quality bands (Sentinel-2 SCL or Landsat QA_PIXEL) and apply bitwise tests via raster calculator to isolate valid pixels.
- Balance training data: Training datasets often over-represent dominant classes (e.g., forest) relative to rare classes (e.g., wetland). Sample training polygons proportional to expected class area, or oversample rare classes to improve model accuracy.
- Check spectral separability: If accuracy assessment reports Jeffries-Matusita distance < 1.5 between any two classes, consider merging classes or collecting more spectrally diverse training polygons.
- Use time-series techniques for change detection: Multi-date classifications can show spurious change due to atmospheric variation. Spectral indices are more robust; consider computing NDVI or NDWI time series and detecting change directly in index space.
- Memory is your bottleneck: Large multispectral stacks fill RAM quickly. Use band-by-band processing or out-of-memory methods for scenes > 1 GB.