Tutorial — Acquiring Free Satellite Imagery
Goal: After this tutorial, you can find and download satellite imagery for any location on Earth for free, both manually and programmatically.
All intelligence analysis starts with data. The good news: terabytes of satellite imagery are freely available. Sentinel-2 optical, Sentinel-1 SAR, Landsat (50+ year archive), SRTM elevation — all free, all global coverage. You will never need to pay for data to learn GEOINT.
Step 1: Copernicus Data Space (Browser)
URL: https://dataspace.copernicus.eu
This is your primary source. ESA’s Copernicus program provides Sentinel-1 (SAR), Sentinel-2 (optical), Sentinel-3 (ocean/land), and Sentinel-5P (atmosphere) — all free.
API Update 2024
The Copernicus Data Space Ecosystem (CDSE) replaced the legacy Copernicus Open Access Hub (SciHub) in 2023. The new API uses OAuth2 authentication and OData queries. If you encounter old SciHub code examples, they will not work with the new system.
Create an Account
- Go to https://dataspace.copernicus.eu
- Click “Register” — free, requires email verification
- This account works for both the browser interface and the API
- Important: Save your credentials securely — you’ll need them for API access
API Authentication (New System)
The CDSE uses OAuth2 for authentication. You’ll need to:
- Obtain an access token using your username/password
- Include the token in API requests
- Tokens expire and must be refreshed
# CDSE OAuth2 authentication example
import requests
def get_access_token(username, password):
"""Obtain OAuth2 access token for CDSE API."""
token_url = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
data = {
'grant_type': 'password',
'username': username,
'password': password,
'client_id': 'cdse-public',
}
response = requests.post(token_url, data=data)
response.raise_for_status()
return response.json()['access_token']
# Use token in subsequent requests
headers = {'Authorization': f'Bearer {access_token}'}Browser Interface
- Open the Copernicus Browser (browser.dataspace.copernicus.eu)
- Navigate to your area of interest (zoom/pan or search by place name)
- Left panel: select data source
- Sentinel-2 L2A — atmospherically corrected surface reflectance (use this)
- Sentinel-2 L1C — top-of-atmosphere (raw, needs atmospheric correction)
- Sentinel-1 GRD — SAR amplitude (easier to use)
- Sentinel-1 SLC — SAR complex (for interferometry)
- Set date range and maximum cloud cover (e.g., <20%)
- Click “Search” — results appear as footprints on the map
- Click a result to preview → “Download” to get the full product (ZIP file)
Product naming convention:
S2A_MSIL2A_20240315T100021_N0510_R122_T35VLF_20240315T134020.zip
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ Processing timestamp
│ │ │ │ │ └─ Tile ID (MGRS grid)
│ │ │ │ └─ Relative orbit
│ │ │ └─ Processing baseline
│ │ └─ Acquisition date/time
│ └─ Product level (L2A = surface reflectance)
└─ Satellite (A or B)
Tile structure (Sentinel-2): Each product covers a 100x100 km tile in the MGRS grid. Estonia is covered primarily by tiles T35VLF (Tallinn area), T35VLE, T35VMF. A full tile at all bands is ~800MB compressed.
What’s Inside a Sentinel-2 Product
After unzipping, the structure is:
S2A_MSIL2A_.../
GRANULE/
L2A_T35VLF.../
IMG_DATA/
R10m/ # 10m resolution bands
T35VLF_B02_10m.jp2 # Blue
T35VLF_B03_10m.jp2 # Green
T35VLF_B04_10m.jp2 # Red
T35VLF_B08_10m.jp2 # NIR
T35VLF_TCI_10m.jp2 # True Color Image (RGB composite)
R20m/ # 20m resolution bands
T35VLF_B05_20m.jp2 # Red Edge 1
T35VLF_B06_20m.jp2 # ...
T35VLF_B11_20m.jp2 # SWIR 1
T35VLF_B12_20m.jp2 # SWIR 2
T35VLF_SCL_20m.jp2 # Scene Classification (cloud mask)
R60m/ # 60m resolution bands
QI_DATA/ # Quality indicators
The SCL (Scene Classification Layer) is critical for analysis — it classifies each pixel as cloud, cloud shadow, water, vegetation, bare soil, etc. Use it to mask clouds before any analysis.
Step 2: USGS EarthExplorer
URL: https://earthexplorer.usgs.gov
Primary source for Landsat data (since 1972!) and other US datasets.
What’s Available
- Landsat-9 (2021-present): 30m multispectral, 15m pan, 100m thermal
- Landsat-8 (2013-present): same as above
- Landsat-7 (1999-present): has scan-line corrector failure since 2003 (striped data)
- Landsat-5 (1984-2013): Thematic Mapper, 30m
- Landsat 1-4 (1972-1993): MSS, 60-80m — historical archive
- ASTER: 15m visible, 30m SWIR, 90m thermal
- SRTM DEM: 30m global elevation
- Declassified satellite imagery: CORONA, GAMBIT, HEXAGON — cold war spy satellites, now unclassified
Download Process
- Create free USGS account at https://ers.cr.usgs.gov/register
- Define area of interest (map, coordinates, or place name)
- Select dataset (e.g., Landsat 8-9 OLI/TIRS C2 L2)
- Set date range, cloud cover
- Results → click to preview → “Add to Bulk Download” or direct download
- Use the Bulk Download Application for large orders
Declassified spy satellite imagery is a unique resource. CORONA (1960-1972) provides ~2m resolution imagery decades before commercial satellites existed. Invaluable for historical analysis of military installations, urban growth, environmental change.
Step 3: Copernicus Data Space API (Programmatic)
For automated workflows, search and download via API. The OData API provides full access.
Search for Sentinel-2 Products
import requests
from datetime import datetime, timedelta
def search_sentinel2(lat, lon, start_date, end_date, max_cloud=20, max_results=10):
"""
Search for Sentinel-2 L2A products covering a location.
Uses Copernicus Data Space OData API.
"""
# OData API endpoint
base_url = "https://catalogue.dataspace.copernicus.eu/odata/v1/Products"
# Build filter
# Geographic filter: intersects with point
geo_filter = (
f"OData.CSC.Intersects(area=geography'SRID=4326;POINT({lon} {lat})')"
)
# Collection and processing level
collection_filter = (
"Collection/Name eq 'SENTINEL-2' and "
"Attributes/OData.CSC.StringAttribute/any(att:att/Name eq "
"'productType' and att/OData.CSC.StringAttribute/Value eq 'S2MSI2A')"
)
# Cloud cover filter
cloud_filter = (
f"Attributes/OData.CSC.DoubleAttribute/any(att:att/Name eq "
f"'cloudCover' and att/OData.CSC.DoubleAttribute/Value lt {max_cloud})"
)
# Date filter
date_filter = (
f"ContentDate/Start gt {start_date}T00:00:00.000Z and "
f"ContentDate/Start lt {end_date}T23:59:59.000Z"
)
filter_str = f"{geo_filter} and {collection_filter} and {cloud_filter} and {date_filter}"
params = {
"$filter": filter_str,
"$orderby": "ContentDate/Start desc",
"$top": max_results,
}
response = requests.get(base_url, params=params)
response.raise_for_status()
data = response.json()
results = []
for product in data.get("value", []):
results.append({
"id": product["Id"],
"name": product["Name"],
"date": product["ContentDate"]["Start"],
"size_mb": product.get("ContentLength", 0) / 1e6,
})
return results
# Search for Sentinel-2 over Tallinn in the last 30 days
results = search_sentinel2(
lat=59.44, lon=24.75,
start_date="2024-03-01",
end_date="2024-03-31",
max_cloud=30,
)
for r in results:
print(f"{r['date'][:10]} {r['name'][:60]}... {r['size_mb']:.0f} MB")Download a Product
import requests
import os
def get_access_token(username, password):
"""Get access token from Copernicus Data Space."""
token_url = "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token"
data = {
"client_id": "cdse-public",
"username": username,
"password": password,
"grant_type": "password",
}
response = requests.post(token_url, data=data)
response.raise_for_status()
return response.json()["access_token"]
def download_product(product_id, token, output_dir="."):
"""Download a product by its ID."""
url = (
f"https://zipper.dataspace.copernicus.eu/odata/v1/"
f"Products({product_id})/$value"
)
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers, stream=True)
response.raise_for_status()
# Get filename from content-disposition header
cd = response.headers.get("content-disposition", "")
filename = cd.split("filename=")[-1].strip('"') if "filename=" in cd else f"{product_id}.zip"
filepath = os.path.join(output_dir, filename)
total = int(response.headers.get("content-length", 0))
downloaded = 0
with open(filepath, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
downloaded += len(chunk)
if total > 0:
pct = downloaded / total * 100
print(f"\r Downloading: {pct:.1f}% ({downloaded/1e6:.0f}/{total/1e6:.0f} MB)",
end="")
print(f"\n Saved: {filepath}")
return filepath
# Usage:
# token = get_access_token("your_email", "your_password")
# download_product(results[0]["id"], token, output_dir="./data")Alternative: sentinelhub Library
The sentinelhub Python library provides a higher-level API for Copernicus Data Space:
# pip install sentinelhub
from sentinelhub import (
SHConfig, BBox, CRS, DataCollection,
SentinelHubRequest, MimeType, bbox_to_dimensions,
)
import numpy as np
# Configure credentials (Copernicus Data Space)
config = SHConfig()
config.sh_client_id = "your_client_id" # from dataspace.copernicus.eu
config.sh_client_secret = "your_client_secret"
config.sh_base_url = "https://sh.dataspace.copernicus.eu"
config.sh_token_url = (
"https://identity.dataspace.copernicus.eu/auth/realms/"
"CDSE/protocol/openid-connect/token"
)
# Define area: Tallinn harbor area
tallinn_bbox = BBox([24.70, 59.42, 24.82, 59.47], crs=CRS.WGS84)
resolution = 10 # meters
size = bbox_to_dimensions(tallinn_bbox, resolution=resolution)
# Request true color image
evalscript = """
//VERSION=3
function setup() {
return {
input: ["B04", "B03", "B02"],
output: { bands: 3 }
};
}
function evaluatePixel(sample) {
return [3.5 * sample.B04, 3.5 * sample.B03, 3.5 * sample.B02];
}
"""
request = SentinelHubRequest(
evalscript=evalscript,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=("2024-06-01", "2024-06-30"),
maxcc=0.2,
)
],
responses=[SentinelHubRequest.output_response("default", MimeType.TIFF)],
bbox=tallinn_bbox,
size=size,
config=config,
)
images = request.get_data()
image = images[0] # numpy array, shape (H, W, 3)
print(f"Image shape: {image.shape}")Step 4: Google Earth Engine
URL: https://code.earthengine.google.com (JavaScript) or Python API
Google Earth Engine (GEE) is a cloud platform for processing planetary-scale geospatial data. Instead of downloading terabytes, you write analysis scripts that run on Google’s servers.
What’s Available (Free for Research/Education)
- Sentinel-2 (2015-present)
- Sentinel-1 (2014-present)
- Landsat entire archive (1972-present)
- MODIS, VIIRS
- SRTM, Copernicus DEM
- Night lights (DMSP, VIIRS)
- Many more: 900+ public datasets
Python API Setup
# pip install earthengine-api
import ee
# Authenticate (first time: opens browser for Google account)
ee.Authenticate()
ee.Initialize(project="your-project-id") # from console.cloud.google.com
# Get median Sentinel-2 image for Tallinn, summer 2024
tallinn = ee.Geometry.Point(24.75, 59.44)
s2 = (
ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
.filterBounds(tallinn)
.filterDate("2024-06-01", "2024-08-31")
.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 20))
.median() # pixel-wise median reduces clouds
)
# Compute NDVI
ndvi = s2.normalizedDifference(["B8", "B4"]).rename("NDVI")
# Export to Drive (downloads to your Google Drive)
task = ee.batch.Export.image.toDrive(
image=ndvi,
description="tallinn_ndvi_summer2024",
scale=10,
region=tallinn.buffer(10000).bounds(), # 10km radius
maxPixels=1e9,
)
task.start()
print(f"Export started. Check status at: https://code.earthengine.google.com/tasks")GEE JavaScript (Quick Analysis in Browser)
// Paste into code.earthengine.google.com
var tallinn = ee.Geometry.Point(24.75, 59.44);
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(tallinn)
.filterDate('2024-06-01', '2024-08-31')
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
.median();
// True color
Map.centerObject(tallinn, 12);
Map.addLayer(s2, {bands: ['B4', 'B3', 'B2'], min: 0, max: 3000}, 'True Color');
// NDVI
var ndvi = s2.normalizedDifference(['B8', 'B4']);
Map.addLayer(ndvi, {min: 0, max: 0.8, palette: ['red', 'yellow', 'green']}, 'NDVI');Step 5: Other Free Sources
NASA Worldview
URL: https://worldview.earthdata.nasa.gov
- Quick browse of global imagery (MODIS, VIIRS, Landsat, Sentinel)
- Good for fire detection, smoke plumes, large-scale events
- Animation feature: watch daily changes over time
- Direct download of visible tiles
SRTM DEM (Elevation Data)
URL: https://dwtkns.com/srtm30m/ or via USGS EarthExplorer
- 30m resolution global elevation model (56S to 60N latitude)
- Essential for terrain analysis, viewshed, slope
- Alternative: Copernicus DEM (also 30m, better quality, global)
OpenAerialMap
URL: https://openaerialmap.org
- Crowdsourced aerial and drone imagery
- Varies in coverage and quality
- Useful for high-res reference imagery in specific areas
Night Lights
- VIIRS DNB (NASA): 750m, monthly composites
- Applications: urbanization, power outage detection, military activity
Step 6: Data Formats
| Format | Extension | Contains | Used By |
|---|---|---|---|
| GeoTIFF | .tif | Raster + georeference metadata + CRS | Everything — the standard |
| JPEG2000 | .jp2 | Compressed raster + georeference | Sentinel-2 raw products |
| NetCDF | .nc | Multi-dimensional arrays (time series, climate) | MODIS, climate data |
| Shapefile | .shp (+.dbf,.shx,.prj) | Vector geometry + attributes | Boundaries, AOIs |
| GeoJSON | .geojson | Vector (JSON format) | Web mapping, APIs |
| GeoPackage | .gpkg | Vector or raster (SQLite-based) | Modern replacement for shapefile |
| SAFE | .SAFE (directory) | ESA product structure | Sentinel-1/2 products |
GeoTIFF is king. When in doubt, convert to GeoTIFF. All tools (rasterio, GDAL, QGIS) handle it natively. It stores georeference, CRS, and band data in a single file.
# Convert JP2 to GeoTIFF with GDAL
# gdal_translate input.jp2 output.tif -of GTiff -co COMPRESS=DEFLATE
# Or in Python with rasterio:
import rasterio
from rasterio.enums import Compression
with rasterio.open("sentinel2_B04.jp2") as src:
profile = src.profile.copy()
profile.update(driver="GTiff", compress="deflate")
with rasterio.open("sentinel2_B04.tif", "w", **profile) as dst:
dst.write(src.read())Try This Next
Exercise 1: Download and View Your City
- Go to Copernicus Browser, find a cloud-free Sentinel-2 L2A image of your city
- Download the product, unzip it
- Load B04 (Red), B03 (Green), B02 (Blue) with rasterio
- Display as true color composite — can you identify major landmarks?
- Compute NDVI using B08 and B04 — find the greenest park
Exercise 2: SAR of a Harbor
- On Copernicus Browser, find a Sentinel-1 GRD-IW product covering a major harbor (Tallinn, Helsinki, or any port)
- Download the VV polarization band
- Load and display in dB scale:
10 * np.log10(amplitude) - Can you identify ships? What do they look like in SAR? (See SAR Fundamentals and Analysis)
Exercise 3: Historical Comparison
- On USGS EarthExplorer, find Landsat-5 TM imagery of Tallinn from 1990
- Find Landsat-9 of the same area from 2024
- Compare: what changed in 34 years? New construction, urban expansion, coastline?
- This is your first change detection exercise
Self-Test Questions
- What is the difference between Sentinel-2 L1C and L2A products?
- How do you filter Sentinel-2 results to get only cloud-free imagery?
- What is the MGRS tile ID for Tallinn, Estonia?
- Name three advantages of using Google Earth Engine over downloading data locally.
- What data format should you use for most raster analysis workflows?
See also: Satellite Fundamentals | Sensor Types and Imagery Next: Tutorial - Working with Raster Data