How to access the Normalized digital surface model with stac

from odc import stac as odc_stac
import xarray as xr
import matplotlib.pyplot as plt
import geopandas
import numpy as np
from pystac_client import Client

Connect to the eodc stac

eodc_catalog = Client.open(
    "https://stac.eodc.eu/api/v1",
)
# Select the region
bbox = [16.75, 47.92, 16.79, 47.95]

# Normalized digital surface model (if you want to use the Digital terrain model use collections=["topo-dc-austria-dtm"] and for the Digital surface model use collections=["topo-dc-austria-dsm"])
search = eodc_catalog.search(
    collections=["topo-dc-austria-ndsm"],
    # collections=["topo-dc-austria-dtm"],
    # collections=["topo-dc-austria-dsm"],
    bbox = bbox,
)

print("We found ", search.matched(), " items, that match our filter criteria.")

items = list(search.items())
We found  16  items, that match our filter criteria.

Select all items with the datetime 2024

Even if the time of the data is 2024 the time of the collected data is 2023, so keep that in mind depending on what you want to calculate

items_2024 = []
for item in items:
    dt = item.properties.get("datetime", "")
    if dt.startswith("2024"):
        items_2024.append(item)
items_2024
[<Item id=N2785312_E4827535_nDSM_2024>,
 <Item id=N2785312_E4823439_nDSM_2024>,
 <Item id=N2781216_E4827535_nDSM_2024>,
 <Item id=N2781216_E4823439_nDSM_2024>]

Create a xarray dataset from the items

crs = "EPSG:3035"   # native CRS
res = 1             # meters (from gsd)

ds_2024 = odc_stac.load(
    items_2024,
    crs=crs,
    resolution=res,
    bbox=bbox,
)
print(ds_2024)
<xarray.Dataset> Size: 94MB
Dimensions:      (y: 3597, x: 3274, time: 1)
Coordinates:
  * y            (y) float64 29kB 2.783e+06 2.783e+06 ... 2.779e+06 2.779e+06
  * x            (x) float64 26kB 4.825e+06 4.825e+06 ... 4.828e+06 4.828e+06
    spatial_ref  int32 4B 3035
  * time         (time) datetime64[ns] 8B 2024-09-15
Data variables:
    data         (time, y, x) float32 47MB 0.4529 0.1094 0.0 0.0 ... 0.0 0.0 0.0
    mask         (time, y, x) float32 47MB 255.0 255.0 255.0 ... 255.0 255.0
# print some values
ds_2024["data"].isel(time=0, y=slice(0, 5), x=slice(0, 5)).values
array([[4.52926636e-01, 1.09436035e-01, 0.00000000e+00, 0.00000000e+00,
        2.13623047e-04],
       [1.28112793e-01, 2.02941895e-02, 0.00000000e+00, 4.42504883e-04,
        4.27246094e-04],
       [0.00000000e+00, 0.00000000e+00, 3.50952148e-04, 3.35693359e-04,
        4.73022461e-04],
       [1.37329102e-04, 3.05175781e-05, 0.00000000e+00, 4.57763672e-05,
        1.52587891e-04],
       [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
        0.00000000e+00]], dtype=float32)

Plot only the data

import matplotlib.pyplot as plt

plt.figure(figsize=(8, 6))
ds_2024["data"].isel(time=0).plot(
    cmap="viridis",
    vmin=0,
    vmax=15
)
plt.title("nDSM 2023")
plt.show()
../_images/b57cd93e5b72dfd2738eed52542fcf5d209c5b33d13d04352241b97e901e8cda.png

Plot the masked data

da = ds_2024["data"].isel(time=0)
mask = ds_2024["mask"].isel(time=0)

da_masked = da.where(mask == 255)

plt.figure(figsize=(8, 6))
da_masked.plot(
    cmap="viridis",
    vmin=0,
    vmax=15
)
plt.title("nDSM 2023 after mask")
plt.show()
../_images/264e8bfe2fc82b91308efd604c095d9c175cbda0b4553453ab1580ec235ad8a1.png

Create a dataset from all years

Sometimes you need to run the stac search again to make it work

crs = "EPSG:3035"   # native CRS -> known from the stac metadata
res = 1             # meters

ds_combined = odc_stac.load(
    items,
    crs=crs,
    resolution=res,
    bbox=bbox,
)

print(ds_combined)
<xarray.Dataset> Size: 377MB
Dimensions:      (y: 3597, x: 3274, time: 4)
Coordinates:
  * y            (y) float64 29kB 2.783e+06 2.783e+06 ... 2.779e+06 2.779e+06
  * x            (x) float64 26kB 4.825e+06 4.825e+06 ... 4.828e+06 4.828e+06
    spatial_ref  int32 4B 3035
  * time         (time) datetime64[ns] 32B 2021-09-15 2022-09-15 ... 2024-09-15
Data variables:
    data         (time, y, x) float32 188MB 0.8824 0.007553 0.0 ... 0.0 0.0 0.0
    mask         (time, y, x) float32 188MB 255.0 255.0 255.0 ... 255.0 255.0
from pyproj import Transformer

lon_min, lat_min, lon_max, lat_max = 16.773838,47.934488,16.773908,47.934538

t = Transformer.from_crs("EPSG:4326", "EPSG:3035", always_xy=True)
x_min, y_min = t.transform(lon_min, lat_min)
x_max, y_max = t.transform(lon_max, lat_max)

# make sure min/max are ordered
x0, x1 = sorted([x_min, x_max])
y0, y1 = sorted([y_min, y_max])

print(x0, y0, x1, y1)
4826606.8683285285 2781068.5887244726 4826611.581265222 2781074.605533941

Create a subset for detailed analysis

subset = ds_combined.sel(
    x=slice(x0, x1),
    y=slice(y1, y0),
)
print(subset)
<xarray.Dataset> Size: 1kB
Dimensions:      (y: 6, x: 5, time: 4)
Coordinates:
  * y            (y) float64 48B 2.781e+06 2.781e+06 ... 2.781e+06 2.781e+06
  * x            (x) float64 40B 4.827e+06 4.827e+06 ... 4.827e+06 4.827e+06
    spatial_ref  int32 4B 3035
  * time         (time) datetime64[ns] 32B 2021-09-15 2022-09-15 ... 2024-09-15
Data variables:
    data         (time, y, x) float32 480B 0.0 0.0005722 ... 0.0003815 0.0005188
    mask         (time, y, x) float32 480B 255.0 255.0 255.0 ... 255.0 255.0
data = subset["data"].where(subset["mask"] == 255)

ts_mean = data.mean(dim=("y", "x"))
print(ts_mean.values)
[0.00162608 0.00169907 0.00152791 0.00162582]
ts_mean.plot(marker="o")
plt.title("Mean nDSM in bbox over time")
plt.ylabel("nDSM")
plt.show()
../_images/73a8ab8c1f86c817c274da97cb058ee9c9a70422c06b31e6401107a22f6c9be5.png

Plot the trajectories of each pixel over the years

sub_data = subset["data"]              # (time, y, x)
pix = sub_data.stack(pixel=("y", "x"))  # (time, pixel)

plt.figure(figsize=(9, 5))
for i in range(pix.sizes["pixel"]):
    plt.plot(pix["time"].values, pix.isel(pixel=i).values, marker="o", linewidth=1)

plt.title("Pixel trajectories (6×5 window) over time")
plt.xlabel("Time")
plt.ylabel("nDSM")
plt.show()
../_images/1d1a15b54152f77205a6632fcd4bb9d6a91befa56401bc8c956801e818a49def.png