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:

  1. Download SRTM-1 elevation tiles via the internal fetch module
  2. Read raster data into NumPy arrays using Rasterio
  3. Compute illumination values with matplotlib.colors.LightSource.hillshade()
  4. 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 ./SRTM1 directory 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →