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.

Choosing codec, predictor, and resampling by raster type A decision diagram branching from a single raster-type question into three outcomes. The top box asks what the raster type is. A branch to RGB 8-bit orthomosaic leads to a box recommending JPEG quality 85 for visual or DEFLATE predictor 2 for lossless, with average resampling. A branch to float32 DEM leads to a box recommending ZSTD or DEFLATE with predictor 3 and average resampling. A branch to palettized or categorical leads to a box recommending DEFLATE predictor 2 with nearest or mode resampling. Raster type? dtype + band count RGB 8-bit ortho JPEG q85 (visual) or DEFLATE predictor 2 average resampling float32 DEM ZSTD / DEFLATE predictor 3 average resampling palettized / class DEFLATE predictor 2 nearest / mode preserve exact values

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:

Exporting Cloud-Optimized GeoTIFFs with Rasterio