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 What the compression predictor does to a smooth elevation profile A row of elevation samples shown twice. Above, the raw values along a gentle slope: large numbers that differ from each other only slightly, which a general-purpose compressor cannot exploit because each value is distinct. Below, the same row after the horizontal differencing predictor: the values become a run of small, repeated deltas, which compresses far better. A note explains that predictor 2 differences integers and predictor 3 differences floating-point values, and that using predictor 2 on float data produces a file that is larger than no predictor at all. raw elevation samples along one row 412.06 412.11 412.15 412.20 412.24 412.29 412.33 412.38 every value distinct — nothing for a byte-level compressor to repeat after horizontal differencing (PREDICTOR) 412.06 +0.05 +0.04 +0.05 +0.04 +0.05 +0.04 +0.05 a short alphabet of repeating deltas — typically 30–50% smaller after DEFLATE or ZSTD PREDICTOR=2 differences integers · PREDICTOR=3 differences floats Using 2 on a float32 DEM differences the raw bytes of the mantissa and makes the file larger than no predictor at all.

Figure 2 — The predictor is why a DEM compresses well at all. It is also the one option whose wrong value costs you size rather than correctness, which is what makes it easy to leave misconfigured for years.

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.

How many overview levels a raster actually needs A pyramid of decimation levels for a twenty-thousand by twenty-thousand pixel orthomosaic. Each level halves both dimensions: level two is ten thousand square, level four is five thousand, and so on down to level sixty-four which is roughly three hundred pixels square and fits in a single tile. A marker shows the stopping rule: build levels until the smallest overview fits within one block, because a further level adds a directory entry and saves nothing. Beside the pyramid, the cumulative size overhead is given as approximately a third of the base image. base · 20000 × 20000 /2 · 10000 /4 /8 · 2500 px /16 · 1250 px /32 · 625 px /64 · 312 px — fits one 512 px tile: stop here the stopping rule build levels until the smallest overview fits inside one block one more level costs a directory entry and saves no requests total overhead ≈ 33% of the base Too few levels and a zoomed-out client decodes full-resolution tiles it will throw away; too many and you pay for entries nobody requests.

Figure 3 — Overview depth has a terminating condition, and it is not a matter of taste: stop when the top of the pyramid fits in one tile. The one-third size overhead is the price of the whole feature.

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.

One measurement is worth taking once per project rather than trusting a table: time a realistic read pattern, not just the file size. A codec that produces a 10% smaller file but decodes 40% slower is a poor trade for a raster served to a web map, where every pan and zoom pays the decode cost, and a good trade for an archive that is written once and read rarely. The right compression setting is a property of how the raster will be used, and that is knowable at export time.

When to escalate

This page tunes a raster that is already a valid COG. Escalate when the problem is elsewhere:

Whatever combination you settle on, record it in the run manifest beside the raster it produced, so a later size or performance comparison is between known configurations rather than between guesses.

Exporting Cloud-Optimized GeoTIFFs with Rasterio