TerraTexture

Curvature-driven shaded relief that brings out the texture of terrain.

What is it?

TerraTexture turns a digital elevation model (DEM) into relief imagery that shows shape, not just height. It computes terrain derivatives (slope, aspect, profile and plan curvature, hillshade) and combines them with Photoshop/SVG-style blend modes such as soft light and luminosity, so ridges, gullies, moraines and breaks of slope stand out far more clearly than in a plain hillshade. The result can be plotted on its own, draped over a web basemap, or used as a backdrop for your own data.

Why?

TerraTexture grew out of a plotting tool I built during my PhD. Working with DEMs in polar regions it seemed a shame not to use those DEMs to show the texture of the surfaces they describe. Default basemaps hide much of the real topographic complexity, especially over ice, where the imagery is often a near-uniform white.

The classic GIS fix is to burn basemap imagery onto a hillshade. TerraTexture instead blends the basemap with a composite of three layers: curvature, hillshade and the raw DEM. Sharp, high-relief features come out bright, while smooth, low-relief surfaces fall darker, so the texture of the terrain carries through the imagery rather than being flattened by it.

The project also became a chance to explore Rust for performance. Polar DEM mosaics at around 10 m resolution span millions of square kilometres, which means hundreds of billions of pixels, so both memory use and speed matter. The heavy computations run in a Rust core that releases Python's Global Interpreter Lock (GIL), so the work can be split across threads and use every CPU core rather than one.

The result is a small, dependency-light toolkit that does this reproducibly. It pulls DEMs from open data, namely the OpenTopography STAC collection and the Polar Geospatial Center (PGC) mosaics, and uses the open-source contextily library for basemap imagery.

Before / after relief blend

↔
Basemap only
With relief blend

Example

import TerraTexture as tt

path = tt.sources.make_demo_geotiff("demo.tif")
dem = tt.io.load_dem(path)
fig = tt.plotting.plot_dem_curvature_softlight(dem)

Core breakdown

Each group is importable on its own. Submodules are loaded lazily, so import TerraTexture never pulls in rasterio or contextily until you touch a module that needs them.

Analysis and blending (numpy + scipy only)

  • derivatives -- curvatures, hillshade (Zevenbergen & Thorne, 1987)
  • blend -- soft_light, luminosity_blend (Photoshop / SVG blend modes)
  • stretch -- normalize, stretch_std (percentile and standard-deviation stretches)
  • plotting -- plot_dem_curvature_softlight (6-panel summary figure)

Raster I/O (+ rasterio)

  • io -- load_dem, load_dem_mosaic, _open_raster

Open-data DEM sources (+ rasterio, requests)

  • sources -- dynamic STAC APIs (PGC): stac_search, arcticdem_mosaic, rema_mosaic
  • sources -- static / search-less STAC catalogs (OpenTopography or any catalog by URL): list_stac_collections, stac_collection_items, opentopography_mosaic
  • sources -- make_demo_geotiff for quick synthetic test data

Basemaps and overlays (+ contextily)

  • basemap -- plot_dem_basemap_luminosity_relief, add_relief_basemap
  • overlay -- burn_data_onto_relief

