Skip to main content

Python library to download DTM data from various providers.

Project description

Quick Start

Install the package using pip:

pip install pydtmdl

Then, you can use it in your Python scripts:

from pydtmdl import DTMProvider, ImageryProvider, extract_area_from_image
from pydtmdl.imagery_providers.sentinel2 import Sentinel2L2AImagerySettings

# Prepare coordinates of the center point and size (in meters).
coords = 45.285460396731374, 20.237491178279715  # Center point of the region of interest.
size = 2048  # Size of the region in meters (2048x2048 m).

# Get the best provider for the given coordinates.
best_provider = DTMProvider.get_best(coords)
print(f"Best provider: {best_provider.name()}")

# Create an instance of the provider with the given coordinates and size.
provider = best_provider(coords, size=size)

# Get the DTM data as a numpy array.
np_data = provider.image

# Production-oriented rectangular extraction with structured metadata.
result = DTMProvider.extract_area(
    center=coords,
    width_m=4096,
    height_m=2048,
    rotation_deg=30,
    provider_code=best_provider.code(),
    fallback_provider_code="srtm30",
)
print(result.metadata.model_dump())

# Global imagery fallback with Sentinel-2.
imagery = ImageryProvider.extract_area(
    center=coords,
    width_m=4096,
    height_m=2048,
    rotation_deg=30,
    provider_code="sentinel2_l2a",
    user_settings=Sentinel2L2AImagerySettings(max_items=4),
)
print(imagery.metadata.model_dump())

# Extract the same kind of ROI from your own georeferenced raster.
local = extract_area_from_image(
    image_path="my_raster.tif",
    center=coords,
    width_m=4096,
    height_m=2048,
    rotation_deg=30,
)
print(local.metadata.model_dump())

Positive rotation_deg values rotate the requested ROI clockwise around its center.

The legacy square API remains available. If you need machine-readable metadata, cache identifiers, fallback reporting, or rotated ROI extraction, use get_result() on a provider instance or the high-level DTMProvider.extract_area(...) helper.

The same rectangular ROI workflow is also available for imagery providers and for user-provided georeferenced rasters via ImageryProvider.extract_area(...) and extract_area_from_image(...).

For production pipelines, both DTM and imagery extraction also accept min_valid_coverage. When set, pydtmdl rejects outputs with too many no-data pixels and can trigger the configured fallback provider.

Overview

pydtmdl is a Python library designed to provide access to Digital Terrain Models (DTMs) and raster imagery from various providers. It supports multiple providers, each with its own resolution and data format. The library allows users to easily retrieve terrain or imagery data for specific geographic coordinates and sizes.

Note, that some providers may require additional settings, such as API keys or selection of a specific dataset. More details can be found in the demo script and in the providers source code.

The library will retrieve all the required tiles, merge them, window them and return the result as a numpy array. If additional processing is required, such as normalization or resizing, it can be done using OpenCV or other libraries (example code is provided in the demo script).

For application backends and worker pipelines, the library also exposes structured result metadata including cache information, output file paths, provider fallback details, and machine-readable error types.

In addition to remote providers, the library can also extract identical rotated ROIs from your own georeferenced GeoTIFF or COG files using extract_area_from_image(...).

What is a DTM?

First of all, it's important to understand what a DTM is.
There are two main types of elevation models: Digital Terrain Model (DTM) and Digital Surface Model (DSM). The DTM represents the bare earth surface without any objects like buildings or vegetation. The DSM, on the other hand, represents the earth's surface including all objects.

DTM vs DSM, example 1

DTM vs DSM, example 2

The library is focused on the DTM data and the DSM sources are not supported and will not be added in the future. The reason for this is that the DTM data is more suitable for terrain generation in games, as it provides a more accurate representation of the earth's surface without any objects.

Supported DTM providers

coverage map

In addition to SRTM 30m, which provides global coverage, the map above highlights all countries and/or regions where higher resolution coverage is provided by one of the DTM providers.

Provider Name Resolution Developer
🌎 SRTM30 30 meters iwatkot
🌎 ArcticDEM 2 meters kbrandwijk
🌎 REMA Antarctica 2 meters kbrandwijk
🇺🇸 USGS 1-90 meters ZenJakey
🇦🇹 Austria 1 meter iwatkot
🏴󠁧󠁢󠁥󠁮󠁧󠁿 England 1 meter kbrandwijk
🏴󠁧󠁢󠁳󠁣󠁴󠁿 Scotland 0.25-1 meter kbrandwijk
🏴󠁧󠁢󠁷󠁬󠁳󠁿󠁧󠁢󠁷󠁬󠁳󠁿 Wales 1 meter garnwenshared
🇩🇪 Hessen, Germany 1 meter kbrandwijk
🇩🇪 Niedersachsen, Germany 1 meter kbrandwijk
🇩🇪 Bayern, Germany 1 meter H4rdB4se
🇩🇪 Nordrhein-Westfalen, Germany 1 meter kbrandwijk
🇩🇪 Mecklenburg-Vorpommern, Germany 1-25 meter kbrandwijk
🇩🇪 Baden-Württemberg, Germany 1 meter kbrandwijk
🇩🇪 Sachsen-Anhalt, Germany 1 meter kbrandwijk
🇩🇪 Thüringen, Germany 1 meter H4rdB4se
🇨🇦 Canada 1 meter kbrandwijk
🇧🇪 Flanders, Belgium 1 meter kbrandwijk
🇫🇷 France 1 meter kbrandwijk
🇮🇹 Italy 10 meter kbrandwijk
🇳🇴 Norway 1 meter kbrandwijk
🇪🇸 Spain 5 meter kbrandwijk
🇫🇮 Finland 2 meter kbrandwijk
🇩🇰 Denmark 0.4 meter kbrandwijk
🇸🇪 Sweden 1 meter GustavPersson
🇨🇭 Switzerland 0.5-2 meter kbrandwijk
🇨🇿 Czech Republic 5 meter kbrandwijk
🇨🇿 Czech Republic 2 meter VidhosticeSDK
🇱🇹 Lithuania 1 meter Tox3

Supported Imagery Providers

The imagery stack uses the same rectangular ROI API as the DTM stack, including rotation, structured metadata, and local caching.

Provider Name Coverage Resolution Notes
Sentinel-2 L2A RGB Global land coverage 10 meters Free global fallback built from public STAC + COG assets
NAIP RGB Contiguous United States 0.3-0.6 m High-resolution USDA orthophotos, public domain
North Rhine-Westphalia DOP RGB North Rhine-Westphalia, Germany 0.1 m State orthophoto WMS
Bavaria DOP20 RGB Bavaria, Germany 0.2 m State orthophoto WMS
Hessen DOP20 RGB Hessen, Germany 0.2 m State orthophoto WMS
Lower Saxony DOP20 RGB Lower Saxony, Germany 0.2 m State orthophoto WMS
Thuringia DOP20 RGB Thuringia, Germany 0.2 m State orthophoto WMS

Example imagery usage:

from pydtmdl import ImageryProvider
from pydtmdl.imagery_providers.naip import NAIPImagerySettings

result = ImageryProvider.extract_area(
    center=(40.03, -105.22),
    width_m=2048,
    height_m=2048,
    provider_code="naip",
    min_valid_coverage=0.98,
    user_settings=NAIPImagerySettings(
        date_from="2020-01-01",
        date_to="2026-12-31",
        max_items=8,
    ),
)
print(result.metadata.model_dump())

Example local raster usage:

from pydtmdl import extract_area_from_image

result = extract_area_from_image(
    image_path="my_raster.tif",
    center=(45.285460396731374, 20.237491178279715),
    width_m=4096,
    height_m=2048,
    rotation_deg=30,
)
print(result.metadata.model_dump())

Postprocessing

pydtmdl provides standalone postprocessing helpers that run outside of the provider extraction pipeline. This makes it easy to keep source extraction unchanged while applying production-specific normalization and PNG export only where needed.

