Merging Tiles with rasterio.merge
To assemble adjacent rasters into one mosaic, pass their datasets to merge with an explicit nodata value and a deliberate ordering:
import rasterio
from rasterio.merge import merge
sources = [rasterio.open(p) for p in sorted(tile_paths)] # order decides overlap priority
mosaic, transform = merge(sources, nodata=0, method="first")
profile = sources[0].profile | {
"height": mosaic.shape[1], "width": mosaic.shape[2],
"transform": transform, "driver": "COG", "compress": "DEFLATE",
}
with rasterio.open("mosaic.tif", "w", **profile) as dst:
dst.write(mosaic)
for src in sources:
src.close()
Merging is the assembly step that precedes the blending questions in Seamless Mosaicking and Edge Blending.
Why This Arises in Remote Sensing Workflows
Satellite data arrives tiled, and study areas rarely respect tile boundaries. A catchment straddles two Sentinel-2 tiles; a district spans three Landsat paths; a national product is assembled from hundreds of scenes. Somewhere in every pipeline, adjacent rasters have to become one.
merge does the geometric part: it computes the union extent, allocates the output array, and copies each input into its place. What it does not do is make the result look continuous. Where two scenes overlap it applies a fixed rule — first wins, last wins, maximum wins — and the boundary between them is a hard edge. If the scenes differ radiometrically, that edge is visible, and no merge method hides it.
Separating those two concerns keeps the pipeline honest. Merge assembles; blending and histogram matching make the assembly look continuous, and they are separate steps with their own decisions.
Environment & Setup
| Package | Version | Why |
|---|---|---|
rasterio |
≥1.3.0 | merge, dataset handling, COG output |
numpy |
≥1.23 | Custom merge callables |
shapely |
≥2.0 | Optional: footprint checks before merging |
pip install "rasterio>=1.3.0" "numpy>=1.23"
Complete Working Example
This function validates that the inputs can legitimately be merged, orders them by a quality key, merges with an explicit nodata, and writes a tiled output with overviews.
import rasterio
from rasterio.enums import Resampling
from rasterio.merge import merge
def merge_tiles(
paths: list[str],
dst_path: str,
*,
nodata: float | int = 0,
method: str = "first",
quality_key=None,
) -> dict:
"""Merge compatible rasters into one mosaic, with overlap priority under your control."""
if quality_key is not None:
paths = sorted(paths, key=quality_key) # best first: 'first' then keeps the best
sources = [rasterio.open(p) for p in paths]
try:
crs = {src.crs for src in sources}
res = {tuple(round(v, 6) for v in src.res) for src in sources}
dtypes = {src.dtypes[0] for src in sources}
if len(crs) != 1:
raise ValueError(f"inputs span {len(crs)} CRSs — reproject onto one grid first")
if len(res) != 1:
raise ValueError(f"inputs have {len(res)} resolutions: {res}")
if len(dtypes) != 1:
raise ValueError(f"inputs have mixed dtypes: {dtypes}")
mosaic, transform = merge(sources, nodata=nodata, method=method)
profile = sources[0].profile | {
"driver": "COG",
"height": mosaic.shape[1],
"width": mosaic.shape[2],
"count": mosaic.shape[0],
"transform": transform,
"nodata": nodata,
"compress": "DEFLATE",
"blocksize": 512,
}
finally:
for src in sources:
src.close()
with rasterio.open(dst_path, "w", **profile) as dst:
dst.write(mosaic)
dst.build_overviews([2, 4, 8, 16], Resampling.average)
dst.update_tags(ns="rio_overview", resampling="average")
dst.update_tags(merged_from=str(len(paths)), merge_method=method)
return {"shape": mosaic.shape, "transform": transform, "inputs": len(paths)}
if __name__ == "__main__":
# Least cloudy first, so 'first' keeps the cleanest pixels in every overlap
info = merge_tiles(tile_paths, "mosaic.tif", nodata=0, method="first",
quality_key=lambda p: cloud_fraction_lookup[p])
print(info)
The three compatibility assertions are the point of the wrapper. merge does not check them, and each failure produces a plausible-looking output: mixed CRSs give a mosaic where one scene is in the wrong place, mixed resolutions give one that is stretched, and mixed dtypes give silent truncation.
Variant Patterns
1. A custom merge callable
When neither first nor last is right — for example, averaging the overlap rather than choosing — pass a callable.
import numpy as np
def mean_merge(merged_data, new_data, merged_mask, new_mask, **kwargs):
"""Average where both contribute; take whichever exists elsewhere."""
both = ~merged_mask & ~new_mask
only_new = merged_mask & ~new_mask
merged_data[both] = (merged_data[both] + new_data[both]) / 2
merged_data[only_new] = new_data[only_new]
merged_mask[only_new] = False
mosaic, transform = merge(sources, nodata=0, method=mean_merge)
Averaging removes the hard edge but replaces it with a band where the values are neither scene’s, which is visible whenever the two differ radiometrically — the case that motivates Histogram Matching Across Scenes.
2. Merging into a fixed output grid
By default the mosaic covers the union of the inputs. For a tiled product you usually want a predetermined extent so outputs abut exactly.
mosaic, transform = merge(
sources,
bounds=(300_000, 9_880_000, 400_000, 9_980_000), # the product tile, not the data extent
res=(10.0, 10.0),
nodata=0,
)
Fixing bounds and resolution makes the output reproducible and lets two runs produce files that align pixel for pixel, which is what a downstream mosaic of mosaics requires.
3. Merging more data than fits in memory
import rasterio
from rasterio.merge import merge
from rasterio.windows import Window
with rasterio.open(dst_path, "w", **profile) as dst:
for _, window in dst.block_windows(1):
bounds = rasterio.windows.bounds(window, dst.transform)
hits = [p for p in paths if footprints[p].intersects(box(*bounds))]
if not hits:
continue
srcs = [rasterio.open(p) for p in hits]
block, _ = merge(srcs, bounds=bounds, res=dst.res, nodata=profile["nodata"])
for s in srcs:
s.close()
dst.write(block, window=window)
The footprint index is what keeps this from reopening every input for every block; building it once from the STAC items costs nothing, as in Querying STAC Catalogs Programmatically.
Checking the Mosaic
Three checks catch the mistakes that survive a successful merge.
Extent and alignment: the output transform’s origin should be a multiple of the resolution, and the extent should match what you expect from the input footprints. An origin at an odd offset means the inputs were not on a shared grid.
Nodata behaviour: sample a few pixels along a join. A pixel that is valid in one input and fill in the other must take the valid value; if it is fill, the nodata declaration did not reach merge.
Continuity across joins: take a transect crossing a seam and plot the values. A step indicates a radiometric difference that merging cannot fix, and points to the histogram-matching and feathering steps in Removing Seams in Multi-Scene Mosaics with Feathering.
Common Errors
MergeError: Datasets have different CRS
Reproject onto a single grid first. Merging across CRSs is not something to work around — the result would be geometrically wrong.
The mosaic is enormous and mostly empty
One input has an outlying footprint — often a scene from the wrong tile or a mislabelled CRS — so the union extent balloons. Check src.bounds for every input before merging.
Memory blows up on a large mosaic
merge allocates the whole output. Use the block-wise pattern above, or merge in groups and then merge the groups.
Frequently Asked Questions
Q: What decides which scene wins in an overlap?
The method argument. The default first keeps whichever dataset appears earliest in the list, so ordering the inputs is a real decision — usually least cloudy first, or most recent first.
Q: Why does my mosaic have black stripes along the joins?
The inputs declare no nodata, so fill pixels are treated as data and win in the overlap. Pass nodata explicitly to merge, and make sure the source files declare it too.
Q: Can I merge rasters in different CRSs?
No. merge assumes a common CRS and resolution, and mixing them produces a misaligned result rather than an error. Reproject onto one grid first, then merge.
Related
- Seamless Mosaicking and Edge Blending — the parent topic, including what causes visible seams.
- Histogram Matching Across Scenes — normalising radiometry before the merge.
- Removing Seams in Multi-Scene Mosaics with Feathering — blending the joins this step creates.
- Writing and Validating Cloud-Optimized GeoTIFFs — writing the mosaic so it is cheap to read.