Tiling Orthomosaics for Web Map Delivery
A Cloud-Optimized GeoTIFF streams beautifully to a client that speaks GDAL, but the moment you want a Roman villa survey to appear in a public Leaflet map, an offline field tablet, or a partner’s MapLibre viewer, you need a tile pyramid: the orthomosaic pre-rendered into millions of 256-pixel images addressed by zoom, column, and row. Tiling trades storage and build time for the simplest possible client — a plain HTTP directory or a single portable file that any web map can consume with no server logic. This guide, part of orthomosaic generation and tiling, covers gdal2tiles.py, the XYZ-versus-TMS distinction that catches everyone, zoom-range selection, MBTiles packaging, and serving.
Context & When to Use
Tile when the consumer is a browser or a field device, not a GIS analyst. An XYZ pyramid needs no tile server — a static file host or CDN serves the {z}/{x}/{y}.png directory directly, which is why it suits public-facing heritage viewers and low-cost hosting. The costs are real: a full-resolution 2 cm survey tiled to native zoom can generate hundreds of thousands of tiles and take significant build time, and every re-survey means a full rebuild. Two decisions govern the outcome. First, zoom range: the maximum zoom should match your GSD — tiling past the point where one tile pixel is finer than one ortho pixel just upsamples blur and multiplies file count for nothing. Second, XYZ versus TMS tile addressing: both schemes lay out identical tiles but number the Y axis in opposite directions. XYZ (Google/OSM/Leaflet/MapLibre default) counts rows from the top; TMS (the OGC-rooted scheme) counts from the bottom. Mismatch the two and your map renders upside-down in the vertical axis. gdal2tiles.py defaults to --xyz in modern GDAL, which is what web clients expect.
Implementation
Tile the validated COG from the Cloud-Optimized GeoTIFF guide. Web tile pyramids are served in Web Mercator, so the source is reprojected to EPSG:3857; keep the original in your site datum (EPSG:27700 — # substitute your site's EPSG) as the master and treat the tiles as a derivative.
# GDAL 3.8.4
# Reproject the COG to Web Mercator once, then tile
gdalwarp -t_srs EPSG:3857 -r bilinear site_ortho_cog.tif site_ortho_3857.tif
gdal2tiles.py --xyz --zoom=14-22 --processes=8 \
--tilesize=256 --resampling=average \
site_ortho_3857.tif ./tiles/
The --xyz flag forces top-origin addressing; omit it only if your client explicitly wants TMS. --zoom=14-22 should end at your GSD-matched level. To automate the whole step and pack the result into a single portable file for the field, drive GDAL from Python and let it write MBTiles — a SQLite container holding every tile in one file, ideal for offline sync to a tablet:
# requirements.txt
# GDAL Python bindings 3.8.4
# GDAL 3.8.4 (osgeo.gdal)
import subprocess
from pathlib import Path
def tile_to_mbtiles(cog: Path, mbtiles: Path, zmin: int, zmax: int) -> None:
"""Produce an MBTiles pyramid from a COG for offline field delivery."""
# gdal_translate writes MBTiles directly; ZOOM_LEVEL_STRATEGY keeps
# the top level from over-decimating below the site extent.
subprocess.run([
"gdal_translate", "-of", "MBTILES",
"-co", "TILE_FORMAT=PNG", # lossless tiles; use JPEG for smaller visual base
"-co", f"ZOOM_LEVEL_STRATEGY=AUTO",
str(cog), str(mbtiles),
], check=True)
# Build the pyramid inside the MBTiles container
subprocess.run([
"gdaladdo", "-r", "average", str(mbtiles),
*[str(2 ** n) for n in range(1, zmax - zmin + 1)],
], check=True)
if __name__ == "__main__":
tile_to_mbtiles(Path("site_ortho_3857.tif"), Path("site_ortho.mbtiles"), 14, 22)
For a directory pyramid, gdal2tiles.py also writes a ready leaflet.html and openlayers.html preview into the output folder — open it to sanity-check the layer before wiring it into your own viewer. Serve the ./tiles/ directory from any static host and point a Leaflet layer at https://host/tiles/{z}/{x}/{y}.png.
Verification
Confirm the pyramid structure and a sample tile before publishing. The directory scheme should contain one folder per zoom level:
ls tiles/ | sort -n
# expected: 14 15 16 17 18 19 20 21 22
Check a mid-zoom tile decodes and has the right dimensions:
# GDAL 3.8.4
gdalinfo tiles/18/130026/86545.png | grep "Size is"
# expected: Size is 256, 256
For the MBTiles container, query its metadata table to assert the zoom bounds landed as intended:
sqlite3 site_ortho.mbtiles "SELECT name, value FROM metadata WHERE name IN ('minzoom','maxzoom','format');"
# expected:
# minzoom|14
# maxzoom|22
# format|png
Finally, load the layer in the generated leaflet.html and confirm the orthomosaic sits over its true location on the base map — a north-up, correctly-placed site is the real acceptance test that XYZ/TMS addressing is right.
Common Errors & Fixes
ERROR 1: Input file must have a georeferencingfromgdal2tiles.py— you fed it a plain image or a raster whose CRS was stripped. Rungdalinfoto confirm aCoordinate System is:block, and reproject toEPSG:3857first.- Map renders upside-down / tiles appear at wrong rows — XYZ/TMS mismatch. Re-run with
--xyzfor Leaflet/MapLibre, or set your client to TMS if the tiles were built without it. ERROR 1: Attempt to create MBTiles dataset ... but content is not in EPSG:3857— MBTiles only accepts Web Mercator. Reproject withgdalwarp -t_srs EPSG:3857before the translate step; tiling a native-datum raster straight to MBTiles always fails here.
Related
- Orthomosaic Generation and Tiling for Archaeological Site Maps — the section overview linking orthorectification to delivery.
- Generating Cloud-Optimized GeoTIFF Orthomosaics — producing the validated COG this guide tiles.
- Automated Drone Image Processing Workflows — the survey processing upstream of tiling.
- Exporting 3D Models to Spatial Databases — pairing tiled base maps with 3D archive records.