Available functions:

  • postprocess_dtm(...)
  • postprocess_imagery(...)
  • export_single_channel_png(...)
  • postprocess_dtm_to_png(...)

Quick DTM example:

from pydtmdl import postprocess_dtm

processed, metadata = postprocess_dtm(
    dtm_array,
    normalize_to_dtype=True,
    target_dtype="uint16",
    apply_zero_floor=True,
    zero_floor_value=35000,
)

Quick one-step PNG export example (single channel):

from pydtmdl import postprocess_dtm_to_png

processed, post_meta, png_meta = postprocess_dtm_to_png(
    dtm_array,
    "output/dtm.png",
    normalize_to_dtype=True,
    target_dtype="uint16",
    apply_zero_floor=True,
    zero_floor_value=35000,
    png_dtype="uint16",
)

See the full guide in POSTPROCESSING.md.

Licensing and Data Usage

⚠️ Important: This library provides access to DTM and imagery data from various third-party providers. PyDTMDL does not own, host, or distribute this data. Each provider has its own licensing terms and usage restrictions.

It is your responsibility to:

  • Check the license and terms of use for each DTM or imagery provider you use
  • Ensure compliance with the provider's licensing requirements
  • Verify that your use case (commercial, research, personal, etc.) is permitted
  • Provide proper attribution when required by the data provider
  • Respect any usage limits or restrictions imposed by the provider

The library itself is licensed under the GNU Affero General Public License v3 (AGPL-3.0), but this does not grant you any rights to the DTM data accessed through the library. The data licenses are separate and must be obtained directly from the respective providers.

By using this library, you acknowledge that you are solely responsible for ensuring compliance with all applicable data licenses and terms of use.

For information about data licensing from specific providers, please refer to their official websites and documentation.

Contributing

Contributions are welcome! If you want to add your own DTM or imagery provider, please follow this guide.
You can also contribute by reporting issues, suggesting improvements, or helping with documentation.

What a DTM provider does?

A DTM provider is a service that provides elevation data for a given location. While there's plenty of DTM providers available, only the ones that provide a free and open access to their data can be used in this library.

The base provider class, DTMProvider, handles all the heavy lifting: merging tiles, reprojecting to EPSG:4326, and extracting the region of interest. Individual DTM providers only need to implement the download_tiles() method to fetch the raw data.

The process for generating elevation data is:

  1. Download all DTM tiles for the desired map area (implemented by each DTM provider)
  2. Merge multiple tiles if necessary (handled by base class)
  3. Reproject to EPSG:4326 if needed (handled by base class)
  4. Extract the map area from the tile (handled by base class)

What an imagery provider does?

An imagery provider follows the same ROI extraction contract, but returns raster imagery instead of elevation values. The base imagery class, ImageryProvider, reuses the same crop, rotation, caching and metadata patterns as DTMProvider.

The process for generating imagery data is:

  1. Download or locate all imagery tiles covering the desired map area
  2. Merge multiple source tiles if necessary
  3. Reproject to EPSG:4326 if needed
  4. Extract the rotated ROI from the merged raster

If you already have your own georeferenced raster file, use extract_area_from_image(...) instead of implementing a provider.

Provider Types

There are three main approaches to implementing a DTM provider:

  1. Custom implementation - Inherit from DTMProvider directly for unique APIs
  2. WCS-based - Inherit from both WCSProvider and DTMProvider for OGC WCS services
  3. WMS-based - Inherit from both WMSProvider and DTMProvider for OGC WMS services

For imagery providers, use ImageryProvider for custom STAC/COG-style integrations or WMSImageryProvider for WMS orthophoto services. Examples live in pydtmdl/imagery_providers/.

Example 1: Custom Provider (SRTM)

➡️ Existing providers can be found in the pydtmdl/providers/ folder.

Step 1: Define the provider metadata.

from pydtmdl.base.dtm import DTMProvider

