Handling Partial Overlap between AOI and Scene

Measure coverage from valid pixels, not footprints, and write onto a fixed AOI grid so partial scenes still stack:

import numpy as np
from rasterio.mask import mask

arr, _ = mask(src, [aoi_geom], crop=True, filled=False)
coverage = arr[0].count() / aoi_px                    # valid pixels / pixels in the AOI
if coverage < 0.2:
    skip(scene)                                        # too little to be worth it

Scenes rarely line up with study areas, and the defaults for handling the mismatch produce outputs that do not stack. This page belongs to automated image clipping and cropping in Satellite Processing Workflows & Index Pipelines.


Footprint Coverage Is Not Data Coverage

The tile covers the AOI; the swath does not A Sentinel-2 tile's rectangular footprint fully contains the AOI. The satellite's swath edge crosses the tile diagonally, so only part of the tile holds valid data and the rest is nodata. Measured by footprint, coverage is one hundred per cent; measured by valid pixels it is about forty per cent. Footprint 100%, valid data 40% valid nodata AOI Grey: tile outside the swath. Deciding from the footprint would process a scene that is mostly empty.

Catalog footprints for tiled products are frequently the tile bounds rather than the data boundary, and even where they are the data boundary they are simplified polygons. The only reliable measure of whether a scene is useful for an AOI is the count of valid pixels inside it, and that can be computed cheaply from an overview before the full-resolution read.


Environment & Setup

Package Version pin Used for
rasterio >=1.3.0 Masked reads and fixed-grid writes
numpy >=1.23 Coverage arithmetic and stitching
shapely >=2.0 AOI geometry and bounds
pip install "rasterio>=1.3.0" "numpy>=1.23" "shapely>=2.0"

Complete Working Example

import math

import numpy as np
import rasterio
from rasterio.features import geometry_mask
from rasterio.transform import from_origin
from rasterio.warp import reproject, Resampling


def aoi_grid(aoi_geom, res: float):
    """A fixed grid snapped to res that covers the AOI — shared by every output."""
    l, b, r, t = aoi_geom.bounds
    l, b = math.floor(l / res) * res, math.floor(b / res) * res
    r, t = math.ceil(r / res) * res, math.ceil(t / res) * res
    w, h = int(round((r - l) / res)), int(round((t - b) / res))
    return from_origin(l, t, res, res), h, w


def onto_aoi_grid(scene_path: str, aoi_geom, crs, res: float = 10.0):
    """Warp a scene onto the AOI grid; return array, coverage and grid."""
    transform, h, w = aoi_grid(aoi_geom, res)
    inside = ~geometry_mask([aoi_geom], out_shape=(h, w), transform=transform)
    dst = np.full((h, w), np.nan, dtype="float32")
    with rasterio.open(scene_path) as src:
        reproject(rasterio.band(src, 1), dst,
                  src_transform=src.transform, src_crs=src.crs,
                  dst_transform=transform, dst_crs=crs,
                  src_nodata=src.nodata, dst_nodata=np.nan,
                  resampling=Resampling.bilinear)
    dst[~inside] = np.nan
    coverage = float(np.isfinite(dst[inside]).mean())
    return dst, coverage, (transform, h, w)


def complete_aoi(scene_paths: list[str], aoi_geom, crs, *, min_cov: float = 0.2):
    """Fill the AOI from several scenes of one pass; first valid pixel wins."""
    out, grid, used = None, None, []
    for path in scene_paths:
        arr, cov, g = onto_aoi_grid(path, aoi_geom, crs)
        if cov < min_cov:
            continue
        out = arr if out is None else np.where(np.isfinite(out), out, arr)
        grid = g
        used.append((path, round(cov, 3)))
    return out, grid, used

Warping every scene onto one grid derived from the AOI — not from each scene — is what makes partial outputs usable. Two dates that each cover different halves of the AOI produce arrays of identical shape and transform, so they stack, difference and composite without a further alignment step. The same principle underlies aligning two rasters with reproject_match.


Crop to the Overlap, or Keep the AOI Grid

Two output strategies for partial scenes Cropping each output to its own overlap produces files of different sizes and extents that cannot be stacked without realignment. Writing every output onto the full AOI grid produces identically shaped files with nodata where a scene had no coverage, which stack directly. cropped to each overlap all on the AOI grid different shapes; realign before any stacking identical grids; nodata where uncovered Pick the right-hand strategy whenever outputs from different scenes or dates will be combined.

Cropping to the overlap minimises storage and is right for one-off outputs. For anything that will be combined — time series, composites, change detection — writing onto the fixed AOI grid is worth the nodata margins, because it moves alignment from every downstream step to one place. Compression makes the margins nearly free on disk.


Choosing Scenes to Complete an AOI

When no single scene covers the AOI, scenes from the same pass — adjacent tiles or path/row frames acquired minutes apart — fill it without introducing a date difference. Scenes from different dates also fill it, but create a seam where the atmosphere, illumination or the surface itself changed between acquisitions. Prefer same-pass completion, then nearest dates, and record which scene supplied each pixel if the output will be analysed rather than only viewed. The techniques for blending across such seams are covered in seamless mosaicking and edge blending.


Recording Which Scene Supplied Each Pixel

When an AOI is completed from several scenes, a small companion band recording the index of the source scene for each pixel is cheap and repeatedly useful. It explains seams, lets a later analysis exclude one problematic scene without reprocessing, and turns the question “which acquisition is this pixel from?” into a lookup. Store it as uint8 alongside the data, with the scene identifiers listed in the file’s tags in index order.


Verification

Coverage recorded, grids identical Each output should record its measured coverage in its tags, all outputs for one AOI should share an identical transform and shape, and the combined coverage of the scenes used should approach the full AOI when completion was attempted. Three checks coverage tagged on every output same grid transform and shape equal completion combined coverage near 1 A coverage value on the file saves every consumer from recomputing it.
arr, grid, used = complete_aoi(scenes, aoi_geom, "EPSG:32636")
transform, h, w = grid
inside = ~geometry_mask([aoi_geom], out_shape=(h, w), transform=transform)
total = float(np.isfinite(arr[inside]).mean())
print("scenes used:", used, f"combined coverage {total:.1%}")
assert arr.shape == (h, w)

Common Errors

Outputs from two dates will not stack

Each was cropped to its own overlap. Write both onto the AOI grid.

A “full coverage” scene is mostly empty in the clip

Coverage was judged from the tile footprint. Measure valid pixels.

A visible seam where two scenes meet

They were from different dates. Prefer same-pass scenes, or blend.

The AOI grid is offset by a fraction of a pixel between runs

Bounds were not snapped to the resolution. Snap them as aoi_grid does.


Frequently Asked Questions

Q: Why does the footprint say full coverage but the clip has holes? Scene footprints are often the tile bounds, not the valid-data boundary. Sentinel-2 tiles at the edge of a swath are partly nodata, so a footprint can cover the AOI while the pixels do not. Measure coverage from valid pixels.

Q: Should the output keep the AOI’s full extent? Keep it when outputs from several dates or scenes must stack pixel for pixel, because then every file shares one grid. Crop to the overlap when the output stands alone and storage matters.

Q: What minimum coverage is worth processing? It depends on the product, but a threshold of around 20 to 30 per cent of the AOI is common for composites, since low-coverage scenes add little and cost as much to process as full ones.

Q: Can coverage be estimated before downloading anything? Yes, from an overview level of each candidate scene: a read of a few kilobytes gives the valid-data pattern to within a few per cent, which is enough to rank and filter scenes before any full-resolution read.