How Hillshade Elevation Rendering Works in Prettymaps and Its Dependencies
Prettymaps creates realistic hillshaded maps by converting raw SRTM elevation data into grayscale illumination using Matplotlib's LightSource class, then blending the result as an RGBA overlay with variable transparency.
The hillshade elevation rendering pipeline in marceloprates/prettymaps transforms topographic data into shaded relief visualizations. This article explains the complete technical flow—from tile acquisition to final compositing—based on the actual source implementation.
Overview of the Hillshade Pipeline
The hillshade feature follows a four-stage pipeline:
- Download SRTM-1 elevation tiles via the internal fetch module
- Read raster data into NumPy arrays using Rasterio
- Compute illumination values with
matplotlib.colors.LightSource.hillshade() - Blend the result as an RGBA layer with inverted alpha transparency
Each stage involves specific dependencies that handle geospatial I/O, numerical computation, and visualization.
Stage 1: Fetching Elevation Data (SRTM-1 Tiles)
Prettymaps retrieves elevation data from the USGS Shuttle Radar Topography Mission (SRTM) dataset. The fetch.py module handles HTTP requests and local caching.
# Conceptual flow in prettymaps/fetch.py
import requests
from pathlib import Path
def fetch_srtm_tile(lat, lon, cache_dir="./SRTM1"):
"""Download SRTM-1 tile for given latitude/longitude."""
tile_name = f"N{int(lat):02d}W{abs(int(lon)):03d}.hgt"
url = f"https://e4ftl01.cr.usgs.gov/MEASURES/SRTMGL1.003/2000.02.11/{tile_name}.zip"
# Download, extract, and return local path
...
Tiles are stored temporarily in ./SRTM1 and cleaned after rendering.
Stage 2: Reading Raster Data with Rasterio
The draw_hillshade function in prettymaps/draw.py uses Rasterio to open GeoTIFF files and extract elevation arrays.
# From prettymaps/draw.py
import rasterio
import numpy as np
with rasterio.open(elevation_file) as src:
elevation_data = src.read(1) # First band contains elevation
transform = src.transform # Affine geotransform
crs = src.crs # Coordinate reference system
The geotransform provides spatial metadata. From this, horizontal (dx) and vertical (dy) resolutions are computed as the absolute pixel dimensions in meters.
Stage 3: Computing Hillshade Illumination
The core calculation happens via Matplotlib's LightSource class. An instance ls is configured with sun position parameters, then invoked with terrain data.
# Core hillshade calculation in prettymaps/draw.py
from matplotlib.colors import LightSource
ls = LightSource(azimuth=315, altitude=45) # Default sun position
hillshade = ls.hillshade(
elevation_data,
vert_exag=1.0, # Vertical exaggeration factor
dx=dx, # X resolution (meters per pixel)
dy=dy # Y resolution (meters per pixel)
)
Parameter Reference
| Parameter | Description | Typical Value |
|---|---|---|
azimuth |
Sun direction clockwise from north | 315° (NW) |
altitude |
Sun angle above horizon | 45° |
vert_exag |
Vertical scale multiplier | 0.5–3.0 |
dx, dy |
Raster ground resolution | ~30m for SRTM-1 |
The hillshade() method returns a float array in range [0, 1] where 1.0 represents fully illuminated slopes and 0.0 represents shadowed areas.
Stage 4: RGBA Conversion and Overlay
Prettymaps converts the grayscale hillshade into an RGBA image with strategic transparency. This allows underlying map layers to show through in valleys while preserving terrain definition on ridges.
# RGBA conversion in prettymaps/draw.py
import numpy as np
def hillshade_to_rgba(hillshade):
"""Convert float hillshade to RGBA with inverted alpha."""
h, w = hillshade.shape
rgba = np.zeros((h, w, 4), dtype=np.uint8)
# RGB channels: grayscale illumination
rgba[..., :3] = (hillshade[..., np.newaxis] * 255).astype(np.uint8)
# Alpha channel: inverse shade (dark = more transparent)
rgba[..., 3] = ((1.0 - hillshade) * 255).astype(np.uint8)
return rgba
hillshade_rgba = hillshade_to_rgba(hillshade)
The resulting image is overlaid using Matplotlib's imshow() with geographic extent mapping:
ax.imshow(
hillshade_rgba,
extent=[west, east, south, north], # Geographic bounds
interpolation='bilinear',
zorder=1 # Layer ordering
)
Key Dependencies and Their Roles
| Dependency | Version | Function in Pipeline |
|---|---|---|
| rasterio | ≥1.3 | Reads SRTM GeoTIFF tiles into NumPy arrays |
| numpy | ≥1.21 | Array storage, vectorized math operations |
| matplotlib | ≥3.5 | LightSource hillshade computation; figure rendering |
| requests | ≥2.28 | HTTP client for SRTM tile download |
| scikit-learn | optional | MinMaxScaler for elevation normalization (commented in source) |
The matplotlib.colors.LightSource implementation is the critical dependency. It performs:
- Slope calculation from elevation gradients
- Aspect computation (downhill direction)
- Illumination modeling based on Lambertian reflection
Practical Usage Examples
Basic Hillshade Activation
import prettymaps as pm
# Enable hillshade with default parameters
pm.plot(
center=(46.5197, 6.6323), # Lausanne, Switzerland
style={"hillshade": {}},
size=(800, 600)
)
Custom Sun Position and Exaggeration
import prettymaps as pm
pm.plot(
center=(36.0544, -112.1401), # Grand Canyon
style={
"hillshade": {
"vert_exag": 2.5,
"azimuth": 270, # Sun from west
"altitude": 30 # Lower sun angle = longer shadows
}
},
size=(1200, 800)
)
Programmatic Access to draw_hillshade
from prettymaps.draw import draw_hillshade
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 10))
# Direct usage with prepared elevation path
draw_hillshade(
ax=ax,
elevation_path="./SRTM1/N36W113.hgt",
extent=[-113, -112, 35, 36],
vert_exag=1.5,
azimuth=315,
altitude=45
)
Source File Reference
| File | Lines | Purpose |
|---|---|---|
prettymaps/draw.py |
515–560 | draw_hillshade() function; RGBA conversion and overlay |
prettymaps/fetch.py |
Full | SRTM tile download and caching logic |
prettymaps/utils.py |
Various | Raster metadata extraction (dx, dy calculation) |
Summary
- Elevation data originates from SRTM-1 tiles fetched via HTTP and parsed with Rasterio
- Illumination calculation delegates to
matplotlib.colors.LightSource.hillshade()with configurable sun geometry - RGBA conversion inverts the hillshade for alpha transparency, creating blended relief effects
- Cleanup removes temporary
./SRTM1directory after rendering - The entire pipeline requires only standard scientific Python libraries: NumPy, Rasterio, and Matplotlib
Frequently Asked Questions
What coordinate system does prettymaps use for hillshade data?
Prettymaps uses WGS84 (EPSG:4326) for all geographic operations. SRTM tiles are natively in this CRS, so no reprojection is required during the hillshade pipeline. The extent parameter passed to ax.imshow() uses [west, east, south, north] in decimal degrees.
Can I use custom elevation data instead of SRTM?
Yes. The draw_hillshade function accepts any file path readable by Rasterio. Provide a GeoTIFF with a single elevation band and appropriate geotransform. Set dx and dy explicitly if your raster resolution differs from SRTM's ~30m.
Why does low vert_exag make terrain appear flat?
Vertical exaggeration scales the Z-axis relative to XY. With vert_exag=1.0, slopes appear at true geometric angles. Natural terrain often has gentle gradients; reducing vert_exag below 1.0 compresses relief further, while values of 2.0–3.0 enhance visual distinction of subtle features.
How does the alpha inversion create the blended effect?
By setting alpha = (1 - hillshade) * 255, shadowed areas (low hillshade values) become more transparent. This lets underlying map layers—roads, buildings, water—remain visible in valleys while ridges appear solid and well-defined.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →