15. Whitebox#

Open In Colab

15.1. Overview#

In this lecture, we will explore the use of WhiteboxTools, a powerful open-source library for performing geospatial analysis. Specifically, we will focus on two key applications: watershed analysis and LiDAR data analysis. You will learn how to manipulate geospatial data using Python, conduct hydrological analysis, and derive digital elevation models (DEMs) and canopy height models (CHMs) from LiDAR data.

This lecture is structured into two main sections:

  1. Watershed Analysis: Using DEMs and hydrological tools to delineate watersheds, calculate flow accumulation, and extract stream networks.

  2. LiDAR Data Analysis: Processing LiDAR point cloud data to derive DEMs, DSMs, and CHMs while removing outliers and improving data quality.

By the end of this session, you will have gained hands-on experience with WhiteboxTools and leafmap, allowing you to perform a wide range of geospatial and hydrological analyses.

15.2. Learning Objectives#

By the end of this lecture, you will be able to:

  • Install and configure WhiteboxTools and leafmap for geospatial analysis.

  • Create interactive maps to visualize basemaps and geospatial datasets.

  • Perform watershed analysis by delineating watersheds, flow directions, and stream networks.

  • Manipulate and analyze Digital Elevation Models (DEMs) to conduct hydrological modeling.

  • Process and analyze LiDAR data to generate Digital Surface Models (DSMs), Digital Elevation Models (DEMs), and Canopy Height Models (CHMs).

  • Integrate WhiteboxTools with Python workflows to automate geospatial analysis.

15.3. Introduction#

Below is a brief introduction to Whitebox, a powerful open-source library for geospatial analysis. For more information, please refer to the whiteboxgeo website: https://www.whiteboxgeo.com.

15.3.1. What is Whitebox?#

Whitebox is geospatial data analysis software originally developed at the University of Guelph‘s Geomorphometry and Hydrogeomatics Research Group (GHRG) directed by Dr. John Lindsay. Whitebox contains over 550 tools for processing many types of geospatial data. It has many great features such as its extensive use of parallel computing, it doesn’t need other libraries to be installed (e.g., GDAL), it can be used from scripting environments, and it easily plugs into other geographical information system (GIS) software. The Whitebox Toolset Extension provides even more power.

15.3.2. What can Whitebox do?#

Whitebox can be used to perform common GIS and remote sensing analysis tasks. Whitebox also contains advanced tooling for spatial hydrological analysis and LiDAR data processing. Whitebox is not a cartographic or spatial data visualization package; instead it’s meant to serve as an analytical back-end for other data visualization software, like QGIS and ArcGIS.

15.3.3. How is Whitebox different?#

Whitebox doesn’t compete with QGIS, ArcGIS/Pro, and ArcPy but rather it extends them. You can plug WhiteboxTools into QGIS and ArcGIS and it’ll provide hundreds of additional tools for analyzing all kinds of geospatial data. You can also call Whitebox functions from Python scripts using Whitebox Workflows (WbW). Combine WbW with ArcPy to more effectively automate your data analysis workflows and streamline your geoprocessing solutions.

There are many tools in Whitebox that you won’t find elsewhere. You can think of Whitebox as a portable, cross-platform GIS analysis powerhouse, allowing you to extend your preferred GIS or to embed Whitebox capabilities into your automated scripted workflows. Oh, and it’s fast, really fast!

15.4. Useful Resources for Whitebox#

15.5. Installation#

To get started, we need to install the required packages, such as leafmap, and whitebox. Uncomment the following lines to install the packages.

# %pip install "leafmap[raster]" "leafmap[lidar]" mapclassify
# %pip install numpy==1.26.4

15.6. Import libraries#

import os
import leafmap
import numpy as np

Some of the raster datasets generated by whitebox will be int32 type with a nodata value of -32768. To make it easier to visualize these datasets, we set the nodata value as an environment variable, which will be used by leafmap to set the nodata value when visualizing the raster datasets.

os.environ["NODATA"] = "-32768"

15.7. Part 1: Watershed Analysis#

15.7.1. Create Interactive Maps#

To perform watershed analysis, we first create an interactive map using leafmap. This step allows us to visualize different basemaps.

m = leafmap.Map()
m.add_basemap("OpenTopoMap")
m.add_basemap("USGS 3DEP Elevation")
m.add_basemap("USGS Hydrography")
m

15.7.2. Download Watershed Data#

Next, we download watershed data for the Calapooia River basin in Oregon. We’ll use the latitude and longitude of a point in the basin to extract watershed boundary data.

lat = 44.361169
lon = -122.821802

m = leafmap.Map(center=[lat, lon], zoom=10)
m.add_marker([lat, lon])
m

Download the watershed data and visualize it:

geometry = {"x": lon, "y": lat}
gdf = leafmap.get_wbd(geometry, digit=10, return_geometry=True)
gdf.explore()
Make this Notebook Trusted to load map: File -> Trust Notebook

