Cloud Optimized GeoTIFF Python Tutorial
Table of Contents
- Understanding the Cloud Optimized GeoTIFF Format
- The Internal Architecture of a COG
- Setting Up Your Python Environment
- Installing Required Packages
- Reading COG Metadata with Rasterio
- Accessing the Image File Directory (IFD)
- Extracting Data Subsets (Windowing)
- Defining a Window Request
- Working with Rioxarray for Data Science Workflows
- Lazy Loading and Dask Integration
- Clipping with Geometries
- Advanced Techniques: Creating Cloud Optimized GeoTIFFs
- Using Rasterio to Write a COG
- Performance Tuning and Network Optimization
- Configuring GDAL Environment Variables
- Concurrent Processing with Dask and Xarray
- Setting up a Local Dask Cluster
- Visualizing Large COGs with Datashader
- Integrating Datashader with Xarray
- Real-World Use Cases and Implementations
- Spatiotemporal Asset Catalog (STAC) Integration
- Machine Learning Workflows
- Handling Authentication and Private Buckets
- Reading from a Private Amazon S3 Bucket
- Troubleshooting Common COG Issues
- Not Truly Cloud Optimized
- Nodata and Masking Inconsistencies
- Conclusion
- Frequently Asked Questions
Welcome to the ultimate cloud optimized geotiff python tutorial, where you will learn how to drastically improve your geospatial data workflows. As the volume and velocity of Earth observation data skyrocket, traditional methods of downloading massive satellite imagery files before processing them locally have become completely unscalable. The modern paradigm relies on storing data in the cloud and retrieving only the specific pixels required for analysis, bypassing the need for huge local storage. This is where the Cloud Optimized GeoTIFF (COG) comes into play. In this comprehensive guide, we will explore everything from the fundamental structure of COGs to advanced Python techniques for interacting with them over HTTP.
Understanding the Cloud Optimized GeoTIFF Format
Before diving into the code, it is critical to understand what makes a GeoTIFF "Cloud Optimized". At its core, a COG is simply a standard GeoTIFF file, meaning any GIS software capable of reading a regular GeoTIFF can seamlessly open a COG. However, a COG is internally structured in a very specific way to support HTTP GET Range requests. This structural enhancement allows HTTP clients to request byte ranges, enabling them to read only the metadata or just a small subset of the image without downloading the entire multi-gigabyte file.
The Internal Architecture of a COG
The magic of the Cloud Optimized GeoTIFF relies on two primary organizational principles: Tiling and Overviews. Regular GeoTIFFs often store pixel data in contiguous strips (scanlines). In contrast, a COG stores pixels in smaller, discrete tiles (usually 256x256 or 512x512 pixels). This means that if you only need to look at a small geographic area, you only have to fetch the tiles covering that specific area.
Furthermore, COGs include "overviews", which are downsampled, lower-resolution versions of the original image stored within the same file. When you zoom out on a web map or request a broader view at a lower resolution, the client fetches the appropriate overview rather than downloading the massive full-resolution data and shrinking it on the fly. Finally, all the metadata and the Image File Directory (IFD) are located at the very beginning of the file. This guarantees that the first HTTP GET Range request (usually fetching the first 16 KB) retrieves all the necessary structural information about where the tiles and overviews are located.
Setting Up Your Python Environment
To follow along with this cloud optimized geotiff python tutorial, you will need a robust Python environment loaded with the geospatial libraries that make reading COGs a breeze. The primary libraries we will utilize are rasterio, rioxarray, xarray, and optionally matplotlib for visualization.
Installing Required Packages
Because geospatial libraries often rely on underlying C-libraries like GDAL, installing them via standard pip can sometimes result in dependency conflicts. We highly recommend using conda or mamba to ensure smooth installations.
conda create -n cog_env python=3.10
conda activate cog_env
conda install -c conda-forge rasterio rioxarray xarray matplotlib dask
With the environment activated, you are ready to start streaming raster data directly from cloud storage buckets.

