Oxbow Lake Segmentation (U-Net, Sentinel-2)
Segments oxbow lakes and active river channels in 10 m Sentinel-2 imagery. Trained on the Global Oxbow Lake Dataset. Try it in the Oxbow Lake Detector Space.
| Architecture | U-Net, ResNet-34 encoder (ImageNet initialisation), segmentation-models-pytorch 0.5 |
| Input | 6 bands: B02, B03, B04, B08, B11, B12 (Sentinel-2 L2A surface reflectance), 10 m, any size (padded to a multiple of 32) |
| Output | 3 classes: 0 background, 1 oxbow lake, 2 active river |
| Training | Kaggle, 1 x Tesla T4, 60 epochs (27 min), best checkpoint at epoch 47 (validation oxbow IoU) |
Results (test split: 149 patches, 149 lakes, river systems not seen in training)
| Class | IoU | F1 | Precision | Recall |
|---|---|---|---|---|
| Oxbow lake | 0.362 | 0.532 | 0.525 | 0.539 |
| Active river | 0.438 | 0.609 | 0.647 | 0.575 |
| Background | 0.962 | 0.981 | 0.979 | 0.983 |
Baseline for comparison: calling every water pixel (MNDWI > 0) an oxbow gives an oxbow IoU of 0.067, so the model learns to separate oxbows from other water, not just to find water.
Splits are grouped by river system, so test lakes come from floodplains the model never saw. The labels are derived automatically (OpenStreetMap outlines + HydroRIVERS + water index), not hand-drawn, which caps the measurable score: in inspected test patches the label calls flooded fields or sand bars "river" or misses murky river water where the prediction looks more plausible. Treat the numbers as a lower-bound-ish estimate with that noise in mind.
Usage
import json, numpy as np, torch, segmentation_models_pytorch as smp
from huggingface_hub import snapshot_download
d = snapshot_download("irfanhossainsust/oxbow-lake-segmentation")
cfg = json.load(open(f"{d}/config.json"))
model = smp.Unet(cfg["encoder"], encoder_weights=None, in_channels=6, classes=3)
model.load_state_dict(torch.load(f"{d}/model.pt", map_location="cpu")); model.eval()
# arr: (6, H, W) int16, Sentinel-2 L2A reflectance x 10000 in band order B02 B03 B04 B08 B11 B12, nodata -9999
x = (arr.astype("float32") * cfg["input_scale"] - np.array(cfg["mean"])[:, None, None]) / np.array(cfg["std"])[:, None, None]
x[:, (arr == -9999).any(0)] = 0
with torch.no_grad():
classes = model(torch.from_numpy(x[None]).float()).argmax(1)[0].numpy() # H, W must be multiples of 32
oxbow_detector.py (in this repo) adds Sentinel-2 download, polygon extraction, dataset-compatible
measurements (area, along-channel length, width, elongation, oxbow shape score, distance to river), matching against
the dataset (lake_index.parquet, 7,325 lakes) and morphology-based similar-lake search.
Files
| File | |
|---|---|
model.pt |
PyTorch state dict (98 MB) |
config.json |
architecture, band order, normalisation mean/std, classes |
metrics.json, history.csv |
test metrics and per-epoch training log |
lake_index.parquet |
the dataset's lakes with measurements, for matching and similarity search |
oxbow_detector.py, train_kaggle.py |
inference helpers and the exact training script |
Limitations
- Sentinel-2 only (no Sentinel-1/Landsat model yet); clouds and haze degrade results.
- Trained on 701 patches, mostly large, high-confidence lakes from 18 river systems; small (< 0.01 km²), dry or vegetated oxbows are often missed, and Africa is barely represented in training (4 lakes).
- Curved backwaters, side channels and abandoned channel segments can be confused with oxbows; sections of the main river are occasionally labelled oxbow.
- One lake can come out as several detections when it is partly vegetated.
- The "similar lakes" search compares shape and size only; it says nothing about the detected lake's age, ecology or water regime.
Citation and licence
Model code and weights: MIT. Training data: Global Oxbow Lake Dataset (ODbL 1.0, derived from © OpenStreetMap contributors; contains modified Copernicus Sentinel data).