Save the watershed boundary to a GeoJSON file:

gdf.to_file("basin.geojson")

15.7.3. Download and Display DEM#

We download a Digital Elevation Model (DEM) from the USGS 3DEP Elevation service to analyze the terrain of the watershed. The DEM will be used to delineate watersheds, calculate flow accumulation, and extract stream networks. The leafmap.get_3dep_dem() function returns the DEM as an xarray.DataArray object. Optionally, you can save the DEM to a GeoTIFF file by setting the output parameter.

array = leafmap.get_3dep_dem(
    gdf,
    resolution=30,
    output="dem.tif",
    dst_crs="EPSG:3857",
    to_cog=True,
    overwrite=True,
)
array
py3dep is not installed. Installing py3dep...
Defaulting to user installation because normal site-packages is not writeable
Collecting py3dep
Downloading py3dep-0.19.0-py3-none-any.whl.metadata (21 kB)
Collecting async-retriever<0.20,>=0.19 (from py3dep)
Downloading async_retriever-0.19.3-py3-none-any.whl.metadata (16 kB)
Requirement already satisfied: click>=0.7 in /usr/lib/python3/dist-packages (from py3dep) (8.1.6)
Collecting cytoolz (from py3dep)
Downloading cytoolz-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.6 kB)
Collecting geopandas>=1 (from py3dep)
Downloading geopandas-1.0.1-py3-none-any.whl.metadata (2.2 kB)
Collecting numpy>=1.17 (from py3dep)
Downloading numpy-2.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (62 kB)
?25l     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/62.0 kB ? eta -:--:--
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.0/62.0 kB 6.7 MB/s eta 0:00:00
?25hCollecting pygeoogc<0.20,>=0.19 (from py3dep)
Downloading pygeoogc-0.19.3-py3-none-any.whl.metadata (18 kB)
Collecting pygeoutils<0.20,>=0.19 (from py3dep)
Downloading pygeoutils-0.19.5-py3-none-any.whl.metadata (12 kB)
Collecting rasterio>=1.2 (from py3dep)
Downloading rasterio-1.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (9.1 kB)
Collecting rioxarray>=0.11 (from py3dep)
Downloading rioxarray-0.19.0-py3-none-any.whl.metadata (5.5 kB)
Collecting shapely>=2 (from py3dep)
Downloading shapely-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.7 kB)
Collecting xarray>=2023.1 (from py3dep)
Downloading xarray-2025.3.1-py3-none-any.whl.metadata (12 kB)
Collecting aiodns (from async-retriever<0.20,>=0.19->py3dep)
Downloading aiodns-3.2.0-py3-none-any.whl.metadata (4.0 kB)
Collecting aiofiles (from async-retriever<0.20,>=0.19->py3dep)
Downloading aiofiles-24.1.0-py3-none-any.whl.metadata (10 kB)
Collecting aiohttp-client-cache>=0.12.3 (from async-retriever<0.20,>=0.19->py3dep)
Downloading aiohttp_client_cache-0.13.0-py3-none-any.whl.metadata (6.9 kB)
Collecting aiohttp>=3.8.3 (from async-retriever<0.20,>=0.19->py3dep)
Downloading aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (7.7 kB)
Collecting aiosqlite (from async-retriever<0.20,>=0.19->py3dep)
Downloading aiosqlite-0.21.0-py3-none-any.whl.metadata (4.3 kB)
Collecting brotli (from async-retriever<0.20,>=0.19->py3dep)
Downloading Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (5.5 kB)
Collecting multidict (from async-retriever<0.20,>=0.19->py3dep)
Downloading multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (5.3 kB)
Collecting orjson<4,>=3.10 (from async-retriever<0.20,>=0.19->py3dep)
Downloading orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (41 kB)
?25l     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/41.8 kB ? eta -:--:--
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.8/41.8 kB 11.7 MB/s eta 0:00:00
?25hCollecting yarl (from async-retriever<0.20,>=0.19->py3dep)
Downloading yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (72 kB)
?25l     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/72.4 kB ? eta -:--:--
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 72.4/72.4 kB 20.8 MB/s eta 0:00:00
?25hCollecting pyogrio>=0.7.2 (from geopandas>=1->py3dep)
Downloading pyogrio-0.10.0-cp312-cp312-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
Requirement already satisfied: packaging in /usr/lib/python3/dist-packages (from geopandas>=1->py3dep) (24.0)
Collecting pandas>=1.4.0 (from geopandas>=1->py3dep)
Downloading pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (89 kB)
?25l     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/89.9 kB ? eta -:--:--
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 89.9/89.9 kB 25.9 MB/s eta 0:00:00
?25hCollecting pyproj>=3.3.0 (from geopandas>=1->py3dep)
Downloading pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (31 kB)
Collecting defusedxml (from pygeoogc<0.20,>=0.19->py3dep)
Downloading defusedxml-0.7.1-py2.py3-none-any.whl.metadata (32 kB)
Collecting joblib (from pygeoogc<0.20,>=0.19->py3dep)
Downloading joblib-1.4.2-py3-none-any.whl.metadata (5.4 kB)
Collecting owslib>=0.27.2 (from pygeoogc<0.20,>=0.19->py3dep)
Downloading owslib-0.33.0-py3-none-any.whl.metadata (6.9 kB)
Requirement already satisfied: requests in /usr/lib/python3/dist-packages (from pygeoogc<0.20,>=0.19->py3dep) (2.31.0)
Collecting requests-cache>=0.9.6 (from pygeoogc<0.20,>=0.19->py3dep)
Downloading requests_cache-1.2.1-py3-none-any.whl.metadata (9.9 kB)
Requirement already satisfied: typing-extensions in /usr/lib/python3/dist-packages (from pygeoogc<0.20,>=0.19->py3dep) (4.10.0)
Collecting url-normalize>=1.4 (from pygeoogc<0.20,>=0.19->py3dep)
Downloading url_normalize-2.2.1-py3-none-any.whl.metadata (5.6 kB)
Requirement already satisfied: urllib3 in /usr/lib/python3/dist-packages (from pygeoogc<0.20,>=0.19->py3dep) (2.0.7)
Collecting netcdf4 (from pygeoutils<0.20,>=0.19->py3dep)
Downloading netCDF4-1.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.8 kB)
Collecting scipy (from pygeoutils<0.20,>=0.19->py3dep)
Downloading scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (61 kB)
?25l     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/62.0 kB ? eta -:--:--
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.0/62.0 kB 19.4 MB/s eta 0:00:00
?25hCollecting affine (from rasterio>=1.2->py3dep)
Downloading affine-2.4.0-py3-none-any.whl.metadata (4.0 kB)
Requirement already satisfied: attrs in /usr/lib/python3/dist-packages (from rasterio>=1.2->py3dep) (23.2.0)
Requirement already satisfied: certifi in /usr/lib/python3/dist-packages (from rasterio>=1.2->py3dep) (2023.11.17)
Collecting cligj>=0.5 (from rasterio>=1.2->py3dep)
Downloading cligj-0.7.2-py3-none-any.whl.metadata (5.0 kB)
Collecting click-plugins (from rasterio>=1.2->py3dep)
Downloading click_plugins-1.1.1-py2.py3-none-any.whl.metadata (6.4 kB)
Requirement already satisfied: pyparsing in /usr/lib/python3/dist-packages (from rasterio>=1.2->py3dep) (3.1.1)
Collecting toolz>=0.8.0 (from cytoolz->py3dep)
Downloading toolz-1.0.0-py3-none-any.whl.metadata (5.1 kB)
Collecting aiohappyeyeballs>=2.3.0 (from aiohttp>=3.8.3->async-retriever<0.20,>=0.19->py3dep)
Downloading aiohappyeyeballs-2.6.1-py3-none-any.whl.metadata (5.9 kB)
Collecting aiosignal>=1.1.2 (from aiohttp>=3.8.3->async-retriever<0.20,>=0.19->py3dep)
Downloading aiosignal-1.3.2-py2.py3-none-any.whl.metadata (3.8 kB)
Collecting frozenlist>=1.1.1 (from aiohttp>=3.8.3->async-retriever<0.20,>=0.19->py3dep)
Downloading frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (16 kB)
Collecting propcache>=0.2.0 (from aiohttp>=3.8.3->async-retriever<0.20,>=0.19->py3dep)
Downloading propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (10 kB)
Collecting itsdangerous>=2.0 (from aiohttp-client-cache>=0.12.3->async-retriever<0.20,>=0.19->py3dep)
Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting lxml (from owslib>=0.27.2->pygeoogc<0.20,>=0.19->py3dep)
Downloading lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl.metadata (3.5 kB)
Requirement already satisfied: python-dateutil in /usr/lib/python3/dist-packages (from owslib>=0.27.2->pygeoogc<0.20,>=0.19->py3dep) (2.8.2)
Requirement already satisfied: pyyaml in /usr/lib/python3/dist-packages (from owslib>=0.27.2->pygeoogc<0.20,>=0.19->py3dep) (6.0.1)
Requirement already satisfied: pytz>=2020.1 in /usr/lib/python3/dist-packages (from pandas>=1.4.0->geopandas>=1->py3dep) (2024.1)
Collecting tzdata>=2022.7 (from pandas>=1.4.0->geopandas>=1->py3dep)
Downloading tzdata-2025.2-py2.py3-none-any.whl.metadata (1.4 kB)
Collecting cattrs>=22.2 (from requests-cache>=0.9.6->pygeoogc<0.20,>=0.19->py3dep)
Downloading cattrs-24.1.3-py3-none-any.whl.metadata (8.4 kB)
Requirement already satisfied: platformdirs>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests-cache>=0.9.6->pygeoogc<0.20,>=0.19->py3dep) (4.3.7)
Requirement already satisfied: idna>=3.3 in /usr/lib/python3/dist-packages (from url-normalize>=1.4->pygeoogc<0.20,>=0.19->py3dep) (3.6)
Collecting pycares>=4.0.0 (from aiodns->async-retriever<0.20,>=0.19->py3dep)
Downloading pycares-4.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.3 kB)
Collecting cftime (from netcdf4->pygeoutils<0.20,>=0.19->py3dep)
Downloading cftime-1.6.4.post1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (8.7 kB)
Collecting cffi>=1.5.0 (from pycares>=4.0.0->aiodns->async-retriever<0.20,>=0.19->py3dep)
Downloading cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.5 kB)
Collecting pycparser (from cffi>=1.5.0->pycares>=4.0.0->aiodns->async-retriever<0.20,>=0.19->py3dep)
Downloading pycparser-2.22-py3-none-any.whl.metadata (943 bytes)
Downloading py3dep-0.19.0-py3-none-any.whl (25 kB)
Downloading async_retriever-0.19.3-py3-none-any.whl (18 kB)
Downloading geopandas-1.0.1-py3-none-any.whl (323 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/323.6 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 323.6/323.6 kB 36.6 MB/s eta 0:00:00
?25hDownloading numpy-2.2.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (16.1 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/16.1 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━ 6.9/16.1 MB 205.8 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━ 13.8/16.1 MB 211.6 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 16.1/16.1 MB 210.8 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.1/16.1 MB 125.2 MB/s eta 0:00:00
?25hDownloading pygeoogc-0.19.3-py3-none-any.whl (34 kB)
Downloading pygeoutils-0.19.5-py3-none-any.whl (30 kB)
Downloading rasterio-1.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (22.3 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/22.3 MB ? eta -:--:--
   ━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━ 7.4/22.3 MB 223.5 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━ 14.4/22.3 MB 207.7 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 22.3/22.3 MB 224.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 22.3/22.3 MB 224.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 22.3/22.3 MB 112.3 MB/s eta 0:00:00
?25hDownloading rioxarray-0.19.0-py3-none-any.whl (62 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/62.2 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.2/62.2 kB 22.6 MB/s eta 0:00:00
?25hDownloading shapely-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.1 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/3.1 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.1/3.1 MB 144.1 MB/s eta 0:00:00
?25hDownloading xarray-2025.3.1-py3-none-any.whl (1.3 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/1.3 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.3/1.3 MB 127.0 MB/s eta 0:00:00
?25hDownloading cytoolz-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.1 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/2.1 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 137.8 MB/s eta 0:00:00
?25hDownloading aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.7 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/1.7 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.7/1.7 MB 132.6 MB/s eta 0:00:00
?25hDownloading aiohttp_client_cache-0.13.0-py3-none-any.whl (32 kB)
Downloading cligj-0.7.2-py3-none-any.whl (7.1 kB)
Downloading multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (223 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/223.5 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 223.5/223.5 kB 59.7 MB/s eta 0:00:00
?25hDownloading orjson-3.10.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (133 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/133.1 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.1/133.1 kB 44.6 MB/s eta 0:00:00
?25hDownloading owslib-0.33.0-py3-none-any.whl (240 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/240.1 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 240.1/240.1 kB 63.0 MB/s eta 0:00:00
?25hDownloading pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.7 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/12.7 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━ 7.1/12.7 MB 215.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 12.7/12.7 MB 227.4 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.7/12.7 MB 140.7 MB/s eta 0:00:00
?25hDownloading pyogrio-0.10.0-cp312-cp312-manylinux_2_28_x86_64.whl (24.0 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/24.0 MB ? eta -:--:--
   ━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━ 7.8/24.0 MB 234.4 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━ 15.7/24.0 MB 228.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 24.0/24.0 MB 243.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 24.0/24.0 MB 243.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 24.0/24.0 MB 115.7 MB/s eta 0:00:00
?25hDownloading pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (9.6 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/9.6 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━ 6.6/9.6 MB 199.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.6/9.6 MB 142.4 MB/s eta 0:00:00
?25hDownloading requests_cache-1.2.1-py3-none-any.whl (61 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/61.4 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.4/61.4 kB 20.1 MB/s eta 0:00:00
?25hDownloading toolz-1.0.0-py3-none-any.whl (56 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/56.4 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.4/56.4 kB 18.6 MB/s eta 0:00:00
?25hDownloading url_normalize-2.2.1-py3-none-any.whl (14 kB)
Downloading yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (349 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/349.2 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 349.2/349.2 kB 80.5 MB/s eta 0:00:00
?25hDownloading affine-2.4.0-py3-none-any.whl (15 kB)
Downloading aiodns-3.2.0-py3-none-any.whl (5.7 kB)
Downloading aiofiles-24.1.0-py3-none-any.whl (15 kB)
Downloading aiosqlite-0.21.0-py3-none-any.whl (15 kB)
Downloading Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.9 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/2.9 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.9/2.9 MB 142.8 MB/s eta 0:00:00
?25hDownloading click_plugins-1.1.1-py2.py3-none-any.whl (7.5 kB)
Downloading defusedxml-0.7.1-py2.py3-none-any.whl (25 kB)
Downloading joblib-1.4.2-py3-none-any.whl (301 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/301.8 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 301.8/301.8 kB 73.0 MB/s eta 0:00:00
?25hDownloading netCDF4-1.7.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (9.3 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/9.3 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━ 7.6/9.3 MB 229.3 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.3/9.3 MB 148.4 MB/s eta 0:00:00
?25hDownloading scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (37.3 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/37.3 MB ? eta -:--:--
   ━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.6/37.3 MB 198.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━ 13.9/37.3 MB 209.3 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━╸━━━━━━━━━━━━━━━━━ 21.1/37.3 MB 207.1 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━ 28.2/37.3 MB 207.1 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╺━ 35.9/37.3 MB 216.8 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 37.3/37.3 MB 220.6 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 37.3/37.3 MB 220.6 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 37.3/37.3 MB 89.9 MB/s eta 0:00:00
?25hDownloading aiohappyeyeballs-2.6.1-py3-none-any.whl (15 kB)
Downloading aiosignal-1.3.2-py2.py3-none-any.whl (7.6 kB)
Downloading cattrs-24.1.3-py3-none-any.whl (66 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/66.5 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 66.5/66.5 kB 25.1 MB/s eta 0:00:00
?25hDownloading frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (316 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/316.2 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 316.2/316.2 kB 76.3 MB/s eta 0:00:00
?25hDownloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Downloading propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (245 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/245.0 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 245.0/245.0 kB 69.9 MB/s eta 0:00:00
?25hDownloading pycares-4.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (290 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/290.8 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 290.8/290.8 kB 75.5 MB/s eta 0:00:00
?25hDownloading tzdata-2025.2-py2.py3-none-any.whl (347 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/347.8 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 347.8/347.8 kB 77.3 MB/s eta 0:00:00
?25hDownloading cftime-1.6.4.post1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.4 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/1.4 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.4/1.4 MB 122.9 MB/s eta 0:00:00
?25hDownloading lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl (5.0 MB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/5.0 MB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╸ 4.9/5.0 MB 197.2 MB/s eta 0:00:01
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.0/5.0 MB 131.9 MB/s eta 0:00:00
?25hDownloading cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (479 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/479.4 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 479.4/479.4 kB 88.3 MB/s eta 0:00:00
?25hDownloading pycparser-2.22-py3-none-any.whl (117 kB)
?25l   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/117.6 kB ? eta -:--:--
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.6/117.6 kB 33.2 MB/s eta 0:00:00
?25hInstalling collected packages: brotli, url-normalize, tzdata, toolz, pyproj, pycparser, propcache, orjson, numpy, multidict, lxml, joblib, itsdangerous, frozenlist, defusedxml, cligj, click-plugins, cattrs, aiosqlite, aiohappyeyeballs, aiofiles, affine, yarl, shapely, scipy, requests-cache, rasterio, pyogrio, pandas, owslib, cytoolz, cftime, cffi, aiosignal, xarray, pycares, netcdf4, geopandas, aiohttp, rioxarray, aiohttp-client-cache, aiodns, pygeoutils, async-retriever, pygeoogc, py3dep
Successfully installed affine-2.4.0 aiodns-3.2.0 aiofiles-24.1.0 aiohappyeyeballs-2.6.1 aiohttp-3.11.18 aiohttp-client-cache-0.13.0 aiosignal-1.3.2 aiosqlite-0.21.0 async-retriever-0.19.3 brotli-1.1.0 cattrs-24.1.3 cffi-1.17.1 cftime-1.6.4.post1 click-plugins-1.1.1 cligj-0.7.2 cytoolz-1.0.1 defusedxml-0.7.1 frozenlist-1.6.0 geopandas-1.0.1 itsdangerous-2.2.0 joblib-1.4.2 lxml-5.4.0 multidict-6.4.3 netcdf4-1.7.2 numpy-2.2.5 orjson-3.10.16 owslib-0.33.0 pandas-2.2.3 propcache-0.3.1 py3dep-0.19.0 pycares-4.6.1 pycparser-2.22 pygeoogc-0.19.3 pygeoutils-0.19.5 pyogrio-0.10.0 pyproj-3.7.1 rasterio-1.4.3 requests-cache-1.2.1 rioxarray-0.19.0 scipy-1.15.2 shapely-2.1.0 toolz-1.0.0 tzdata-2025.2 url-normalize-2.2.1 xarray-2025.3.1 yarl-1.20.0
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/leafmap/common.py:9851, in get_3dep_dem(geometry, resolution, src_crs, output, dst_crs, to_cog, overwrite, **kwargs)
   9850 try:
-> 9851     import py3dep
   9852 except ImportError:

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/py3dep/__init__.py:7
      5 from importlib.metadata import PackageNotFoundError, version
----> 7 from py3dep import exceptions
      8 from py3dep.geoops import deg2mpm, fill_depressions

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/py3dep/exceptions.py:5
      3 from __future__ import annotations
----> 5 import async_retriever.exceptions as ar
      6 import pygeoogc.exceptions as ogc

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/async_retriever/__init__.py:18
     17 from async_retriever.print_versions import show_versions
---> 18 from async_retriever.streaming import generate_filename, stream_write
     20 try:

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/async_retriever/streaming.py:13
     12 import orjson
---> 13 from aiohttp import (
     14     ClientConnectorDNSError,
     15     ClientResponseError,
     16     ClientSession,
     17     ClientTimeout,
     18     TCPConnector,
     19 )
     20 from multidict import MultiDict

ImportError: cannot import name 'ClientConnectorDNSError' from 'aiohttp' (/home/runner/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/aiohttp/__init__.py)

During handling of the above exception, another exception occurred:

ImportError                               Traceback (most recent call last)
Cell In[10], line 1
----> 1 array = leafmap.get_3dep_dem(
      2     gdf,
      3     resolution=30,
      4     output="dem.tif",
      5     dst_crs="EPSG:3857",
      6     to_cog=True,
      7     overwrite=True,
      8 )
      9 array

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/leafmap/common.py:9855, in get_3dep_dem(geometry, resolution, src_crs, output, dst_crs, to_cog, overwrite, **kwargs)
   9853     print("py3dep is not installed. Installing py3dep...")
   9854     install_package("py3dep")
-> 9855     import py3dep
   9857 import geopandas as gpd
   9859 if output is not None and os.path.exists(output) and not overwrite:

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/py3dep/__init__.py:7
      3 from __future__ import annotations
      5 from importlib.metadata import PackageNotFoundError, version
----> 7 from py3dep import exceptions
      8 from py3dep.geoops import deg2mpm, fill_depressions
      9 from py3dep.print_versions import show_versions

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/py3dep/exceptions.py:5
      1 """Customized Py3DEP exceptions."""
      3 from __future__ import annotations
----> 5 import async_retriever.exceptions as ar
      6 import pygeoogc.exceptions as ogc
      9 class InputTypeError(ar.InputTypeError):

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/async_retriever/__init__.py:18
     10 from async_retriever.async_retriever import (
     11     delete_url_cache,
     12     retrieve,
   (...)
     15     retrieve_text,
     16 )
     17 from async_retriever.print_versions import show_versions
---> 18 from async_retriever.streaming import generate_filename, stream_write
     20 try:
     21     __version__ = version("async_retriever")

File ~/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/async_retriever/streaming.py:13
     11 import aiofiles
     12 import orjson
---> 13 from aiohttp import (
     14     ClientConnectorDNSError,
     15     ClientResponseError,
     16     ClientSession,
     17     ClientTimeout,
     18     TCPConnector,
     19 )
     20 from multidict import MultiDict
     21 from yarl import URL

ImportError: cannot import name 'ClientConnectorDNSError' from 'aiohttp' (/home/runner/work/geog-312/geog-312/.venv/lib/python3.12/site-packages/aiohttp/__init__.py)

Visualize the DEM on the map:

m.add_raster("dem.tif", palette="terrain", nodata=np.nan, layer_name="DEM")
m

15.7.4. Get DEM metadata#

We can get the metadata of the DEM, such as the spatial resolution, bounding box, and coordinate reference system (CRS).

metadata = leafmap.image_metadata("dem.tif")
metadata

Get a summary statistics of the DEM.

15.7.5. Add colorbar#

Add a colorbar to the map to show the elevation values. Use the image_min_max() function to get the minimum and maximum values of the DEM.

leafmap.image_min_max("dem.tif")
m.add_colormap(cmap="terrain", vmin="60", vmax=1500, label="Elevation (m)")

15.7.6. Initialize WhiteboxTools#

Initialize the WhiteboxTools class.

wbt = leafmap.WhiteboxTools()

Check the WhiteboxTools version.

wbt.version()

Display the WhiteboxTools interface, which lists all available tools. You can use this interface to search for specific tools. You can also run any tool using the interface. However, we will use the Python API to run the tools.

leafmap.whiteboxgui()

15.7.7. Set working directory#

Set the working directory to save the intermediate and output files. Set wbt.version=False to suppress the Whitebox processing log.

wbt.set_working_dir(os.getcwd())
wbt.verbose = False

15.7.8. Smooth DEM#

All WhiteboxTools functions will return 0 if they are successful, and 1 if they are not.

Smoothing the DEM enhances the quality of subsequent hydrological analysis.

wbt.feature_preserving_smoothing("dem.tif", "smoothed.tif", filter=9)

Visualize the smoothed DEM and watershed boundary on the map.

Visualize the smoothed DEM:

m = leafmap.Map()
m.add_basemap("Satellite")
m.add_raster("smoothed.tif", colormap="terrain", layer_name="Smoothed DEM")
m.add_geojson("basin.geojson", layer_name="Watershed", info_mode=None)
m.add_basemap("USGS Hydrography", show=False)
m

15.7.9. Create hillshade#

Create a hillshade from the smoothed DEM.

wbt.hillshade("smoothed.tif", "hillshade.tif", azimuth=315, altitude=35)

Overlay the hillshade on the smoothed DEM with transparency.

m.add_raster("hillshade.tif", layer_name="Hillshade")
m.layers[-1].opacity = 0.6

15.7.10. Find no-flow cells#

Find cells with undefined flow, i.e. no valid flow direction, based on the D8 flow direction algorithm.

wbt.find_no_flow_cells("smoothed.tif", "noflow.tif")

Visualize the no-flow cells on the map.

m.add_raster("noflow.tif", layer_name="No Flow Cells")

15.7.11. Fill depressions#

First, we fill any depressions in the DEM to ensure proper flow simulation.

wbt.fill_depressions("smoothed.tif", "filled.tif")

Alternatively, you can use depression breaching to fill the depressions.

wbt.breach_depressions("smoothed.tif", "breached.tif")
wbt.find_no_flow_cells("breached.tif", "noflow2.tif")
m.layers[-1].visible = False
m.add_raster("noflow2.tif", layer_name="No Flow Cells after Breaching")
m

15.7.12. Delineate flow direction#

Next, we delineate the flow direction based on the D8 algorithm.

wbt.d8_pointer("breached.tif", "flow_direction.tif")

15.7.13. Calculate flow accumulation#

Now, calculate flow accumulation to understand how water collects across the landscape.

wbt.d8_flow_accumulation("breached.tif", "flow_accum.tif")
m.add_raster("flow_accum.tif", layer_name="Flow Accumulation")

15.7.14. Extract streams#

Extract the stream network using the flow accumulation data.

wbt.extract_streams("flow_accum.tif", "streams.tif", threshold=5000)
m.layers[-1].visible = False
m.add_raster("streams.tif", layer_name="Streams")

15.7.15. Calculate distance to outlet#

wbt.distance_to_outlet(
    "flow_direction.tif", streams="streams.tif", output="distance_to_outlet.tif"
)
m.add_raster("distance_to_outlet.tif", layer_name="Distance to Outlet")

15.7.16. Vectorize streams#

wbt.raster_streams_to_vector(
    "streams.tif", d8_pntr="flow_direction.tif", output="streams.shp"
)

The raster_streams_to_vector tool has a bug. The output vector file is missing the coordinate system. Use leafmap.vector_set_crs() to set the coordinate system.

leafmap.vector_set_crs(source="streams.shp", output="streams.shp", crs="EPSG:3857")
m.add_shp(
    "streams.shp",
    layer_name="Streams Vector",
    style={"color": "#ff0000", "weight": 3},
    info_mode=None,
)
m

You can turn on the USGS Hydrography basemap to visualize the stream network and compare it with the extracted stream network.

15.7.17. Delineate the longest flow path#

You can delineate the longest flow path in the watershed.

wbt.basins("flow_direction.tif", "basins.tif")
wbt.longest_flowpath(
    dem="breached.tif", basins="basins.tif", output="longest_flowpath.shp"
)

Select only the longest flow path.

leafmap.select_largest(
    "longest_flowpath.shp", column="LENGTH", output="longest_flowpath.shp"
)
m.add_shp(
    "longest_flowpath.shp",
    layer_name="Longest Flowpath",
    style={"color": "#ff0000", "weight": 3},
)
m

15.7.18. Generate a pour point#

To delineate a watershed, you need to specify a pour point. You can use the outlet of the longest flow path as the pour point or specify a different point. Use the drawing tool to place a marker on the map to specify the pour point. If no marker is placed, the default pour point below will be used.

if m.user_roi is not None:
    m.save_draw_features("pour_point.shp", crs="EPSG:3857")
else:
    lat = 44.284642
    lon = -122.611217
    leafmap.coords_to_vector([lon, lat], output="pour_point.shp", crs="EPSG:3857")
    m.add_marker([lat, lon])

15.7.19. Snap pour point to stream#

Snap the pour point to the nearest stream.

wbt.snap_pour_points(
    "pour_point.shp", "flow_accum.tif", "pour_point_snapped.shp", snap_dist=300
)

Visualize the snapped pour point on the map.

m.add_shp("pour_point_snapped.shp", layer_name="Pour Point", info_mode=False)

15.7.20. Delineate watershed#

Delineate the watershed using a pour point and the flow direction data.

wbt.watershed("flow_direction.tif", "pour_point_snapped.shp", "watershed.tif")

Visualize the watershed boundary on the map.

m.add_raster("watershed.tif", layer_name="Watershed")
m

15.7.21. Convert watershed raster to vector#

You can convert the watershed raster to a vector file.

wbt.raster_to_vector_polygons("watershed.tif", "watershed.shp")

Visualize the watershed boundary on the map.

m.layers[-1].visible = False
m.add_shp(
    "watershed.shp",
    layer_name="Watershed Vector",
    style={"color": "#ffff00", "weight": 3},
    info_mode=False,
)

15.8. Part 2: LiDAR Data Analysis#

In this section, we will process LiDAR data to derive Digital Surface Models (DSMs), Digital Elevation Models (DEMs), and Canopy Height Models (CHMs). We will also remove outliers and improve the quality of the LiDAR data.

15.8.1. Set up whitebox#

First, we set up the WhiteboxTools and leafmap libraries.

import leafmap
wbt = leafmap.WhiteboxTools()
wbt.set_working_dir(os.getcwd())
wbt.verbose = False

15.8.2. Download a sample dataset#

We download a sample LiDAR dataset for Madison.

url = "https://github.com/opengeos/datasets/releases/download/lidar/madison.zip"
filename = "madison.las"
leafmap.download_file(url, "madison.zip", quiet=True)

15.8.3. Read LAS/LAZ data#

Load and inspect the LiDAR data:

laz = leafmap.read_lidar(filename)
laz
str(laz.header.version)

15.8.4. Upgrade file version#

Upgrade the LAS file version to 1.4.

las = leafmap.convert_lidar(laz, file_version="1.4")
str(las.header.version)

15.8.5. Write LAS data#

Save the LAS data to a new file.

leafmap.write_lidar(las, "madison.las")

15.8.6. Histogram analysis#

Plot the histogram of the LiDAR data. The histogram shows the distribution of the LiDAR points based on their elevation values. The tool generates a histogram of the LiDAR data and saves it to an HTM file. You can open the HTM file in a web browser to view the histogram.

wbt.lidar_histogram("madison.las", "histogram.html")

15.8.7. Visualize LiDAR data#

Run the view_lidar() function to visualize the LiDAR data in 3D. Note that the view_lidar() function may not work in some environments, such as Google Colab.

leafmap.view_lidar("madison.las")

15.8.8. Remove outliers#

Remove outliers from the LiDAR dataset:

wbt.lidar_elevation_slice("madison.las", "madison_rm.las", minz=0, maxz=450)

15.8.9. Visualize LiDAR data after removing outliers#

We can visualize the LiDAR data after removing the outliers.

leafmap.view_lidar("madison_rm.las", cmap="terrain")

15.8.10. Create DSM#

Using the LiDAR data to create a Digital Surface Model (DSM).

wbt.lidar_digital_surface_model(
    "madison_rm.las", "dsm.tif", resolution=1.0, minz=0, maxz=450
)

The DSM generated by whitebox is missing the coordinate system. Use leafmap.raster_set_crs() to set the coordinate system.

leafmap.add_crs("dsm.tif", epsg=2255)

15.8.11. Visualize DSM#

Visualize the DSM on the map.

m = leafmap.Map()
m.add_basemap("Satellite")
m.add_raster("dsm.tif", colormap="terrain", layer_name="DSM")
m

15.8.12. Create DEM#

We can create a bare-earth DEM from the DSM. The tool is typically applied to LiDAR DEMs which frequently contain numerous off-terrain objects (OTOs) such as buildings, trees and other vegetation, cars, fences and other anthropogenic objects.

wbt.remove_off_terrain_objects("dsm.tif", "dem.tif", filter=25, slope=15.0)

15.8.13. Visualize DEM#

Visualize the bear-earth DEM on the map.

m.add_raster("dem.tif", palette="terrain", layer_name="DEM")
m

15.8.14. Create CHM#

We can a Canopy Height Model (CHM) by subtracting the DEM from the DSM.

chm = wbt.subtract("dsm.tif", "dem.tif", "chm.tif")

Visualize the CHM on the map.

m.add_raster("chm.tif", palette="gist_earth", layer_name="CHM")
m.add_layer_manager()
m

15.9. Summary#

This lecture provided a comprehensive introduction to WhiteboxTools, an open-source geospatial analysis library with a focus on hydrological and LiDAR data analysis. Through this session, students learned to install and configure WhiteboxTools in Python, integrate it with leafmap for interactive mapping, and apply it to specific geospatial tasks.

Key takeaways from this lecture include:

  • Watershed Analysis: The lecture covered watershed delineation using Digital Elevation Models (DEMs), including techniques like flow direction, flow accumulation, stream extraction, and watershed boundary delineation.

  • LiDAR Data Processing: Students were introduced to LiDAR data manipulation, including the derivation of Digital Surface Models (DSMs), Digital Elevation Models (DEMs), and Canopy Height Models (CHMs), along with methods for outlier removal and terrain quality improvement.

By completing the hands-on exercises, students gained practical experience with WhiteboxTools for geospatial processing, preparing them to apply these techniques in varied real-world GIS workflows.