Setting Compression and Overviews for Orthomosaic COGs
You have a structurally valid Cloud-Optimized GeoTIFF, but it is either far larger than it should be or it stutters when a web viewer pans and zooms. Both symptoms come down to two tuning decisions that COG validation does not check: which compression codec and predictor you applied, and how deep and with what resampling you built the overview pyramid. Pick DEFLATE with the wrong predictor and a float DEM stays twice its necessary size; pick nearest resampling on a photographic orthomosaic and zoomed-out tiles look blocky; stop the pyramid two levels short and a whole-survey view drags full-resolution pixels across the wire. This page is the decision guide: how to match codec, predictor, and pyramid to the specific raster in front of you.
Why compression and overview choices depend on the raster type
The right answer is different for an 8-bit RGB orthomosaic and a float32 elevation surface, and getting it wrong is silent — the file is valid either way. Two mechanics drive every choice.
Compression and predictors exploit local similarity. Lossless codecs (DEFLATE, ZSTD, LZW) shrink data by finding repeated byte patterns. A predictor pre-transforms the pixels to create those patterns: predictor 2 stores each pixel as its horizontal difference from the previous one, which turns a smoothly varying integer band into long runs of small numbers. Predictor 3 does the same but is built for IEEE floating-point layout — applying predictor 2 to float32 elevation does almost nothing because it differences the raw byte representation, not the values. So an RGB ortho wants predictor 2 and a float DEM wants predictor 3. For a purely visual RGB ortho you can also drop losslessness entirely and use JPEG, which discards high-frequency detail the eye barely notices and shrinks files several-fold — but JPEG on a DEM would corrupt the elevations you are trying to measure.
Overviews trade file size for zoom-out speed. Each pyramid level is a downsampled copy, adding roughly a third to file size for the full set, in exchange for letting a client read a coarse tile instead of decoding full-resolution pixels when zoomed out. The resampling method must suit the data: average (or bilinear) blends neighbouring pixels, which is correct for continuous data — a photographic ortho or a DEM — while nearest and mode preserve exact original values, which is what a categorical raster (a classification, a palettized mask) needs so downsampling does not invent classes that never existed.
This tuning sits on top of the structural export covered in exporting Cloud-Optimized GeoTIFFs with Rasterio, which is the export stage of the wider DEM/DSM raster export pipeline.
Minimal reproducible solution
The function below picks codec, predictor, and resampling from the raster’s dtype and a visual flag, then writes the COG. It encodes the decision tree from the diagram so the choice is explicit and testable, not buried in a config file.
import rasterio
from rio_cogeo.cogeo import cog_translate
from rio_cogeo.profiles import cog_profiles
def tuned_cog(src_path: str, dst_path: str, *, visual: bool = False) -> None:
"""Choose compression/predictor/resampling from the raster type, then write."""
with rasterio.open(src_path) as src:
dtype = src.dtypes[0]
bands = src.count
colormap = src.colormap(1) if bands == 1 else None
if dtype.startswith("float"): # DEM / DSM
profile = cog_profiles.get("deflate")
creation = {"predictor": 3} # float predictor
resampling = "average" # continuous surface
elif colormap is not None: # palettized / categorical
profile = cog_profiles.get("deflate")
creation = {"predictor": 2}
resampling = "nearest" # do NOT invent classes
elif visual and dtype == "uint8" and bands >= 3: # RGB ortho, lossy OK
profile = cog_profiles.get("jpeg") # sets photometric=YCbCr
creation = {"quality": 85}
resampling = "average"
else: # RGB ortho, lossless
profile = cog_profiles.get("deflate")
creation = {"predictor": 2} # integer predictor
resampling = "average"
profile.update(blockxsize=512, blockysize=512)
cog_translate(src_path, dst_path, profile,
overview_resampling=resampling, **creation)
print(f"{dtype} bands={bands} -> {profile['compress']} resampling={resampling}")
The single most impactful line is predictor: 3 on the float branch — it is what stops a DEM COG from being twice its necessary size. The colormap check runs before the visual branch so a single-band palettized raster never gets JPEG’d (which would destroy its exact class values). The jpeg base profile sets YCbCr photometric interpretation, which is what makes JPEG on RGB compress well.
Edge-case matrix
The inputs a real orthomosaic pipeline produces, and how each should be tuned.
| Input variant | Symptom if mis-tuned | Expected handling |
|---|---|---|
uint8 3-band RGB ortho, web basemap |
JPEG artefacts unacceptable, or DEFLATE too large | visual=True → JPEG q85; else DEFLATE predictor 2 |
float32 single-band DEM |
2× oversized with predictor 2 | DEFLATE/ZSTD predictor 3, average resampling |
Palettized uint8 classification |
nearest skipped → invented classes at zoom-out |
DEFLATE predictor 2, nearest/mode resampling |
uint16 multispectral ortho |
JPEG unsupported for 16-bit → write error | DEFLATE/ZSTD predictor 2 (never JPEG) |
| RGBA ortho with alpha | JPEG drops the alpha band | DEFLATE with internal mask, keep alpha |
| Very wide mosaic (>20k px) | pyramid too shallow → slow zoom-out | extend factors until top level ≈ one 512 block |
The uint16 row is a common trap: JPEG in a COG is limited to 8-bit, so a 16-bit multispectral ortho must use a lossless codec — attempting compress="JPEG" on it raises a driver error rather than silently downcasting.
Verification snippet
Confirm the tuning actually landed: the codec and predictor you asked for are on the file, the pyramid is deep enough, and the output is meaningfully smaller than the source.
import os
import rasterio
def assert_tuning(src_path: str, cog_path: str, min_ratio: float = 1.3) -> None:
with rasterio.open(cog_path) as ds:
tags = ds.tags(ns="IMAGE_STRUCTURE")
print("codec:", tags.get("COMPRESSION"), "predictor:", tags.get("PREDICTOR"))
levels = ds.overviews(1)
assert len(levels) >= 4, f"pyramid too shallow: {levels}"
ratio = os.path.getsize(src_path) / os.path.getsize(cog_path)
assert ratio >= min_ratio, f"compression ratio {ratio:.2f} below target"
print(f"tuned: overviews={levels}, size ratio={ratio:.2f}x")
The IMAGE_STRUCTURE tags report the codec and predictor GDAL actually wrote, which catches the case where an option was silently ignored because the GDAL build did not support it. A compression ratio at or below 1.0 on a float DEM is the signature of a missing predictor 3.
When to escalate
This page tunes a raster that is already a valid COG. Escalate when the problem is elsewhere:
- The file fails validation outright (not tiled, no overviews, wrong ordering) — that is a structural fault, fixed in fixing COG validation errors in GDAL, not a tuning one.
- You need the full export walkthrough with profiles, BigTIFF, and NoData handling — return to exporting Cloud-Optimized GeoTIFFs with Rasterio.
- The write runs out of memory on a huge mosaic before tuning even matters — see decoding GDAL “cannot allocate memory” on export.