You can do it with GDAL in two steps:
- pick the correct decoder (Terrarium vs Mapbox Terrain‑RGB),
- apply the formula to the RGB PNG tiles inside the MBTiles and write out a Float32 GeoTIFF (then optionally reproject to WGS84).
Below I give ready‑to‑run commands for both encodings, plus tips to choose the zoom level, mask with the alpha band, and reproject.
-
Mapbox “Terrain‑RGB” tiles decode as:
height_m = (R*256*256 + G*256 + B)*0.1 - 10000(units: meters, precision 0.1 m). Common sources: Mapbox Terrain‑RGB, MapTiler Terrain RGB. [docs.mapbox.com], [data.maptiler.com] -
Mapzen/Nextzen “Terrarium” tiles decode as:
height_m = (R*256 + G + B/256) - 32768(units: meters, ~3.9 mm precision). Common sources: AWS Terrain Tiles (terrarium), legacy Mapzen. [mapzen.com], [docs.safe.com]
If your MBTiles came from Mapbox/MapTiler, use Terrain‑RGB. If it came from AWS Terrain Tiles / (Next)Mapzen, use Terrarium. [data.maptiler.com], [mapzen.com]
FYI, the GDAL MBTiles driver opens these datasets in EPSG:3857 (Web Mercator) and exposes 4 bands (R,G,B,Alpha) by default; you can force a specific ZOOM_LEVEL when opening. [gdal.org]
If your MBTiles contains multiple zooms, create a small VRT that pins one level (replace 14 with what you need):
gdal_translate -of VRT -oo ZOOM_LEVEL=14 input.mbtiles z14.vrt(You can also run gdalinfo input.mbtiles first to see available zooms.) The ZOOM_LEVEL open option is documented in the GDAL MBTiles driver. [gdal.org]
If you don’t care about the zoom, you can just use input.mbtiles directly in the next step (it will use the highest available zoom).
# Uses alpha (band 4) to set NoData
gdal_calc.py \
-A z14.vrt --A_band=1 \
-B z14.vrt --B_band=2 \
-C z14.vrt --C_band=3 \
-D z14.vrt --D_band=4 \
--calc="where(D==0, -32768, (A.astype(float)*65536 + B.astype(float)*256 + C.astype(float))*0.1 - 10000.0)" \
--type=Float32 --NoDataValue=-32768 \
--format=GTiff --co=TILED=YES --co=COMPRESS=DEFLATE \
--outfile=elev_3857.tifFormula source (Mapbox / MapTiler): -10000 + ((R*256*256 + G*256 + B) * 0.1). [docs.mapbox.com], [data.maptiler.com]
gdal_calc.py \
-A z14.vrt --A_band=1 \
-B z14.vrt --B_band=2 \
-C z14.vrt --C_band=3 \
-D z14.vrt --D_band=4 \
--calc="where(D==0, -32768, (A.astype(float)*256 + B.astype(float) + C.astype(float)/256.0) - 32768.0)" \
--type=Float32 --NoDataValue=-32768 \
--format=GTiff --co=TILED=YES --co=COMPRESS=DEFLATE \
--outfile=elev_3857.tifFormula source (Mapzen/Nextzen Terrarium): (R*256 + G + B/256) - 32768. [mapzen.com], [docs.safe.com]
Notes:
- I used
A.astype(float)etc. to avoid 8‑bit overflow during the math (this is supported bygdal_calc, which evaluates NumPy expressions). [gdal.org]- The
where(D==0, …)masks pixels where the alpha channel is 0 (outside coverage). If your tiles lack alpha, drop theDterm or adapt the mask.- Creation options
--coand--format=GTiffare supported bygdal_calc. [gdal.org]
The GeoTIFF produced above is in EPSG:3857 (because that’s what MBTiles uses). Reproject if you want geographic lat/long:
gdalwarp -t_srs EPSG:4326 -r bilinear \
-dstnodata -32768 \
-co TILED=YES -co COMPRESS=DEFLATE \
elev_3857.tif elev_wgs84.tifMBTiles rasters are always in Web Mercator; reprojecting with gdalwarp is the standard way to get to EPSG:4326. [gdal.org]
World‑scale MBTiles at max zoom can explode in size as a single GeoTIFF. You can:
- Clip to an area of interest while warping:
gdalwarp -te minX minY maxX maxY ... - Set output resolution during warp:
gdalwarp -tr <x_res> <y_res> ...
(standard GDAL options; use with care to keep meaningful DEM resolution). [gdal.org]
- Heights look like ~‑10000 everywhere → You probably used the Terrain‑RGB formula on a Terrarium source (or your ocean areas are dominating). Try the other formula. [data.maptiler.com], [mapzen.com]
- Output stays byte/int → ensure the expression casts to float (as shown) and
--type=Float32is set.gdal_calchonors NumPy dtypes and has a--typeswitch for the output dataset. [gdal.org] - Wrong zoom chosen → create the VRT with
-oo ZOOM_LEVEL=...as in step 1. [gdal.org]
Just replace z14.vrt with input.mbtiles in the gdal_calc.py command. The MBTiles driver will pick the max zoom that exists. [gdal.org]
If you tell me which encoding your MBTiles uses (Terrarium or Terrain‑RGB), the area/zoom you want, and your target CRS, I can tailor a single copy‑paste script (or a small makefile) to produce the exact GeoTIFF you need.