Clipping a Raster to a Bounding Box with Windows
To clip a GeoTIFF to a rectangular bounding box without reading the whole image, convert the box to a pixel window with rasterio.windows.from_bounds, then pass that window to src.read. Only the overlapping blocks are fetched:
import rasterio
from rasterio.windows import from_bounds
left, bottom, right, top = 500000, 4600000, 510000, 4610000 # box in raster CRS
with rasterio.open("scene.tif") as src:
window = from_bounds(left, bottom, right, top, src.transform)
clip = src.read(1, window=window)
transform = src.window_transform(window)
This is the rectangular, memory-lean path inside Automated Image Clipping and Cropping. It is deliberately distinct from How to Clip Rasters to Irregular Polygon Boundaries: a polygon clip masks pixels outside an arbitrary shape, whereas a bounding-box window is an axis-aligned rectangle that GDAL can satisfy by reading a contiguous block. When you only need a rectangular extent, the window approach is far cheaper.
Why This Arises in Remote Sensing Workflows
Satellite scenes are large — a single Sentinel-2 or Landsat tile is hundreds of megabytes — but analysis is usually confined to a study area a fraction of that size. Reading the full array to then slice it wastes memory and, for remote assets, transfers pixels you immediately discard. A windowed read flips that: you translate the study-area bounds into a pixel offset and size, and GDAL fetches only the blocks that intersect it.
The mechanism rests on the affine transform. Every raster carries a geotransform mapping pixel row/column to world coordinates. windows.from_bounds inverts that mapping: given four world coordinates and the transform, it returns the fractional pixel window covering them. Round it to whole pixels, clamp it to the raster extent, and you have a Window object that read and write both understand.
The saving is largest for internally tiled rasters, which includes every well-formed Cloud-Optimized GeoTIFF. Such files store pixels in independent blocks — typically 256×256 or 512×512 — and GDAL can seek directly to the blocks a window overlaps. A 200 MB scene from which you want a 2 km study box might touch only a handful of blocks, so both memory use and, for remote assets, network transfer collapse to a few megabytes. On a striped (non-tiled) raster the same call still works, but GDAL must read whole rows, so the win is smaller; that difference is one more reason to convert archives to tiled COGs before running clip-heavy pipelines.
This topic lives in Automated Image Clipping and Cropping under Satellite Processing Workflows & Index Pipelines. It shares its core mechanic — windowed I/O — with Optimizing Rasterio Window Reads for Memory Efficiency, which goes deeper on block alignment and chunked iteration; here the focus is turning a geographic box into a correct, georeferenced clip.
From World Bounds to a Pixel Window
The diagram shows how four bounding-box coordinates become a pixel window, and why only the intersecting blocks of a tiled raster are read.
The rounded window can extend past the raster if the box overhangs the scene edge. Intersecting it with Window(0, 0, src.width, src.height) clamps it, so the read never asks for out-of-bounds pixels.
Environment & Setup
| Package | Minimum version | Why required |
|---|---|---|
rasterio |
1.3.0 | windows.from_bounds, window_transform, windowed read/write |
numpy |
1.24 | Array container for the clipped block |
pip install "rasterio>=1.3.0" "numpy>=1.24"
Complete Working Example
This function clips any GeoTIFF (local or a remote COG) to a bounding box, reprojecting the box into the raster CRS if needed, clamping the window to the raster extent, and writing a correctly georeferenced clip.
import rasterio
from rasterio.windows import from_bounds, Window
from rasterio.warp import transform_bounds
def clip_to_bbox(
src_path: str,
bbox: tuple[float, float, float, float],
out_path: str,
bbox_crs: str | None = None,
) -> None:
"""
Clip a raster to a bounding box using a windowed read.
bbox : (left, bottom, right, top)
bbox_crs : CRS of bbox, e.g. "EPSG:4326". If None, bbox is assumed
to already be in the raster CRS.
"""
with rasterio.open(src_path) as src:
left, bottom, right, top = bbox
# Reproject the box into the raster CRS when they differ
if bbox_crs is not None and src.crs.to_string() != bbox_crs:
left, bottom, right, top = transform_bounds(
bbox_crs, src.crs, left, bottom, right, top
)
# World bounds -> fractional pixel window, then snap to whole pixels
window = from_bounds(left, bottom, right, top, src.transform)
window = window.round_offsets().round_lengths()
# Clamp to the raster so we never read outside the array
full = Window(0, 0, src.width, src.height)
window = window.intersection(full)
if window.width <= 0 or window.height <= 0:
raise ValueError("Bounding box does not overlap the raster extent.")
data = src.read(window=window) # only these blocks are read
out_transform = src.window_transform(window) # shift origin to clip corner
profile = src.profile.copy()
profile.update(
height=int(window.height),
width=int(window.width),
transform=out_transform,
)
with rasterio.open(out_path, "w", **profile) as dst:
dst.write(data)
if __name__ == "__main__":
# A WGS84 study box clipped from a UTM-projected scene
clip_to_bbox(
src_path="S2_scene_utm.tif",
bbox=(11.20, 46.05, 11.35, 46.15), # lon/lat
out_path="study_area_clip.tif",
bbox_crs="EPSG:4326",
)
The two lines that make the clip correct rather than merely cropped are window.intersection(full), which guards against a box that hangs off the scene, and src.window_transform(window), which moves the affine origin so the output carries real-world coordinates for its own top-left corner. Copying src.profile and overriding only height, width, and transform preserves the CRS, dtype, band count, nodata value, and compression of the source, so the clip is a faithful subset rather than a re-encoded approximation.
round_offsets().round_lengths() deserves a note: from_bounds returns a window with fractional offsets because a study-area corner rarely lands exactly on a pixel boundary. Rounding the offsets and lengths to whole pixels snaps the window to the integer grid the array is stored on, which is required before reading — a fractional window has no meaning to read. The trade-off is a sub-pixel shift of at most half a cell, which is immaterial for a rectangular extract and far cheaper than resampling the raster to align it exactly to the box.
Variant Patterns
1. Clipping every band of a multi-band scene
Omitting the band index in read returns all bands as a 3-D array; the same window applies to every band, and the profile already knows the band count.
import rasterio
from rasterio.windows import from_bounds
with rasterio.open("multiband.tif") as src:
win = from_bounds(*bbox, src.transform).round_offsets().round_lengths()
stack = src.read(window=win) # shape (bands, rows, cols)
print(stack.shape, src.count)
2. Clipping a remote COG over HTTPS
Because a windowed read only fetches overlapping blocks, the same call against a Cloud-Optimized GeoTIFF transfers just the clip over the network. See How to Read COG Headers Without Downloading Full Files for the header-first validation that pairs with this.
import rasterio
from rasterio.env import Env
from rasterio.windows import from_bounds
url = "https://example-bucket.s3.amazonaws.com/scene_cog.tif"
with Env(GDAL_DISABLE_READDIR_ON_OPEN="EMPTY_DIR"):
with rasterio.open(url) as src:
win = from_bounds(*bbox, src.transform).round_offsets().round_lengths()
subset = src.read(1, window=win) # only the clip travels over HTTP
3. Snapping the window to the block grid
Aligning the window to the internal tile grid means GDAL reads whole blocks with no partial-block overhead. Round offsets down and lengths up to the block size for the tightest read.
from rasterio.windows import Window
def snap_to_blocks(window: Window, block: int = 256) -> Window:
col = (int(window.col_off) // block) * block
row = (int(window.row_off) // block) * block
w = (int(window.width + window.col_off - col + block - 1) // block) * block
h = (int(window.height + window.row_off - row + block - 1) // block) * block
return Window(col, row, w, h)
Common Errors
The clip is offset or lands in the wrong place
The bounding box and raster are in different coordinate systems. Reproject the box with rasterio.warp.transform_bounds into the dataset CRS before calling from_bounds, which assumes the bounds are already in the raster’s coordinate system.
rasterio.errors.WindowError or an empty array
The window fell entirely outside the raster, or its rounded width/height collapsed to zero. Intersect the window with the full-raster window and check window.width > 0 and window.height > 0 before reading.
The written clip has the original raster’s coordinates
You reused src.transform in the output profile. Replace it with src.window_transform(window) so the affine origin points at the clip’s corner, not the parent scene’s corner.
Related
- Automated Image Clipping and Cropping — the parent section on extracting study areas from full scenes.
- How to Clip Rasters to Irregular Polygon Boundaries — when the area of interest is a shape rather than a rectangle, use masking instead of a window.
- Optimizing Rasterio Window Reads for Memory Efficiency — block alignment and chunked iteration that make windowed reads fast at scale.
- Handling Pixel Resolution and Scaling — the transform and resolution fundamentals behind every window calculation.