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
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_mosaicsources-- static / search-less STAC catalogs (OpenTopography or any catalog by URL):list_stac_collections,stac_collection_items,opentopography_mosaicsources--make_demo_geotifffor quick synthetic test data
Basemaps and overlays (+ contextily)
basemap--plot_dem_basemap_luminosity_relief,add_relief_basemapoverlay--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__))