Sentinel Hub Essentials
Complete guide to accessing and processing satellite imagery using Sentinel Hub APIs.
What is Sentinel Hub?
Sentinel Hub is a cloud-based platform that provides access to satellite imagery from various Earth observation missions, primarily Copernicus Sentinel satellites.
Key Features:
Common Use Cases:
Prerequisites
Requirements
Get API Credentials
Step 1: Create Account
Step 2: Get Credentials
CLIENT_ID and CLIENT_SECRETInstallation
Install Sentinel Hub Python Package
# Step 1: Create virtual environment (recommended)
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Step 2: Install sentinelhub package
pip install sentinelhub
# Step 3: Install additional dependencies
pip install numpy matplotlib rasterio
Verify Installation:
python -c "import sentinelhub; print(sentinelhub.__version__)"
Configuration
Setup Authentication
Create .env file:
# Sentinel Hub credentials
SH_CLIENT_ID=your_client_id_here
SH_CLIENT_SECRET=your_client_secret_here
SH_INSTANCE_ID=your_instance_id_here
Configure in Python:
# ========== Option 1: Using Config ==========
from sentinelhub import SHConfig
# Create configuration
config = SHConfig()
config.sh_client_id = 'your_client_id'
config.sh_client_secret = 'your_client_secret'
# Save configuration (persists for future use)
config.save()
# ========== Option 2: From Environment Variables ==========
import os
from dotenv import load_dotenv
load_dotenv()
config = SHConfig()
config.sh_client_id = os.getenv('SH_CLIENT_ID')
config.sh_client_secret = os.getenv('SH_CLIENT_SECRET')
# ========== Verify Configuration ==========
print(f"Client ID: {config.sh_client_id[:8]}...")
print(f"Base URL: {config.sh_base_url}")
Basic Concepts
Understanding Sentinel Satellites
Sentinel-2:
Sentinel-1:
Common Spectral Bands
# Sentinel-2 bands
BANDS = {
'B01': 'Coastal aerosol (443 nm)',
'B02': 'Blue (490 nm) - 10m',
'B03': 'Green (560 nm) - 10m',
'B04': 'Red (665 nm) - 10m',
'B05': 'Red Edge 1 (705 nm)',
'B06': 'Red Edge 2 (740 nm)',
'B07': 'Red Edge 3 (783 nm)',
'B08': 'NIR (842 nm) - 10m',
'B8A': 'Narrow NIR (865 nm)',
'B09': 'Water vapour (945 nm)',
'B10': 'SWIR Cirrus (1375 nm)',
'B11': 'SWIR 1 (1610 nm)',
'B12': 'SWIR 2 (2190 nm)'
}
Searching for Data
Find Available Images
from sentinelhub import SHConfig, DataCollection, SentinelHubCatalog
from datetime import datetime, timedelta
# ========== Setup ==========
config = SHConfig()
catalog = SentinelHubCatalog(config=config)
# ========== Define Search Parameters ==========
# Area of interest (bounding box)
bbox = [13.822, 45.85, 13.835, 45.86] # Venice, Italy [lon_min, lat_min, lon_max, lat_max]
# Time range
time_interval = (
datetime(2024, 6, 1),
datetime(2024, 8, 31)
)
# ========== Search for Sentinel-2 Images ==========
search_iterator = catalog.search(
DataCollection.SENTINEL2_L2A, # Sentinel-2 Level-2A (atmospherically corrected)
bbox=bbox,
time=time_interval,
filter="eo:cloud_cover < 20", # Max 20% cloud cover
fields={
"include": ["id", "properties.datetime", "properties.eo:cloud_cover"],
"exclude": []
}
)
# ========== Display Results ==========
results = list(search_iterator)
print(f"Found {len(results)} images")
for item in results[:5]: # Show first 5
print(f"ID: {item['id']}")
print(f"Date: {item['properties']['datetime']}")
print(f"Cloud Cover: {item['properties']['eo:cloud_cover']}%")
print("-" * 50)
Expected Output:
Found 12 images
ID: S2A_MSIL2A_20240815T100031_N0510_R122_T33TVG_20240815T171645
Date: 2024-08-15T10:15:42Z
Cloud Cover: 5.2%
--------------------------------------------------
Requesting Imagery
True Color Image
from sentinelhub import (
SHConfig, SentinelHubRequest, DataCollection,
BBox, CRS, MimeType, bbox_to_dimensions
)
# ========== Configuration ==========
config = SHConfig()
# ========== Define Area of Interest ==========
bbox = BBox(bbox=[13.822, 45.85, 13.835, 45.86], crs=CRS.WGS84)
resolution = 10 # 10 meters per pixel
size = bbox_to_dimensions(bbox, resolution=resolution)
print(f"Image size: {size} pixels")
# ========== Evalscript for True Color ==========
evalscript_true_color = """
//VERSION=3
function setup() {
return {
input: [{
bands: ["B04", "B03", "B02"] // Red, Green, Blue
}],
output: {
bands: 3,
sampleType: "AUTO"
}
};
}
function evaluatePixel(sample) {
// Apply gain for brightness
return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02];
}
"""
# ========== Create Request ==========
request = SentinelHubRequest(
evalscript=evalscript_true_color,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=('2024-08-01', '2024-08-31'),
maxcc=0.2 # Maximum 20% cloud coverage
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.PNG)
],
bbox=bbox,
size=size,
config=config
)
# ========== Get Image ==========
true_color_image = request.get_data()[0]
print(f"Image shape: {true_color_image.shape}")
print(f"Data type: {true_color_image.dtype}")
Visualize with Matplotlib
import matplotlib.pyplot as plt
# ========== Display Image ==========
plt.figure(figsize=(12, 8))
plt.imshow(true_color_image)
plt.title('Sentinel-2 True Color Image')
plt.axis('off')
plt.tight_layout()
plt.savefig('true_color.png', dpi=300, bbox_inches='tight')
plt.show()
print("Image saved as 'true_color.png'")
Vegetation Indices
NDVI (Normalized Difference Vegetation Index)
# ========== NDVI Evalscript ==========
evalscript_ndvi = """
//VERSION=3
function setup() {
return {
input: [{
bands: ["B04", "B08"] // Red and NIR
}],
output: {
bands: 1,
sampleType: "FLOAT32"
}
};
}
function evaluatePixel(sample) {
// NDVI = (NIR - Red) / (NIR + Red)
let ndvi = (sample.B08 - sample.B04) / (sample.B08 + sample.B04);
return [ndvi];
}
"""
# ========== Request NDVI ==========
request_ndvi = SentinelHubRequest(
evalscript=evalscript_ndvi,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=('2024-08-01', '2024-08-31'),
maxcc=0.2
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.TIFF)
],
bbox=bbox,
size=size,
config=config
)
# ========== Get NDVI Data ==========
ndvi_data = request_ndvi.get_data()[0]
# ========== Visualize NDVI ==========
plt.figure(figsize=(12, 8))
plt.imshow(ndvi_data, cmap='RdYlGn', vmin=-1, vmax=1)
plt.colorbar(label='NDVI Value')
plt.title('NDVI - Vegetation Health')
plt.axis('off')
plt.tight_layout()
plt.savefig('ndvi.png', dpi=300, bbox_inches='tight')
plt.show()
# ========== Interpret NDVI Values ==========
print("NDVI Interpretation:")
print(" -1.0 to 0.0: Water, snow, clouds")
print(" 0.0 to 0.2: Barren rock, sand, urban areas")
print(" 0.2 to 0.4: Shrubs and grassland")
print(" 0.4 to 0.6: Moderate vegetation")
print(" 0.6 to 1.0: Dense, healthy vegetation")
NDWI (Normalized Difference Water Index)
# ========== NDWI Evalscript ==========
evalscript_ndwi = """
//VERSION=3
function setup() {
return {
input: [{
bands: ["B03", "B08"] // Green and NIR
}],
output: {
bands: 1,
sampleType: "FLOAT32"
}
};
}
function evaluatePixel(sample) {
// NDWI = (Green - NIR) / (Green + NIR)
let ndwi = (sample.B03 - sample.B08) / (sample.B03 + sample.B08);
return [ndwi];
}
"""
# ========== Request NDWI ==========
request_ndwi = SentinelHubRequest(
evalscript=evalscript_ndwi,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=('2024-08-01', '2024-08-31'),
maxcc=0.2
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.TIFF)
],
bbox=bbox,
size=size,
config=config
)
# ========== Get and Visualize ==========
ndwi_data = request_ndwi.get_data()[0]
plt.figure(figsize=(12, 8))
plt.imshow(ndwi_data, cmap='Blues', vmin=-1, vmax=1)
plt.colorbar(label='NDWI Value')
plt.title('NDWI - Water Content')
plt.axis('off')
plt.tight_layout()
plt.savefig('ndwi.png', dpi=300, bbox_inches='tight')
plt.show()
Time Series Analysis
Multi-Temporal Comparison
from sentinelhub import SentinelHubRequest, DataCollection, MimeType
import numpy as np
# ========== Define Time Periods ==========
time_periods = [
('2024-01-01', '2024-01-31', 'January'),
('2024-04-01', '2024-04-30', 'April'),
('2024-07-01', '2024-07-31', 'July'),
('2024-10-01', '2024-10-31', 'October')
]
# ========== Collect Images for Each Period ==========
ndvi_series = []
for start, end, label in time_periods:
request = SentinelHubRequest(
evalscript=evalscript_ndvi,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=(start, end),
maxcc=0.3
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.TIFF)
],
bbox=bbox,
size=size,
config=config
)
ndvi = request.get_data()[0]
ndvi_series.append((ndvi, label))
print(f"{label}: Mean NDVI = {np.nanmean(ndvi):.3f}")
# ========== Visualize Time Series ==========
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
for idx, (ndvi, label) in enumerate(ndvi_series):
ax = axes[idx // 2, idx % 2]
im = ax.imshow(ndvi, cmap='RdYlGn', vmin=-1, vmax=1)
ax.set_title(f'NDVI - {label}')
ax.axis('off')
plt.colorbar(im, ax=axes.ravel().tolist(), label='NDVI')
plt.tight_layout()
plt.savefig('ndvi_time_series.png', dpi=300, bbox_inches='tight')
plt.show()
Advanced Processing
Cloud Masking
# ========== Evalscript with Cloud Mask ==========
evalscript_cloud_mask = """
//VERSION=3
function setup() {
return {
input: [{
bands: ["B04", "B03", "B02", "CLM"] // RGB + Cloud Mask
}],
output: {
bands: 3,
sampleType: "AUTO"
}
};
}
function evaluatePixel(sample) {
// If cloud detected (CLM = 1), return gray
if (sample.CLM == 1) {
return [0.5, 0.5, 0.5];
}
// Otherwise return true color
return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02];
}
"""
# ========== Request with Cloud Masking ==========
request_masked = SentinelHubRequest(
evalscript=evalscript_cloud_mask,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=('2024-08-01', '2024-08-31')
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.PNG)
],
bbox=bbox,
size=size,
config=config
)
masked_image = request_masked.get_data()[0]
False Color Composite
# ========== False Color (NIR, Red, Green) ==========
evalscript_false_color = """
//VERSION=3
function setup() {
return {
input: [{
bands: ["B08", "B04", "B03"] // NIR, Red, Green
}],
output: {
bands: 3,
sampleType: "AUTO"
}
};
}
function evaluatePixel(sample) {
// Vegetation appears red in false color
return [2.5 * sample.B08, 2.5 * sample.B04, 2.5 * sample.B03];
}
"""
# ========== Request False Color ==========
request_false = SentinelHubRequest(
evalscript=evalscript_false_color,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=('2024-08-01', '2024-08-31'),
maxcc=0.2
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.PNG)
],
bbox=bbox,
size=size,
config=config
)
false_color = request_false.get_data()[0]
# ========== Display ==========
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 8))
ax1.imshow(true_color_image)
ax1.set_title('True Color')
ax1.axis('off')
ax2.imshow(false_color)
ax2.set_title('False Color (Vegetation in Red)')
ax2.axis('off')
plt.tight_layout()
plt.savefig('color_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
Batch Processing
Process Multiple Locations
from sentinelhub import BBox, CRS
# ========== Define Multiple Areas ==========
locations = {
'Venice': BBox([13.822, 45.85, 13.835, 45.86], crs=CRS.WGS84),
'Rome': BBox([12.48, 41.88, 12.50, 41.90], crs=CRS.WGS84),
'Milan': BBox([9.18, 45.46, 9.20, 45.48], crs=CRS.WGS84)
}
# ========== Process Each Location ==========
results = {}
for name, bbox in locations.items():
print(f"Processing {name}...")
size = bbox_to_dimensions(bbox, resolution=10)
request = SentinelHubRequest(
evalscript=evalscript_ndvi,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=('2024-08-01', '2024-08-31'),
maxcc=0.2
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.TIFF)
],
bbox=bbox,
size=size,
config=config
)
ndvi = request.get_data()[0]
results[name] = {
'ndvi': ndvi,
'mean': np.nanmean(ndvi),
'std': np.nanstd(ndvi)
}
print(f" Mean NDVI: {results[name]['mean']:.3f}")
print(f" Std Dev: {results[name]['std']:.3f}")
# ========== Compare Results ==========
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
for idx, (name, data) in enumerate(results.items()):
axes[idx].imshow(data['ndvi'], cmap='RdYlGn', vmin=-1, vmax=1)
axes[idx].set_title(f"{name}\nMean NDVI: {data['mean']:.3f}")
axes[idx].axis('off')
plt.colorbar(plt.cm.ScalarMappable(cmap='RdYlGn', norm=plt.Normalize(-1, 1)),
ax=axes, label='NDVI')
plt.tight_layout()
plt.savefig('batch_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
Export to GeoTIFF
Save with Geospatial Metadata
import rasterio
from rasterio.transform import from_bounds
# ========== Get Image Data ==========
ndvi_data = request_ndvi.get_data()[0]
# ========== Define Spatial Reference ==========
bounds = bbox.geometry.bounds # (min_lon, min_lat, max_lon, max_lat)
height, width = ndvi_data.shape
transform = from_bounds(
bounds[0], bounds[1], bounds[2], bounds[3],
width, height
)
# ========== Write GeoTIFF ==========
with rasterio.open(
'ndvi_output.tif',
'w',
driver='GTiff',
height=height,
width=width,
count=1, # Number of bands
dtype=ndvi_data.dtype,
crs='EPSG:4326', # WGS84
transform=transform,
compress='lzw' # Compression
) as dst:
dst.write(ndvi_data, 1)
# Add metadata
dst.update_tags(
DESCRIPTION='NDVI from Sentinel-2',
DATE='2024-08-15',
SOURCE='Sentinel Hub'
)
print("GeoTIFF saved successfully")
print(f"File: ndvi_output.tif")
print(f"CRS: EPSG:4326")
print(f"Bounds: {bounds}")
Common Use Cases
Agriculture Monitoring
# ========== Monitor Crop Health ==========
def analyze_crop_health(bbox, time_interval, threshold=0.6):
"""
Analyze crop health using NDVI
Args:
bbox: Area of interest
time_interval: Date range
threshold: NDVI threshold for healthy vegetation
"""
request = SentinelHubRequest(
evalscript=evalscript_ndvi,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=time_interval,
maxcc=0.2
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.TIFF)
],
bbox=bbox,
size=bbox_to_dimensions(bbox, resolution=10),
config=config
)
ndvi = request.get_data()[0]
# Calculate statistics
healthy = np.sum(ndvi > threshold)
stressed = np.sum((ndvi > 0.2) & (ndvi <= threshold))
total_vegetation = healthy + stressed
print(f"Crop Health Analysis:")
print(f" Healthy vegetation: {healthy / total_vegetation * 100:.1f}%")
print(f" Stressed vegetation: {stressed / total_vegetation * 100:.1f}%")
return ndvi
# Usage
farm_bbox = BBox([13.822, 45.85, 13.835, 45.86], crs=CRS.WGS84)
crop_ndvi = analyze_crop_health(farm_bbox, ('2024-07-01', '2024-07-31'))
Water Body Detection
# ========== Detect Water Bodies ==========
def detect_water(bbox, time_interval):
"""Detect water bodies using NDWI"""
request = SentinelHubRequest(
evalscript=evalscript_ndwi,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=time_interval,
maxcc=0.2
)
],
responses=[
SentinelHubRequest.output_response('default', MimeType.TIFF)
],
bbox=bbox,
size=bbox_to_dimensions(bbox, resolution=10),
config=config
)
ndwi = request.get_data()[0]
# Water typically has NDWI > 0.3
water_mask = ndwi > 0.3
water_pixels = np.sum(water_mask)
total_pixels = ndwi.size
print(f"Water Detection:")
print(f" Water coverage: {water_pixels / total_pixels * 100:.2f}%")
print(f" Water pixels: {water_pixels:,}")
return water_mask
# Usage
water_mask = detect_water(bbox, ('2024-08-01', '2024-08-31'))
Best Practices
Optimize API Usage
1. Use Appropriate Resolution# High resolution (10m) - small areas only
size_10m = bbox_to_dimensions(bbox, resolution=10)
# Medium resolution (20m) - balanced
size_20m = bbox_to_dimensions(bbox, resolution=20)
# Low resolution (60m) - large areas
size_60m = bbox_to_dimensions(bbox, resolution=60)
2. Filter by Cloud Coverage
# Only get images with <10% clouds
input_data = SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=('2024-08-01', '2024-08-31'),
maxcc=0.1 # 10% maximum cloud coverage
)
3. Cache Results
import pickle
# Save processed data
with open('ndvi_cache.pkl', 'wb') as f:
pickle.dump(ndvi_data, f)
# Load cached data
with open('ndvi_cache.pkl', 'rb') as f:
ndvi_data = pickle.load(f)
4. Batch Requests
# Request multiple time periods in one call
from sentinelhub import DownloadRequest
requests = [
request1.download_list[0],
request2.download_list[0],
request3.download_list[0]
]
# Download all at once
data = SentinelHubDownloadClient(config=config).download(requests)
Troubleshooting
Common Issues
Authentication Error:# Verify credentials
config = SHConfig()
print(f"Client ID set: {bool(config.sh_client_id)}")
print(f"Client Secret set: {bool(config.sh_client_secret)}")
# Test authentication
from sentinelhub import SentinelHubRequest
try:
# Simple test request
test_request = SentinelHubRequest(...)
print("Authentication successful!")
except Exception as e:
print(f"Authentication failed: {e}")
No Data Found:
# Check if images exist for your parameters
results = list(catalog.search(
DataCollection.SENTINEL2_L2A,
bbox=bbox,
time=time_interval
))
if len(results) == 0:
print("No images found. Try:")
print(" - Expanding time range")
print(" - Increasing cloud coverage threshold")
print(" - Verifying bbox coordinates")
Rate Limiting:
import time
# Add delay between requests
for location in locations:
process_location(location)
time.sleep(1) # Wait 1 second between requests
Resources
Quick Reference Card
# ========== Setup ==========
from sentinelhub import SHConfig, SentinelHubRequest, DataCollection
config = SHConfig()
config.sh_client_id = 'your_id'
config.sh_client_secret = 'your_secret'
# ========== Define Area ==========
bbox = BBox([lon_min, lat_min, lon_max, lat_max], crs=CRS.WGS84)
size = bbox_to_dimensions(bbox, resolution=10)
# ========== Request Data ==========
request = SentinelHubRequest(
evalscript=your_script,
input_data=[...],
responses=[...],
bbox=bbox,
size=size,
config=config
)
data = request.get_data()[0]
# ========== Common Indices ==========
# NDVI = (NIR - Red) / (NIR + Red)
# NDWI = (Green - NIR) / (Green + NIR)
# EVI = 2.5 * (NIR - Red) / (NIR + 6*Red - 7.5*Blue + 1)
Next Steps
---
Last Updated: 2026-04-20 Sentinel Hub API: v3 Difficulty: Intermediate