Command line

  • cli -- command-line entry point
  1"""
  2
  3**Curvature-driven shaded relief that brings out the texture of terrain.**
  4
  5# What is it?
  6
  7TerraTexture turns a digital elevation model (DEM) into relief imagery
  8that shows *shape*, not just height. It computes terrain derivatives
  9(slope, aspect, profile and plan curvature, hillshade) and combines them
 10with Photoshop/SVG-style blend modes such as soft light and luminosity,
 11so ridges, gullies, moraines and breaks of slope stand out far more
 12clearly than in a plain hillshade. The result can be plotted on its own,
 13draped over a web basemap, or used as a backdrop for your own data.
 14
 15# Why?
 16
 17TerraTexture grew out of a plotting tool I built during my PhD.
 18Working with DEMs in polar regions it seemed a shame not to use
 19those DEMs to show the texture of the surfaces they describe.
 20Default basemaps hide much of the real topographic complexity,
 21especially over ice, where the imagery is often a near-uniform white.
 22
 23The classic GIS fix is to burn basemap imagery onto a hillshade.
 24TerraTexture instead blends the basemap with a composite of three
 25layers: curvature, hillshade and the raw DEM. Sharp, high-relief
 26features come out bright, while smooth, low-relief surfaces fall
 27darker, so the texture of the terrain carries through the imagery
 28rather than being flattened by it.
 29
 30The project also became a chance to explore Rust for performance.
 31Polar DEM mosaics at around 10 m resolution span millions of square
 32kilometres, which means hundreds of billions of pixels, so both
 33memory use and speed matter. The heavy computations run in a Rust
 34core that releases Python's Global Interpreter Lock (GIL), so the
 35work can be split across threads and use every CPU core rather
 36than one.
 37
 38The result is a small, dependency-light toolkit that does this
 39reproducibly. It pulls DEMs from open data, namely the
 40OpenTopography STAC collection and the Polar Geospatial Center (PGC)
 41mosaics, and uses the open-source contextily library for basemap
 42imagery.
 43
 44<!-- terratexture:example-html -->
 45
 46# Example
 47
 48```python
 49import TerraTexture as tt
 50
 51path = tt.sources.make_demo_geotiff("demo.tif")
 52dem = tt.io.load_dem(path)
 53fig = tt.plotting.plot_dem_curvature_softlight(dem)
 54```
 55
 56# Core breakdown
 57
 58Each group is importable on its own. Submodules are loaded lazily, so
 59``import TerraTexture`` never pulls in rasterio or contextily until you
 60touch a module that needs them.
 61
 62### Analysis and blending (numpy + scipy only)
 63
 64- `derivatives` -- `curvatures`, `hillshade`
 65  (Zevenbergen & Thorne, 1987)
 66- `blend` -- `soft_light`, `luminosity_blend`
 67  (Photoshop / SVG blend modes)
 68- `stretch` -- `normalize`, `stretch_std`
 69  (percentile and standard-deviation stretches)
 70- `plotting` -- `plot_dem_curvature_softlight` (6-panel summary figure)
 71
 72### Raster I/O (+ rasterio)
 73
 74- `io` -- `load_dem`, `load_dem_mosaic`, `_open_raster`
 75
 76### Open-data DEM sources (+ rasterio, requests)
 77
 78- `sources` -- dynamic STAC APIs (PGC): `stac_search`,
 79  `arcticdem_mosaic`, `rema_mosaic`
 80- `sources` -- static / search-less STAC catalogs (OpenTopography or any
 81  catalog by URL): `list_stac_collections`, `stac_collection_items`,
 82  `opentopography_mosaic`
 83- `sources` -- `make_demo_geotiff` for quick synthetic test data
 84
 85### Basemaps and overlays (+ contextily)
 86
 87- `basemap` -- `plot_dem_basemap_luminosity_relief`, `add_relief_basemap`
 88- `overlay` -- `burn_data_onto_relief`
 89
 90### Command line
 91
 92- `cli` -- command-line entry point
 93"""
 94
 95from __future__ import annotations
 96
 97import importlib
 98from importlib import resources
 99from types import ModuleType
100from typing import TYPE_CHECKING
101
102if TYPE_CHECKING:  # Real imports for IDEs / type checkers only.
103    from . import (  # noqa: F401
104        basemap,
105        blend,
106        cli,
107        derivatives,
108        io,
109        overlay,
110        plotting,
111        sources,
112        stretch,
113    )
114
115
116# ---------------------------------------------------------------------------
117# Package metadata
118# ---------------------------------------------------------------------------
119
120__version__ = "0.1.0"
121
122__all__ = [
123    "basemap",
124    "blend",
125    "cli",
126    "derivatives",
127    "io",
128    "overlay",
129    "plotting",
130    "sources",
131    "stretch",
132]
133
134
135# ---------------------------------------------------------------------------
136# Docstring example: splice the before/after HTML widget in from a file
137# ---------------------------------------------------------------------------
138
139# Marker in the module docstring that is replaced by the HTML file contents.
140_EXAMPLE_MARKER = "<!-- terratexture:example-html -->"
141
142# Package-relative path of the interactive before/after comparison widget.
143_EXAMPLE_HTML = "assets/before_after.html"
144
145
146def _load_example_html() -> str:
147    """
148    Read the before/after comparison widget shipped with the package.
149
150    Returns:
151        str: The raw HTML, or an empty string if the asset is missing
152            (e.g. an install that did not include package data), so a
153            broken asset never breaks ``import TerraTexture``.
154    """
155    try:
156        asset = resources.files(__name__).joinpath(_EXAMPLE_HTML)
157        return asset.read_text(encoding="utf-8")
158    except (FileNotFoundError, OSError):
159        return ""
160
161
162if __doc__:  # ``python -OO`` strips docstrings, leaving ``None``.
163    __doc__ = __doc__.replace(_EXAMPLE_MARKER, _load_example_html())
164
165
166# ---------------------------------------------------------------------------
167# Lazy submodule loading (PEP 562)
168# ---------------------------------------------------------------------------
169
170def __getattr__(name: str) -> ModuleType:
171    """
172    Import a public submodule on first attribute access.
173
174    This keeps optional heavy dependencies (rasterio, contextily,
175    requests) out of ``import TerraTexture`` until they are actually used.
176
177    Args:
178        name (str): Attribute being looked up on the package.
179
180    Returns:
181        ModuleType: The imported submodule.
182
183    Raises:
184        AttributeError: If ``name`` is not a public submodule.
185    """
186    if name in __all__:
187        module = importlib.import_module(f".{name}", __name__)
188        globals()[name] = module  # Cache so __getattr__ isn't hit again.
189        return module
190    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
191
192
193def __dir__() -> list[str]:
194    """
195    List package attributes, including not-yet-imported submodules.
196
197    Returns:
198        list[str]: Sorted attribute names for tab completion.
199    """
200    return sorted(set(globals()) | set(__all__))