Satellite imagery is one of the most useful free datasets in climate work, and one of the most confusing for developers. Images have 13 bands instead of 3. Pixels are 10 metres wide. Clouds ruin everything.
This is the guide I'd give a web developer who wants to do their first useful thing with satellite data.
What a satellite image actually is
A multispectral image is a stack of grids. Each grid (a band) measures reflected light at a specific wavelength. Sentinel-2, the workhorse for environmental ML, has 13 bands at 10, 20, or 60 m resolution, including visible, near-infrared (NIR), red-edge, and shortwave infrared (SWIR).
Plants reflect a lot of NIR and absorb red. Water absorbs NIR. Bare soil sits in between. Those differences are what ML learns.
Indices: features you get for free
Before ML, try a spectral index. The most famous is NDVI:
NDVI = (NIR - Red) / (NIR + Red)
High NDVI means healthy vegetation, near zero means bare soil or built-up areas, and negative means water. Indices make strong features for models too.
Getting data
Options, from easiest:
- Google Earth Engine: huge catalogue, compute in the cloud, free for research and non-commercial use.
- Microsoft Planetary Computer / AWS Open Data: cloud-optimised GeoTIFFs you can read with Python.
- Copernicus Data Space Ecosystem: the official Sentinel source.
Use cloud masks (Sentinel-2 provides a scene classification layer) or build cloud-free composites by taking the median of many images over a season.
A first classifier
The classic first project is land-cover classification: label pixels as water, forest, cropland, or urban.
import numpy as np import rasterio from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report # Stack of bands for one cloud-free composite: shape (bands, height, width) with rasterio.open("composite.tif") as src: image = src.read().astype("float32") # Labels raster: 0 = unlabeled, 1 = water, 2 = forest, 3 = crop, 4 = urban with rasterio.open("labels.tif") as src: labels = src.read(1) bands, h, w = image.shape X = image.reshape(bands, -1).T y = labels.reshape(-1) # Spatial split: left half of the scene trains, right half tests (see below) cols = np.tile(np.arange(w), h) is_train = cols < w // 2 mask = y > 0 X, y, is_train = X[mask], y[mask], is_train[mask] # Add NDVI as a feature (assuming band order: B2, B3, B4, B8, ...) red, nir = X[:, 2], X[:, 3] ndvi = (nir - red) / (nir + red + 1e-6) X = np.column_stack([X, ndvi]) X_train, y_train = X[is_train], y[is_train] X_test, y_test = X[~is_train], y[~is_train] clf = RandomForestClassifier(n_estimators=200, n_jobs=-1) clf.fit(X_train, y_train) print(classification_report(y_test, clf.predict(X_test)))
The trap: spatial leakage
If you randomly split pixels into train and test, neighbouring pixels end up on both sides. They're nearly identical, so accuracy looks amazing, then collapses on a new region.
Split by area: train on some tiles or regions, test on others. This is the single most common mistake in beginner remote sensing ML, and it shows up in published papers too.
Where to go next
- Change detection: compare before and after (deforestation, floods). See Detecting Deforestation with Satellite Imagery.
- SAR data (Sentinel-1) sees through clouds, which is crucial for floods. See AI Flood Early Warning.
- Deep learning (U-Net style segmentation) for field boundaries, buildings, and roads.