Remote sensing case study

Water Body Detection

Mapping surface water pixel by pixel from Sentinel-2 and Landsat-8 imagery, and testing whether a CNN that sees spatial context beats classical models that look at one pixel at a time.

Project
Undergraduate project, rebuilt and extended in 2026
Data
Sentinel-2 (10 m), Landsat-8 (30 m), JRC Global Surface Water
Stack
PyTorch, scikit-learn, Google Earth Engine, Rasterio

The problem

Knowing where surface water is, and how it changes, matters for water supply planning, flood response and drought monitoring. Satellites photograph the whole planet every few days, so the question is how to turn those images into reliable water maps.

The classic approach is a spectral index. Water reflects green light but absorbs near-infrared, so the Normalized Difference Water Index, NDWI = (Green − NIR) / (Green + NIR), is high over water. A single threshold on NDWI works on clear lakes but struggles with building shadows, dark rooftops, wet soil and muddy water.

I wanted to find out how much learned models improve on that threshold, and whether a convolutional network, which sees the shape and surroundings of each pixel, does better than classical models that only see one pixel's numbers.

The data

I built the dataset with Google Earth Engine. For each study area and month, the pipeline takes every satellite image, removes clouds and cloud shadows using each sensor's quality bands, and combines the clear pixels into one median composite. Both sensors are mapped to the same six bands: blue, green, red, near-infrared and two shortwave-infrared bands.

Labels come from the JRC Global Surface Water monthly history, using the same month as the imagery so the label and the image describe the same moment. Pixels that were cloudy in the imagery are marked "ignore" rather than guessed.

I picked eight areas with different kinds of water, in their dry season to limit clouds:

AreaWater typeMonth
Hyderabad, IndiaCity lakes and reservoirsFeb 2021
Kolleru Lake, IndiaShallow lake surrounded by aquaculture pondsFeb 2021
Chilika Lake, IndiaCoastal lagoonFeb 2021
Varanasi, IndiaGanges river and floodplainMar 2021
Lake Okeechobee, USALarge lake and wetlandsMar 2021
IJsselmeer, NetherlandsLake, polders and canalsApr 2021
Hudson Valley, USARiver and reservoirsSep 2021
Lake Victoria, UgandaLake shoreline near KampalaJul 2021

Approach

  1. Download composites1024 × 1024 pixel scenes per area and sensor, pulled directly from Earth Engine.
  2. Add water indicesNDWI, MNDWI (which uses shortwave infrared and handles built-up areas better), NDVI for vegetation and AWEI for shadows, giving 10 features per pixel.
  3. Tile and split by locationScenes are cut into 128 × 128 tiles, grouped into spatial blocks, and whole blocks go to train, validation or test.
  4. Train and compareTwo index baselines, five classical models and a U-Net CNN, for each sensor.
  5. Score on the same pixelsEvery model is evaluated on one fixed sample of up to 500,000 test pixels.
ModelWhat it sees
NDWI > 0One index, fixed threshold
MNDWI with Otsu thresholdOne index, threshold learned from the data
Logistic regression, KNN, SVM, random forest, gradient boostingAll 10 features of a single pixel
U-Net CNNA 128 × 128 neighbourhood: shapes, shorelines and context

Key decisions

Splitting by location, not at random

Neighbouring tiles look almost identical. With a random split, the test set would contain near-copies of training tiles and the scores would look better than they really are. Grouping tiles into spatial blocks and keeping each block in one split makes the test closer to mapping a place the model has not seen.

Choosing the right metric

Most pixels are land. A model that predicts "land" everywhere can still score high accuracy. So I compare models on IoU and F1 for the water class, with Cohen's kappa as a check, and treat accuracy as secondary.

Keeping the comparison fair

SVM and KNN slow down sharply with more data, so they train on smaller samples (20,000 and 50,000 pixels) than the tree models. Every model, including the CNN, is then scored on exactly the same test pixels, so differences come from the models and not from the test data.

Living with imperfect labels

The JRC labels are derived from 30 m Landsat imagery, so they are a strong reference but not hand-drawn ground truth. Narrow rivers and small ponds can be missed, which matters most for the 10 m Sentinel-2 results. Cloud-masked pixels are excluded instead of being counted as land.

Results

Every model was scored on the same 500,000 test pixels per sensor, from locations it never saw in training.

Bar charts of water IoU for each model on Sentinel-2 and Landsat-8
ModelSentinel-2 IoULandsat-8 IoU
NDWI > 00.9500.921
MNDWI (Otsu threshold)0.9790.957
Logistic regression0.9800.977
KNN0.9570.975
SVM (RBF)0.9810.978
Random forest0.9670.976
Gradient boosting0.9750.978
U-Net CNN0.9840.973

On Sentinel-2, the CNN wins

The U-Net reached 0.984 water IoU and 99.0% accuracy. Compared with the NDWI threshold, the share of mislabelled water area (1 − IoU) fell from 5.0% to 1.6%, about two-thirds fewer errors. At 10 m, shorelines and small ponds span enough pixels for spatial context to pay off.

On Landsat-8, it doesn't

Gradient boosting scored 0.978 against the U-Net's 0.973. At 30 m, a 128 × 128 tile covers almost 4 km and fine shoreline detail is lost. The JRC labels are themselves produced from Landsat by a per-pixel classifier, so per-pixel models trained on Landsat bands can match them very closely.

Thresholds learned from data beat fixed ones

Choosing the MNDWI threshold with Otsu's method (0.19 for Sentinel-2, 0.17 for Landsat-8, instead of the usual 0) closed most of the gap to the learned models.

True-color Sentinel-2 image of Hyderabad beside the U-Net water mask highlighting Hussain Sagar lake

U-Net water mask over Hyderabad, February 2021. The model outlines Hussain Sagar lake and several small ponds.

These are single runs with one random seed, so differences of about 0.005 IoU between the top models could change with a different split.

Next steps