Visualisation and Advanced Usage#
Visualisation#
alsDB includes a set of plot functions for both point-cloud data and gridded products from the ALSZarrStore.
Point-cloud visualisation#
All 2-D plot functions accept a point-cloud DataFrame from ALSProvider.query_bbox() and rasterise it to a regular grid before rendering.
from alsdb import ALSProvider
from alsdb.utils.viz import plot_overview, plot_dsm, plot_rgb, plot_intensity, plot_classification
reader = ALSProvider(storage_type="local", uri="my_array")
df = reader.query_bbox(308_000, 4_688_000, 310_000, 4_690_000, year=2021)
# 4-panel overview: DSM+hillshade, RGB, intensity, classification
fig = plot_overview(df, resolution=1.0)
fig.savefig("overview.png", dpi=150, bbox_inches="tight")
# Individual panels
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
plot_dsm(df, resolution=1.0, hillshade=True, ax=axes[0])
plot_rgb(df, resolution=1.0, ax=axes[1])
Function |
What it shows |
|---|---|
|
Max-Z raster with optional hillshade |
|
RGB orthoimage (percentile contrast stretch) |
|
Mean return intensity (greyscale) |
|
LAS class codes with standard colour palette |
GEDI waveform visualisation#
from alsdb.processing.waveform import simulate_waveform
from alsdb.utils.viz import plot_waveform, plot_rh_profile
result = simulate_waveform(reader, center_x=308_500, center_y=4_689_000, year=2021)
# Waveform energy vs elevation with annotated RH levels
fig = plot_waveform(result)
fig.savefig("waveform.png", dpi=150, bbox_inches="tight")
# GEDI L2A style: RH(p) curve + W(h) energy density
fig = plot_rh_profile(result)
fig.savefig("rh_profile.png", dpi=150, bbox_inches="tight")
plot_waveform produces a two-panel figure:
Left — normalised waveform energy vs elevation, ground return shaded brown, canopy shaded green, RH25/50/75/95/100 annotated as horizontal lines.
Right — horizontal bar chart of RH heights above ground.
plot_rh_profile matches the GEDI L2A canonical representation:
(a) RH(p) curve — height above ground vs cumulative energy percent, with understory and overstory layer shading.
(b) W(h) = dE/dh — normalised energy density vs height with auto-detected layer peaks.
3-D waveform waterfall#
Visualise all simulated shots as a waterfall of RH(p) curves:
from alsdb.utils.viz import plot_waveforms_3d
fig = plot_waveforms_3d(results, color_by="rh98", backend="matplotlib")
fig.savefig("waveforms_3d.png", dpi=150, bbox_inches="tight")
# Interactive plotly version
fig = plot_waveforms_3d(results, color_by="cover", backend="plotly")
fig.show()
3-D point cloud#
from alsdb.utils.viz import plot_pointcloud_3d
plot_pointcloud_3d(df, color_by="Z", backend="matplotlib")
plot_pointcloud_3d(df, color_by="RGB", backend="plotly")
plot_pointcloud_3d(df, color_by="Classification", max_points=100_000)
Gridded product visualisation#
All raster plot functions read from an ALSZarrStore at a given resolution and year:
from alsdb.storage import ALSZarrStore
from alsdb.utils.viz_raster import (
plot_chm, plot_dtm, plot_dsm,
plot_agb, plot_gap, plot_lai,
plot_metrics,
plot_products, plot_products_agb,
)
store = ALSZarrStore("output/forest.zarr")
# Individual product panels
plot_chm(store, resolution=1.0, year=2021)
plot_dtm(store, resolution=1.0, year=2021, hillshade=True)
plot_dsm(store, resolution=1.0, year=2021)
plot_agb(store, resolution=10.0, year=2021)
plot_gap(store, resolution=10.0, year=2021)
plot_lai(store, resolution=10.0, year=2021)
# Six structural metrics in one figure
plot_metrics(store, resolution=10.0, year=2021)
# Three-panel: DTM | DSM | CHM
plot_products(store, resolution=1.0, year=2021)
# Four-panel: DTM | DSM | CHM | AGB
plot_products_agb(store, resolution=10.0, year=2021)
If the store contains only one survey year the year= argument can be omitted.
—
Advanced Usage#
ALSZarrStore internals#
The ALSZarrStore is a thin wrapper around a Zarr v3 group hierarchy. For advanced use cases you can access the underlying Zarr groups directly:
from alsdb.storage import ALSZarrStore
store = ALSZarrStore("output/forest.zarr")
# List what is in the store
print(store.resolutions()) # [1.0, 10.0]
print(store.variables(resolution=1.0)) # ['chm', 'dsm', 'dtm']
print(store.has_data("chm", 1.0, 2021)) # True / False
# Open as xarray (CRS-aware via rioxarray)
ds = store.to_dataset(resolution=1.0)
print(ds)
# Write a custom tile (advanced)
import numpy as np
data = np.zeros((100, 100), dtype=np.float32)
bbox = (308_000.0, 4_688_000.0, 309_000.0, 4_689_000.0)
store.write_tile("my_variable", 10.0, 2021, data, bbox, crs="EPSG:25830")
The CLI#
# Ingest a single tile (local)
alsdb ingest tile.laz my_array
# Ingest to S3
alsdb ingest tile.laz s3://bucket/als_array \
--storage-type s3 \
--s3-url https://s3.example.com \
--s3-access-key KEY --s3-secret-key SECRET
# Filter to specific classes
alsdb ingest tile.laz my_array -c 2 -c 3 -c 4 -c 5
# Show file metadata (year, CRS, bbox, point count)
alsdb info tile.laz
# Verbose logging
alsdb -v ingest tile.laz my_array
Multi-temporal workflows#
Because the TileDB array stores all survey years together, comparing two surveys requires only a change in the year argument:
from alsdb.processing.chm import compute_chm
for year in [2017, 2021]:
compute_chm(provider=reader, store=store, resolution=1.0,
year=year, tile_size=500.0, n_workers=4)
ds = store.to_dataset(resolution=1.0)
chm_2017 = ds["chm"].sel(time=2017)
chm_2021 = ds["chm"].sel(time=2021)
delta_chm = chm_2021 - chm_2017 # canopy height change (m)
The overwrite=False default means re-running the pipeline for a new year never re-computes existing years.