Copernicus GFM — flood extent from Sentinel-1 (EODC STAC)¶
Global Flood Monitoring (GFM) is the Copernicus Emergency Management Service's continuous, global,
near-real-time flood-mapping product. It is derived from all incoming Sentinel-1 SAR acquisitions and
published — publicly and anonymously — through the EODC STAC API (stac.eodc.eu). earthlens reaches it
through the shipped stac backend under the eodc endpoint, as the collection eodc/gfm.
This notebook runs real requests against EODC; a cell that fails (network down, upstream broken) will error so you can see why.
GFM exposes twelve single-band uint8 COG layers (nodata 255): the final ensemble products
(ensemble_flood_extent, ensemble_water_extent, ensemble_likelihood), a reference_water_mask, an
exclusion_mask, advisory_flags, and the per-algorithm dlr_ / tuw_ / list_ intermediates. Licence:
Copernicus (free, attribution — https://extwiki.eodc.eu/en/GFM).
The catalog (no network)¶
The GFM collection is addressed by the logical key eodc/gfm, which the catalog resolves to the upstream id
GFM on the eodc endpoint. No network access happens here — this is the bundled catalog.
from earthlens.stac import Catalog
# The logical catalog key for the GFM flood-monitoring collection.
COLLECTION = "eodc/gfm"
cat = Catalog()
gfm = cat.get_collection(COLLECTION)
print(
"endpoint:",
cat.get_endpoint("eodc").url,
"| signer:",
cat.get_endpoint("eodc").signer,
)
print("resolves to:", cat.resolve("eodc", COLLECTION))
print("default asset:", gfm.default_assets)
print("layers:", list(gfm.assets))
Download a flood-extent scene — 2022 Pakistan (Sindh) floods¶
GFM flood extent is sparse: each layer maps only what a given Sentinel-1 swath observed, so an area outside the swath (or not processed that day) is legitimately all-nodata. We therefore pick an area and date where the swath carried flood-mapped pixels — the catastrophic 2022 Indus floods over Sindh on 11 September 2022.
aoi is [west, south, east, north] in degrees; download() writes one Cloud-Optimized GeoTIFF per
(collection, acquisition date) and returns the paths.
import tempfile
from earthlens.core import EarthLens
el = EarthLens(
data_source="eodc",
start="2022-09-11",
end="2022-09-11",
dataset=COLLECTION,
variables=["ensemble_flood_extent"],
aoi=[67.0, 27.0, 68.0, 28.0],
path=tempfile.mkdtemp(),
)
paths = el.download()
paths
Inspect the written COG¶
Open it with pyramids and look at the flood-extent encoding: 0 = no flood observed, 1 = flood, 255 =
nodata (outside the swath / not processed).
import numpy as np
from pyramids.dataset import Dataset
ds = Dataset.read_file(str(paths[0]))
arr = ds.read_array()
values, counts = np.unique(arr, return_counts=True)
print("epsg:", ds.epsg, "| shape:", arr.shape)
print("value histogram:", {int(v): int(c) for v, c in zip(values, counts)})
Map the flood extent¶
Mask the nodata (255) and show the observed no-flood (0) vs flood (1) pixels.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import ListedColormap
band = arr[0] if arr.ndim == 3 else arr
masked = np.ma.masked_equal(band, 255)
cmap = ListedColormap(["#e8e4d8", "#1f6fb4"]) # 0 = dry land, 1 = flood
fig, ax = plt.subplots(figsize=(9, 4))
im = ax.imshow(masked, cmap=cmap, vmin=0, vmax=1, interpolation="nearest")
ax.set_title("GFM ensemble flood extent — Sindh, Pakistan, 2022-09-11")
ax.set_xticks([])
ax.set_yticks([])
cbar = fig.colorbar(im, ax=ax, ticks=[0.25, 0.75], shrink=0.8)
cbar.ax.set_yticklabels(["no flood", "flood"])
flood_px = int((band == 1).sum())
ax.set_xlabel(f"{flood_px:,} flood pixels mapped in this AOI")
plt.tight_layout()
plt.show()
Selecting other layers¶
Any of the twelve layers is selectable through variables=. For example, the reference water mask —
GFM's permanent-water baseline — is useful to separate seasonal flooding from standing water. Pull it over the
same AOI:
el_ref = EarthLens(
data_source="eodc",
start="2022-09-11",
end="2022-09-11",
dataset=COLLECTION,
variables=["reference_water_mask"],
aoi=[67.0, 27.0, 68.0, 28.0],
path=tempfile.mkdtemp(),
)
ref_paths = el_ref.download()
ref = Dataset.read_file(str(ref_paths[0])).read_array()
ref_band = ref[0] if ref.ndim == 3 else ref
print(
"reference_water_mask values:",
{int(v): int(c) for v, c in zip(*np.unique(ref_band, return_counts=True))},
)