Reading COG Metadata with Rasterio
Let us begin our practical journey by reading the metadata of a COG hosted on Amazon Web Services (AWS). Rasterio makes this process incredibly simple. We will use a public Landsat 8 image for our examples.
Accessing the Image File Directory (IFD)
The first step in any COG workflow is to read the metadata to understand the coordinate reference system (CRS), the bounding box, the number of bands, and the data types. Because of the COG structure, rasterio will automatically perform an HTTP GET Range request to fetch just the header.
import rasterio
cog_url = "https://landsat-pds.s3.amazonaws.com/c1/L8/139/045/LC08_L1TP_139045_20170304_20170316_01_T1/LC08_L1TP_139045_20170304_20170316_01_T1_B4.TIF"
with rasterio.open(cog_url) as src:
print(f"Driver: {src.driver}")
print(f"Width: {src.width}, Height: {src.height}")
print(f"Bands: {src.count}")
print(f"CRS: {src.crs}")
print(f"Bounding Box: {src.bounds}")
print(f"Overviews: {src.overviews(1)}")
When you execute this block, you will notice that it returns the information almost instantaneously. If this were a non-optimized GeoTIFF, rasterio might have had to download the entire file to find the IFDs if they were placed at the end of the file. The src.overviews(1) call specifically lists the decimation factors of the internal overviews available for the first band.
Extracting Data Subsets (Windowing)
The most powerful feature of a COG is the ability to read a specific geographic window. If you have a massive satellite scene but you are only interested in a small city within that scene, you can specify a bounding box, and Python will only download the tiles intersecting that box.
Defining a Window Request
To extract a subset, we define a Window in terms of pixel offsets, or we can use spatial coordinates and convert them to pixel offsets using the dataset's affine transform.
from rasterio.windows import from_bounds
import matplotlib.pyplot as plt
# Define a bounding box in the same CRS as the image
minx, miny = 450000, 3100000
maxx, maxy = 480000, 3130000
with rasterio.open(cog_url) as src:
# Convert spatial bounds to pixel window
window = from_bounds(minx, miny, maxx, maxy, src.transform)
# Read the data for just this window
subset = src.read(1, window=window)
subset_transform = src.window_transform(window)
plt.imshow(subset, cmap='gray')
plt.title("Extracted Subset from COG")
plt.colorbar()
plt.show()
This operation only fetches the required tiles over the network. If you were monitoring your network traffic, you would see that only a few megabytes were transferred instead of the hundreds of megabytes that make up the full TIF file.
Working with Rioxarray for Data Science Workflows
While rasterio is an excellent foundational library, many modern data science workflows rely on xarray for multidimensional array manipulation. The rioxarray extension bridges the gap, allowing you to lazily load COGs directly into xarray DataArrays.
Lazy Loading and Dask Integration
When you open a COG with rioxarray, it does not immediately pull the pixel data into memory. Instead, it creates a lazy Dask array pointer to the cloud resource. Computations are only triggered when you explicitly request the data (e.g., for plotting or saving).
import rioxarray
# Open the COG lazily with chunking enabled
ds = rioxarray.open_rasterio(cog_url, chunks={'x': 512, 'y': 512})
print(ds)
The output will show a DataArray backed by Dask. This is exceptionally powerful when processing massive datasets, as Dask can parallelize the chunk retrieval and processing across multiple CPU cores or even distributed clusters.
Clipping with Geometries
Rioxarray makes it trivial to clip a COG using a GeoJSON polygon or a Shapely geometry. Under the hood, it calculates the bounding box of the geometry, performs a targeted GET Range request, and then masks the pixels outside the polygon.
from shapely.geometry import box
# Create a Shapely geometry
geom = box(450000, 3100000, 480000, 3130000)
# Clip the lazy DataArray
clipped_ds = ds.rio.clip([geom], crs=ds.rio.crs)
# Compute and plot
clipped_ds.plot(cmap='viridis')
plt.show()
Advanced Techniques: Creating Cloud Optimized GeoTIFFs
Consuming COGs is only half the battle; producing them is equally important. If you process data locally and want to share it via the cloud, you must format it correctly. We can generate COGs programmatically using GDAL bindings or rasterio.
Using Rasterio to Write a COG
To convert an existing GeoTIFF into a COG using python, we must build overviews, tile the dataset, and properly order the IFD. Modern versions of rasterio include a specialized driver called COG that handles this automatically.
import rasterio
from rasterio.enums import Resampling
input_file = "local_unoptimized_data.tif"
output_cog = "cloud_ready_output.tif"
# First, let's open the source dataset
with rasterio.open(input_file) as src:
profile = src.profile
# Update profile for COG format
profile.update(
driver="COG",
tiled=True,
blockxsize=256,
blockysize=256,
compress="deflate"
)
data = src.read()
# Write the new COG
with rasterio.open(output_cog, "w", **profile) as dst:
dst.write(data)
This script seamlessly translates a flat raster into a tiled, compressed COG. The COG driver automatically generates the optimal internal overviews based on the image size. However, if you are using an older GDAL version where the COG driver is unavailable, you must take a two-step approach: first, write a tiled GeoTIFF, build overviews, and then translate it using the COPY_SRC_OVERVIEWS=YES creation option.
Performance Tuning and Network Optimization
When working with remote sensing data over HTTP, network latency is often the primary bottleneck. Python's default configurations are not always optimized for the highly parallel GET Range requests required by COG processing.
Configuring GDAL Environment Variables
Rasterio and rioxarray utilize GDAL's Virtual File System (VSI) to handle HTTP connections. You can dramatically improve performance by tuning GDAL's environment variables within your Python script using the rasterio.Env() context manager.
import rasterio
# Configure GDAL optimizations
env = rasterio.Env(
GDAL_DISABLE_READDIR_ON_OPEN="EMPTY_DIR",
CPL_VSIL_CURL_ALLOWED_EXTENSIONS="tif",
VSI_CACHE=True,
VSI_CACHE_SIZE=536870912, # 512 MB Cache
GDAL_HTTP_MULTIPLEX="YES",
GDAL_HTTP_VERSION="2"
)
with env:
with rasterio.open(cog_url) as src:
# Operations here will be significantly faster
window_data = src.read(1, window=window)
Understanding the Optimization Flags
Let's break down these critical flags:
- GDAL_DISABLE_READDIR_ON_OPEN: When opening a remote file, GDAL sometimes tries to read the parent directory to look for auxiliary files (like .tfw or .prj). Setting this prevents unnecessary HTTP directory listings.
- VSI_CACHE: Enables caching of HTTP range requests in memory, preventing duplicate downloads of the same tiles.
- GDAL_HTTP_MULTIPLEX and VERSION 2: Enables HTTP/2 multiplexing, allowing multiple byte range requests to be sent concurrently over a single TCP connection, drastically reducing handshake overhead.
Concurrent Processing with Dask and Xarray
As datasets scale into the terabytes, single-threaded Python scripts hit their limits. By combining COGs, xarray, and Dask, you can process planetary-scale datasets by dispatching work across a distributed cluster.
Setting up a Local Dask Cluster
Even on a single machine, a local Dask cluster can utilize all your CPU cores to fetch COG tiles concurrently.
from dask.distributed import Client
import rioxarray
# Initialize a local cluster
client = Client(n_workers=4, threads_per_worker=2)
print(f"Dask Dashboard accessible at: {client.dashboard_link}")
# Open multiple COGs as a single lazy dataset (e.g., a time series)
urls = [
"https://example.com/cog_time1.tif",
"https://example.com/cog_time2.tif",
"https://example.com/cog_time3.tif"
]
# We use rioxarray to lazily open each one and concatenate them
import xarray as xr
datasets = [rioxarray.open_rasterio(url, chunks={'x': 1024, 'y': 1024}) for url in urls]
time_series = xr.concat(datasets, dim='time')
# Perform a lazy calculation (e.g., finding the maximum pixel value over time)
max_composite = time_series.max(dim='time')
# Compute the result in parallel
# The Dask workers will concurrently fetch tiles from the COGs
result = max_composite.compute()
This paradigm is precisely how platforms like Microsoft Planetary Computer and AWS Earth operate. They store massive archives of COGs and provide JupyterHub environments with Dask clusters, allowing researchers to compute NDVI or monitor deforestation over entire continents without downloading a single source file to their personal computers.
Visualizing Large COGs with Datashader
Attempting to render a massive geospatial raster in matplotlib can easily crash your Python kernel due to memory exhaustion. Datashader is a library specifically designed to render massive datasets by rapidly aggregating pixels into a fixed-size viewing window.
Integrating Datashader with Xarray
Because Datashader natively understands xarray and Dask, it forms the perfect visualization layer for a COG-based workflow.
import datashader as ds
import datashader.transfer_functions as tf
# Assuming 'large_lazy_cog' is a Dask-backed rioxarray dataset
canvas = ds.Canvas(plot_width=800, plot_height=600)
# Datashader will automatically pull only the required overviews or compute the aggregate
agg = canvas.raster(large_lazy_cog)
# Render with a color map
img = tf.shade(agg, cmap=['black', 'white'])
When you integrate this with an interactive plotting library like HoloViews or Bokeh, you can create interactive maps where zooming and panning dynamically trigger new Dask computations to fetch the exact COG tiles required for the new viewport.
Real-World Use Cases and Implementations
The adoption of the Cloud Optimized GeoTIFF has fundamentally transformed the Earth Observation industry. Organizations that previously spent millions on local storage arrays have migrated entirely to cloud-native workflows.
Spatiotemporal Asset Catalog (STAC) Integration
In modern architectures, COGs are rarely used in isolation. They are typically cataloged using the Spatiotemporal Asset Catalog (STAC) specification. STAC provides a standardized JSON format to describe the geographic extent, time, and properties of remote sensing assets.
By querying a STAC API via Python (using libraries like pystac-client), a user can search for all satellite images intersecting a polygon over a specific time range. The STAC API returns JSON objects containing the URLs to the underlying COGs. The Python script then iterates over these URLs, using rioxarray to stream just the required pixels, calculating indices, and returning the final analytical product. This completely decouples data discovery from data processing, creating a highly modular and scalable architecture.
Machine Learning Workflows
Machine learning models, particularly Convolutional Neural Networks (CNNs) used for land cover classification, require large amounts of training data fed in small spatial chips (e.g., 256x256 pixels). Instead of writing complex pre-processing pipelines to chip up massive GeoTIFFs locally, machine learning engineers use PyTorch Datasets that generate random bounding boxes, perform HTTP GET requests to a COG, and feed the resulting numpy arrays directly into the neural network for training. This ensures that the storage overhead remains near zero, and training data can scale infinitely in the cloud.
Handling Authentication and Private Buckets
While public COGs are great for learning, most production data resides in private cloud storage buckets. Authenticating your Python script to read from Amazon S3, Google Cloud Storage, or Azure Blob Storage requires configuring the correct GDAL environmental credentials.
Reading from a Private Amazon S3 Bucket
When using rasterio to access s3:// URIs, GDAL will automatically look for standard AWS environment variables (e.g., AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) or IAM roles.
import os
import rasterio
# Set AWS credentials (ensure you don't hardcode these in production!)
os.environ["AWS_ACCESS_KEY_ID"] = "YOUR_ACCESS_KEY"
os.environ["AWS_SECRET_ACCESS_KEY"] = "YOUR_SECRET_KEY"
os.environ["AWS_REGION"] = "us-west-2"
# Use the s3:// protocol rather than https://
private_cog = "s3://my-private-bucket/data/high_res_image.tif"
env = rasterio.Env(
AWS_NO_SIGN_REQUEST="NO", # Ensure GDAL signs the requests
GDAL_DISABLE_READDIR_ON_OPEN="EMPTY_DIR"
)
with env:
with rasterio.open(private_cog) as src:
data = src.read(1)
print("Successfully read from private S3 bucket!")
For cross-account access, rasterio also supports AWS session tokens and assume-role capabilities. The integration is seamless; once the environment is properly configured, reading from a highly secured corporate data lake acts exactly the same as reading from a local hard drive.
Troubleshooting Common COG Issues
As you build complex pipelines, you may encounter edge cases. Here are some common pitfalls and how to debug them.
Not Truly Cloud Optimized
Sometimes you might be given a URL that claims to be a COG, but your Python scripts are hanging or taking exceptionally long to read metadata. This usually means the IFD is located at the end of the file, forcing GDAL to download the entire multi-gigabyte file just to figure out the metadata.
You can validate a COG using a python utility called rio cogeo:
pip install rio-cogeo
rio cogeo validate https://example.com/suspicious_file.tif
If it returns false, you will need to re-process the file to restructure it correctly.
Nodata and Masking Inconsistencies
COGs handle nodata values through either an explicit NODATA metadata tag or via an internal alpha band (mask). When utilizing rioxarray to read subsets, ensure you are utilizing the masked=True parameter to ensure nodata values are properly converted to NaN in the resulting xarray dataset, preventing skewing in analytical calculations.
Conclusion
Throughout this comprehensive guide, we have established a strong foundational and advanced understanding of remote geospatial data access. By leveraging the principles taught in this cloud optimized geotiff python tutorial, developers and scientists can completely bypass archaic, monolithic data download strategies. Utilizing the synergistic combination of the COG format, rasterio for low-level HTTP Range requests, and rioxarray for multi-dimensional distributed compute capabilities natively unlocks planetary-scale spatial analysis for anyone with an internet connection. The cloud-native geospatial era is firmly here, and COGs serve as the indispensable bedrock powering the next generation of Earth observation platforms.
Frequently Asked Questions
What is a Cloud Optimized GeoTIFF (COG)?
A Cloud Optimized GeoTIFF (COG) is a standard GeoTIFF file formatted internally with overviews and tiled structure, allowing software to issue HTTP GET Range requests to read just the needed pixels without downloading the entire file.
How do you read a COG in Python?
Python libraries like Rasterio and GDAL make it easy to read COGs directly from cloud storage (like AWS S3) by simply passing the URL to the dataset and extracting specific spatial windows.
Why should I use Cloud Optimized GeoTIFFs instead of regular GeoTIFFs?
COGs drastically reduce bandwidth usage, storage costs, and processing time for web mapping applications, making them the industry standard for hosting and analyzing massive satellite imagery archives in the cloud.