class SRTM30Provider(DTMProvider):
    """Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""

    _code = "srtm30"
    _name = "SRTM 30 m"
    _region = "Global"
    _icon = "🌎"
    _resolution = 30.0
    _url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"

Step 2 (optional): Define custom settings if your provider requires authentication or configuration.

from pydtmdl.base.dtm import DTMProviderSettings

class SwedenProviderSettings(DTMProviderSettings):
    """Settings for the Sweden provider."""
    username: str = ""
    password: str = ""

class SwedenProvider(DTMProvider):
    _settings = SwedenProviderSettings
    _instructions = "ℹ️ This provider requires username and password..."

Access settings in your code:

username = self.user_settings.username
password = self.user_settings.password

Step 3: Implement the download_tiles() method.

def download_tiles(self) -> list[str]:
    """Download SRTM tiles."""
    north, south, east, west = self.get_bbox()
    
    tiles = []
    for pair in [(north, east), (south, west), (south, east), (north, west)]:
        tile_parameters = self.get_tile_parameters(*pair)
        tile_name = tile_parameters["tile_name"]
        tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
        
        if not os.path.isfile(tile_path):
            # Download and decompress tile
            compressed_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
            if not self.download_tile(compressed_path, **tile_parameters):
                raise FileNotFoundError(f"Tile {tile_name} not found.")
            # ... decompress logic ...
        
        tiles.append(tile_path)
    return list(set(tiles))

Example 2: WCS Provider (England)

For WCS-based providers, inherit from both WCSProvider and DTMProvider. The base class handles coordinate transformation automatically using the transform_bbox() utility from pydtmdl/utils.py.

from pydtmdl.base.dtm import DTMProvider
from pydtmdl.base.wcs import WCSProvider

class England1MProvider(WCSProvider, DTMProvider):
    """Provider of England data."""
    
    _code = "england1m"
    _name = "England DGM1"
    _region = "UK"
    _icon = "🏴󠁧󠁢󠁥󠁮󠁧󠁿"
    _resolution = 1.0
    _extents = [(55.877, 49.851, 2.084, -7.105)]
    
    _url = "https://environment.data.gov.uk/geoservices/datasets/.../wcs"
    _wcs_version = "2.0.1"
    _source_crs = "EPSG:27700"  # British National Grid
    _tile_size = 1000
    
    def get_wcs_parameters(self, tile):
        return {
            "identifier": ["dataset_id"],
            "subsets": [("E", str(tile[1]), str(tile[3])), ("N", str(tile[0]), str(tile[2]))],
            "format": "tiff",
        }

The WCSProvider base class automatically:

  • Transforms your bbox from EPSG:4326 to _source_crs
  • Tiles the area based on _tile_size
  • Downloads each tile using your get_wcs_parameters() method
  • Returns the list of downloaded files

Example 3: Custom API with Authentication (Sweden)

For providers with custom APIs requiring authentication:

class SwedenProvider(DTMProvider):
    _settings = SwedenProviderSettings  # Define custom settings
    
    def download_tiles(self):
        """Download tiles from STAC API."""
        download_urls = self.get_download_urls()
        return self.download_tif_files(download_urls, self.shared_tiff_path)
    
    def _get_auth_headers(self) -> dict[str, str]:
        """Generate auth headers from user settings."""
        credentials = f"{self.user_settings.username}:{self.user_settings.password}"
        encoded = base64.b64encode(credentials.encode()).decode()
        return {"Authorization": f"Basic {encoded}"}
    
    def get_download_urls(self) -> list[str]:
        """Query STAC API for tile URLs within bbox."""
        bbox = self.get_bbox()
        # ... API logic using self._get_auth_headers() ...
        return urls

Unified Download Methods

⚠️ Important: All DTM providers must use the unified download methods provided by the base DTMProvider class. Do not implement your own download logic.

The base class provides three unified download methods with built-in retry logic, error handling, and progress tracking:

1. download_tif_files(urls, output_path, headers=None, timeout=60)

For downloading multiple GeoTIFF files from a list of URLs.

def download_tiles(self) -> list[str]:
    download_urls = self.get_download_urls()
    return self.download_tif_files(download_urls, self.shared_tiff_path)

Use for: Simple URL-based downloads (SRTM, Scotland, Wales, etc.)

2. download_file(url, output_path, headers=None, method='GET', data=None, timeout=60)

For downloading a single file with flexible HTTP methods (GET/POST).

def download_tiles(self) -> list[str]:
    url = self.formatted_url(**tile_parameters)
    output_path = os.path.join(self._tile_directory, "tile.tif")
    self.download_file(url, output_path, method="POST", data=polygon_data)
    return [output_path]

Use for: Single file downloads or POST requests (Bavaria, custom APIs)

3. download_tiles_with_fetcher(tiles, output_path, data_fetcher, file_name_generator=None)

For OGC Web Services (WCS/WMS) or any service requiring custom data fetching.

def download_tiles(self) -> list[str]:
    bbox = self.get_bbox()
    bbox = transform_bbox(bbox, self._source_crs)
    tiles = tile_bbox(bbox, self._tile_size)
    
    wcs = WebCoverageService(self._url, version=self._wcs_version)
    
    def wcs_fetcher(tile):
        return wcs.getCoverage(**self.get_wcs_parameters(tile))
    
    return self.download_tiles_with_fetcher(tiles, self.shared_tiff_path, wcs_fetcher)

Use for: WCS/WMS providers (automatically handled by WCSProvider/WMSProvider base classes)

Why Use Unified Methods?

  • Built-in retry logic - Automatic retries with configurable attempts and delays
  • Error handling - Consistent error messages and logging
  • Progress tracking - Visual progress bars with tqdm
  • File caching - Skips already downloaded files
  • Timeout support - Configurable timeouts for slow connections
  • Authentication - Support for custom headers (API keys, Basic Auth, etc.)

If you need functionality not provided by these methods, extend the base class methods rather than implementing your own. This ensures all providers benefit from improvements and bug fixes.

Other Helper Methods

The base DTMProvider class also provides:

  • get_bbox() - Returns (north, south, east, west) in EPSG:4326
  • unzip_img_from_tif(file_name, output_path) - Extracts .img or .tif from zip files
  • _tile_directory - Temporary directory for your provider's tiles
  • _max_retries - Number of retry attempts (default: 5)
  • _retry_pause - Seconds between retries (default: 5)

For coordinate transformation, use the utility function from pydtmdl/utils.py:

from pydtmdl.utils import transform_bbox
bbox = self.get_bbox()
transformed_bbox = transform_bbox(bbox, "EPSG:25832")

Requirements

  • Providers must be free and openly accessible
  • If authentication is required, users must provide their own credentials via settings
  • The download_tiles() method must return a list of file paths to GeoTIFF files
  • All tiles should contain valid elevation data readable by rasterio

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydtmdl-0.3.2.tar.gz (80.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydtmdl-0.3.2-py3-none-any.whl (89.4 kB view details)

Uploaded Python 3

File details

Details for the file pydtmdl-0.3.2.tar.gz.

File metadata

  • Download URL: pydtmdl-0.3.2.tar.gz
  • Upload date:
  • Size: 80.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pydtmdl-0.3.2.tar.gz
Algorithm Hash digest
SHA256 7f077cbff735e20d9f11c07e1ad3685119d519674000cfbbd023a5d8939c31e4
MD5 fee9eb02a8e138f5dcaba57d55d856fa
BLAKE2b-256 33bd381ade4c466ab63403528061fc92eef956f29798ee5830eadbe7f42fe3cc

See more details on using hashes here.

File details

Details for the file pydtmdl-0.3.2-py3-none-any.whl.

File metadata

  • Download URL: pydtmdl-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 89.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pydtmdl-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 0eea95b2cb504582600f5b7bb91e08a63c6c6d0b3107f3604d16f16b2ac36db9
MD5 ade33fb44b4ffa1103de222e406d8b79
BLAKE2b-256 fc5bc7e7daddc9b485c843b9b88f8d3361103c4e7bf9106ad1f115e431574887

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page