Skip to content

Instantly share code, notes, and snippets.

@eddy-geek
Created January 14, 2026 21:46
Show Gist options
  • Select an option

  • Save eddy-geek/a2adc1169b40d1f2baddb701427d906e to your computer and use it in GitHub Desktop.

Select an option

Save eddy-geek/a2adc1169b40d1f2baddb701427d906e to your computer and use it in GitHub Desktop.

How to convert terrarrium terrain RGB mbtiles back into tif ?

You can do it with GDAL in two steps:

  1. pick the correct decoder (Terrarium vs Mapbox Terrain‑RGB),
  2. 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.


0) Identify the encoding you have

  • 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]


1) (Optional) Lock to a specific zoom level

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).


2) Decode RGB → elevation and write a GeoTIFF

A) Mapbox Terrain‑RGB decoding

# 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.tif

Formula source (Mapbox / MapTiler): -10000 + ((R*256*256 + G*256 + B) * 0.1). [docs.mapbox.com], [data.maptiler.com]

B) Terrarium decoding

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.tif

Formula 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 by gdal_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 the D term or adapt the mask.
  • Creation options --co and --format=GTiff are supported by gdal_calc. [gdal.org]

3) (Optional) Reproject from Web Mercator to WGS84 (or another CRS)

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.tif

MBTiles rasters are always in Web Mercator; reprojecting with gdalwarp is the standard way to get to EPSG:4326. [gdal.org]


4) (Optional) Clip or control output size

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]

Quick troubleshooting

  • 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=Float32 is set. gdal_calc honors NumPy dtypes and has a --type switch for the output dataset. [gdal.org]
  • Wrong zoom chosen → create the VRT with -oo ZOOM_LEVEL=... as in step 1. [gdal.org]

One‑liner variant (no VRT, take highest zoom automatically)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment