Skip to content

Geometry & ML

Two optional modules turn observed events into numbers, label rasters and scores. They need extras, so the core keeps its single dependency:

pip install 'copernicus-ems[geo]'   # shapely, pyproj: areas, observed dry
pip install 'copernicus-ems[ml]'    # + numpy: label rasters, scores, chips

Areas and summaries (copernicus_ems.geo)

from copernicus_ems.geo import observed_dry, summary

async with RapidMapping() as ems:
    events = await ems.observed_events("EMSR664")

for s in summary(events):
    print(s.product, s.aoi_name, f"{s.flooded_km2:.2f} km² of {s.observed_km2:.1f}")
AOI01_DEL_MONIT01 Forlì 112.06 km² of 3214.4
AOI02_GRA_PRODUCT Lugo 135.30 km² of 455.4
AOI07_GRA_PRODUCT Ravenna 63.81 km² of 419.4
...

Areas are geodesic (WGS84 ellipsoid), with the flooded union clipped to the AOI. On EMSR762 AOI01 they match Rapid Mapping's own area attribute within 1%. flooded(event), observed_dry(event) and mask(event) return shapely geometries in lon/lat.

Tri-state label rasters (copernicus_ems.ml)

Value Meaning Where
1 flooded inside an observedEventA feature
0 dry inside the AOI, outside every feature
255 ignore outside the AOI: nobody looked
from copernicus_ems.ml import rasterize

raster = rasterize(events[0], resolution_m=20)  # UTM zone of the AOI by default
raster.labels  # numpy uint8, rows by columns
raster.grid.crs  # "EPSG:32632"
raster.grid.transform  # GDAL affine: rasterio.Affine(*raster.grid.transform)
raster.meta  # activation, product, observed_time, source credit

Pixels are labelled by their centre. On the 11 products of EMSR664, at 20 m, the flooded pixels add up to the published areas within 0.1%.

255 goes straight into a training loss, so a model is never punished for predicting water where nobody looked:

loss = torch.nn.CrossEntropyLoss(ignore_index=255)

To align with a satellite scene, pass its grid: rasterize(event, grid=Grid(crs=..., x0=..., y0=..., resolution_m=10, width=..., height=...)).

Scoring a flood model, only where someone looked

from copernicus_ems.ml import evaluate, evaluate_geometry, Scores

scores = evaluate(prediction, raster)  # boolean array on raster's grid
scores = evaluate_geometry(predicted_geojson, events[0], resolution_m=20)

scores.csi, scores.pod, scores.far, scores.f1, scores.bias
scores.unobserved_predicted_km2  # outside the AOI: reported, never a false alarm

overall = sum((evaluate_geometry(p, e) for p, e in pairs), Scores.zero())
Score Formula Reads as
CSI (= IoU) hits / (hits + misses + false alarms) overall overlap
POD hits / (hits + misses) share of the observed flood found
FAR false alarms / (hits + false alarms) share of predicted flood that was observed dry
bias (hits + false alarms) / (hits + misses) above 1 over-predicts

Ratios are nan when undefined. Scores add up, pooling pixels across products, and refuse to mix resolutions.

A lower bound

Satellite delineation misses short urban floods and water under canopy: some "false alarms" inside the AOI may be floods nobody saw.

Chips for training

from pathlib import Path
from copernicus_ems.ml import chips

for chip in chips(raster, size=256, min_observed=0.5, min_flooded=0.01):
    chip.save(Path("dataset/labels"))  # .npy + .json sidecar
    chip.save(Path("dataset/labels"), file_format="tif")  # or a GeoTIFF

GeoTIFF

raster.to_geotiff(Path("labels/EMSR664_AOI07.tif"))
back = LabelRaster.from_geotiff(Path("labels/EMSR664_AOI07.tif"))

Single band uint8, deflate-compressed and tiled, 255 as nodata, so QGIS and GDAL show only what was observed; the provenance (activation, product, observed time, Copernicus credit) is in the file's tags. GeoTIFF needs rasterio, which the ml extra installs.

stride= makes overlapping tiles; edge tiles are padded with 255. On EMSR664, 256 × 20 m chips with at least 1% flood: 185.

The imagery to pair the labels with (Sentinel-1/2) is the next step of the roadmap.