Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

earth and related environmental sciences

Computing Leaf Area Index with Sentinel-2 in EOPF Zarr format

LAI with Sentinel-2

Authors
Affiliations
Eurac Research
Eurac Research
Eurac Research
Eurac Research
ESA EOPF Zarr Logo

🚀 Launch in JupyterHub

Run this notebook interactively with all dependencies pre-installed

Introduction

This notebook focuses on retrieving Leaf Area Index (LAI) from Sentinel-2 data using the Simplified Level 2 Prototype Processor (SL2P) framework [Weiss & Baret 2016] as implemented by Djamai et al., 2024.

LAI is a key biophysical variable for describing vegetation canopy structure and is defined “total leaf area per unit of ground area" [Watson, 1947]. In practice, LAI is widely used to monitor vegetation productivity, evapotranspiration, and drought through vegetation status and land–atmosphere interactions from remote sensing data.

SL2P is a physics-informed hybrid retrieval model. It is useful because it provides an operational way to estimate vegetation biophysical variables such as LAI from Sentinel-2 reflectance data. It combines simulations derived from the radiative transfer model PROSAIL [Jacquemoud et al., 2009] with deep learning based retrievals, making it suitable for large-scale, repeatable processing of satellite imagery.

This notebook is therefore useful both for understanding the SL2P workflow and for building a reproducible Python-based implementation for LAI retrieval.

Dj amai N., Fernandes, R.A. Sun. L., Canisius, F. and Hong. G, 2024. Python version of Simplified Level 2 Prototype Processor for Retrieving Canopy Biophysical Variables from Sentinel 2 Multispectral Instrument Data, Open File

Jacquemoud, S., Verhoef, W., Baret, F., Bacour, C., Zarco-Tejada, P. J., Asner, G. P., François, C., & Ustin, S. L. (2009). “PROSPECT+SAIL models: A review of use for vegetation characterization.” Remote Sensing of Environment, 113, S56–S66

Weiss, M., & Baret, F. (2016). S2ToolBox Level 2 Products: LAI, FAPAR, FCOVER (Version 1.1). Institut National de la Recherche Agronomique (INRA), Avignon, France.

Watson, D. J. (1947). Comparative physiological studies on the growth of field crops: I. Variation in net assimilation rate and leaf area between species and varieties, and within and between years. Annals of Botany, 11(41), 41–76

Setup

Requirement already satisfied: xarray-eopf in /opt/conda/lib/python3.12/site-packages (0.3.1)
Requirement already satisfied: aiohttp in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (3.14.3)
Requirement already satisfied: dask in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2026.7.1)
Requirement already satisfied: dask-image in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2026.5.0)
Requirement already satisfied: flox in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (0.11.2)
Requirement already satisfied: fsspec in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2026.6.0)
Requirement already satisfied: numpy in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2.4.6)
Requirement already satisfied: pyproj in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (3.7.2)
Requirement already satisfied: requests in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2.34.2)
Requirement already satisfied: rioxarray in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (0.20.0)
Requirement already satisfied: s3fs in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2026.6.0)
Requirement already satisfied: xarray>=2024.10 in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2024.11.0)
Requirement already satisfied: xcube-resampling>=0.3.4 in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (0.3.5)
Requirement already satisfied: zarr<3,>=2.11 in /opt/conda/lib/python3.12/site-packages (from xarray-eopf) (2.18.7)
Requirement already satisfied: asciitree in /opt/conda/lib/python3.12/site-packages (from zarr<3,>=2.11->xarray-eopf) (0.3.3)
Requirement already satisfied: fasteners in /opt/conda/lib/python3.12/site-packages (from zarr<3,>=2.11->xarray-eopf) (0.20)
Requirement already satisfied: numcodecs!=0.14.0,!=0.14.1,<0.16,>=0.10.0 in /opt/conda/lib/python3.12/site-packages (from zarr<3,>=2.11->xarray-eopf) (0.15.1)
Requirement already satisfied: deprecated in /opt/conda/lib/python3.12/site-packages (from numcodecs!=0.14.0,!=0.14.1,<0.16,>=0.10.0->zarr<3,>=2.11->xarray-eopf) (1.3.1)
Requirement already satisfied: packaging>=23.2 in /opt/conda/lib/python3.12/site-packages (from xarray>=2024.10->xarray-eopf) (26.2)
Requirement already satisfied: pandas>=2.1 in /opt/conda/lib/python3.12/site-packages (from xarray>=2024.10->xarray-eopf) (3.0.3)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.12/site-packages (from pandas>=2.1->xarray>=2024.10->xarray-eopf) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas>=2.1->xarray>=2024.10->xarray-eopf) (1.17.0)
Requirement already satisfied: affine>=2.2 in /opt/conda/lib/python3.12/site-packages (from xcube-resampling>=0.3.4->xarray-eopf) (2.4.0)
Requirement already satisfied: numba>=0.52 in /opt/conda/lib/python3.12/site-packages (from xcube-resampling>=0.3.4->xarray-eopf) (0.66.0)
Requirement already satisfied: click>=8.1 in /opt/conda/lib/python3.12/site-packages (from dask->xarray-eopf) (8.4.2)
Requirement already satisfied: cloudpickle>=3.0.0 in /opt/conda/lib/python3.12/site-packages (from dask->xarray-eopf) (3.1.2)
Requirement already satisfied: partd>=1.4.0 in /opt/conda/lib/python3.12/site-packages (from dask->xarray-eopf) (1.4.2)
Requirement already satisfied: pyyaml>=5.4.1 in /opt/conda/lib/python3.12/site-packages (from dask->xarray-eopf) (6.0.3)
Requirement already satisfied: toolz>=0.12.0 in /opt/conda/lib/python3.12/site-packages (from dask->xarray-eopf) (1.1.0)
Requirement already satisfied: scipy>=1.7.0 in /opt/conda/lib/python3.12/site-packages (from dask-image->xarray-eopf) (1.17.1)
Requirement already satisfied: pims>=0.4.1 in /opt/conda/lib/python3.12/site-packages (from dask-image->xarray-eopf) (0.7)
Requirement already satisfied: tifffile>=2020.10.1 in /opt/conda/lib/python3.12/site-packages (from dask-image->xarray-eopf) (2026.3.3)
Requirement already satisfied: llvmlite<0.49,>=0.48.0dev0 in /opt/conda/lib/python3.12/site-packages (from numba>=0.52->xcube-resampling>=0.3.4->xarray-eopf) (0.48.0)
Requirement already satisfied: locket in /opt/conda/lib/python3.12/site-packages (from partd>=1.4.0->dask->xarray-eopf) (1.0.0)
Requirement already satisfied: imageio in /opt/conda/lib/python3.12/site-packages (from pims>=0.4.1->dask-image->xarray-eopf) (2.37.0)
Requirement already satisfied: slicerator>=0.9.8 in /opt/conda/lib/python3.12/site-packages (from pims>=0.4.1->dask-image->xarray-eopf) (1.1.0)
Requirement already satisfied: certifi in /opt/conda/lib/python3.12/site-packages (from pyproj->xarray-eopf) (2026.7.22)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (2.7.1)
Requirement already satisfied: aiosignal>=1.4.0 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (1.4.0)
Requirement already satisfied: attrs>=17.3.0 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (26.1.0)
Requirement already satisfied: frozenlist>=1.1.1 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (1.8.0)
Requirement already satisfied: multidict<7.0,>=4.5 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (6.7.1)
Requirement already satisfied: propcache>=0.2.0 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (0.5.2)
Requirement already satisfied: typing_extensions>=4.4 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (4.16.0)
Requirement already satisfied: yarl<2.0,>=1.17.0 in /opt/conda/lib/python3.12/site-packages (from aiohttp->xarray-eopf) (1.24.5)
Requirement already satisfied: idna>=2.0 in /opt/conda/lib/python3.12/site-packages (from yarl<2.0,>=1.17.0->aiohttp->xarray-eopf) (3.18)
Requirement already satisfied: wrapt<3,>=1.10 in /opt/conda/lib/python3.12/site-packages (from deprecated->numcodecs!=0.14.0,!=0.14.1,<0.16,>=0.10.0->zarr<3,>=2.11->xarray-eopf) (1.17.3)
Requirement already satisfied: numpy_groupies>=0.9.19 in /opt/conda/lib/python3.12/site-packages (from flox->xarray-eopf) (0.11.3)
Requirement already satisfied: pillow>=8.3.2 in /opt/conda/lib/python3.12/site-packages (from imageio->pims>=0.4.1->dask-image->xarray-eopf) (12.3.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /opt/conda/lib/python3.12/site-packages (from requests->xarray-eopf) (3.4.9)
Requirement already satisfied: urllib3<3,>=1.26 in /opt/conda/lib/python3.12/site-packages (from requests->xarray-eopf) (2.7.0)
Requirement already satisfied: rasterio>=1.4.3 in /opt/conda/lib/python3.12/site-packages (from rioxarray->xarray-eopf) (1.5.0)
Requirement already satisfied: cligj>=0.5 in /opt/conda/lib/python3.12/site-packages (from rasterio>=1.4.3->rioxarray->xarray-eopf) (0.7.2)
Requirement already satisfied: pyparsing in /opt/conda/lib/python3.12/site-packages (from rasterio>=1.4.3->rioxarray->xarray-eopf) (3.3.2)
Requirement already satisfied: aiobotocore<4.0.0,>=2.19.0 in /opt/conda/lib/python3.12/site-packages (from s3fs->xarray-eopf) (2.22.0)
Requirement already satisfied: aioitertools<1.0.0,>=0.5.1 in /opt/conda/lib/python3.12/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->xarray-eopf) (0.13.0)
Requirement already satisfied: botocore<1.37.4,>=1.37.2 in /opt/conda/lib/python3.12/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->xarray-eopf) (1.37.3)
Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /opt/conda/lib/python3.12/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs->xarray-eopf) (1.1.0)
Note: you may need to restart the kernel to use updated packages.

STAC Search

Define AOI and convert to raster CRS (area above Bolzano, South Tyrol, tile T32TPS)

SL2P biophysical variable retrieval

Preprocess input Sentinel-2 Zarr cubes and run SL2P on them .

Expected inputs:

  • 10 m grid

  • Bands: B03,B04,B05,B06,B07,B8A,B11,B12 (SCL optional)

  • Angular features already as cosines: cosSZA, cosVZA, cosRAA

Parameters

In the original SL2P version, we could derive different types of variables (LAI, fAPAR, fCover, CCC, CWC, Albedo), from different types of input images (Sentinel-2, Landsat8/9, Harmonized Landsat-Sentinel), using two different algorithms (SL2P, SL2P-CCRS).

For the scope of this notebook, the possibilities have been reduced to the only case study of LAI from the SL2P algorithm using Sentinel-2 multispectral inputs

Furthemore, we can select whether the input data are already prescaled (PRESCALED_INPUT). Zarr cubes are already scaled to surface reflectance (0-1), so we skip SL2P pre-scaling.

We can choose to clip the input tile to an Area of Interest (AOI), and in this case we choose to clip the tile over some field polygons

Validation and options

This cells checks whether the conditions to create the neural network are met. If yes, it proceeds to extract the network options.

The required input layers for the SL2P LAI at 10 meters are: ['cosVZA', 'cosSZA', 'cosRAA', 'B03', 'B04', 'B05', 'B06', 'B07', 'B8A', 'B11', 'B12']

Data preprocessing

  • Read the data from the urls obtained from the STAC Items

  • Combine the Sentinel-2 bands with the sun angles

  • Clips to the area of interest extent (minimum bounding box around it)

  • Stores the clipped pre-processed cube in a Zarr (necessary for memory management)

  • Finally recombines all the Zarr products into an Xarray Dataset loaded in memory

Run the data pre-processing in parallel using 3 cores. Max number on EOPF JupyterLab, otherwise the memory will be filled and the kernel will crash.

/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
/opt/conda/lib/python3.12/site-packages/dask/array/chunk.py:288: RuntimeWarning: invalid value encountered in cast
  return x.astype(astype_dtype, **kwargs)
CPU times: user 356 ms, sys: 76 ms, total: 432 ms
Wall time: 1min 45s

Open the multiple Zarr products as a single Xarray object:

Loading...

Collect and summarize zonal statistics for a single polygon geometry on the SL2P LAI output. It applies the same quality masks as SL2P to ensure only valid pixels are included in the stats.

This function computes zonal statistics for all polygons in a GeoDataFrame on the SL2P LAI output and returns a GeoDataFrame ready for mapping.

Main SL2P Algorithm

This cell iterates over the time series dataset, preparing inputs and then running the SL2P model to retrieve LAI for each timestep of the temporal window.

For each date, it optionally clips the data to the AOI, computes zonal statistics over the selected geometry, and stores the results in a time-ordered dataframe.

For the purposes of this specific notebook, it also shows the classified polygons for a brief temporal window from June to July 2023

Processing time steps: 100%|██████████| 74/74 [00:21<00:00,  3.42it/s]
<Figure size 600x1200 with 35 Axes>
Processing complete
Loading...

LAI time series

Visualize the time-series of the zonal statistics collected over the plot. For each valid data point we will visualize the mean, the median, and the standard deviation.

<Figure size 1200x500 with 1 Axes>

How can we interpret this graph showcasing the season evolution of canopy development and disturbance effects over a managed grassland field?

Early season (May–early June): LAI values are relatively high (~4–6), indicating a well-developed canopy. The variability suggests some spatial heterogeneity within the field, possibly linked to uneven growth or early management differences. There is not a lot of data, probably due to high cloud presence in early May.

Sharp drop (early June): The abrupt decrease to ~1–1.5 is characteristic of a mowing event, where biomass is removed. This marks a reset of the canopy.

Regrowth phase (June–July): LAI gradually increases, reflecting vegetation regrowth after mowing. The curve is relatively smooth, which is typical of continuous biomass accumulation.

Mid-season variability (July–August): You see fluctuations and occasional dips. These can be interpreted as: Secondary disturbances (e.g., mowing over multiple days → partial drops) Lodging effects, where flattened grass alters reflectance and leads to underestimated LAI Retrieval noise, especially when canopy structure is complex

Peak and second decline (August–September): Another growth peak (~3–4.5) is followed by a decline, again consistent with subsequent mowing cycles typical of managed grasslands.

Late season (September–October): Overall lower LAI with smaller amplitude cycles, indicating: Reduced regrowth capacity (seasonal effect) Continued management (smaller or partial cuts) Increased noise/outliers due to senescence, moisture changes, or residual biomass on the field.

Analysis of LAI Time Series

<Figure size 1200x500 with 1 Axes>

We could already spot some trends in the time series - but let’s add some context information.

The red dashed lines indicate possible occurrences of mowing events, that would explain the sudden drop and subsequent regrowth of LAI values. Following these dates, a decrease in LAI is expected in following observations, as biomass is removed. However, in some cases, grass is left on the field to dry after cutting, which can interfere with satellite retrieval and lead to inconsistencies in the estimated LAI values.

Additionally, mowing is not always performed uniformly across the field and may occur over multiple days. This can result in partial or gradual reductions in LAI rather than a sharp drop immediately after a single date.

The green dashed lines represent possible occurrences of lodging events. The observed patterns around these dates are consistent with lodging phenomena. Lodging is a phenomenon that occurs in grasslands when tall grass is bent over or is flattened due to strong weather events, such as rain storms or strong winds, or their own weight. This causes the canopy to lie more horizontally and alters light reflectance, affecting the estimation from satellite, leading to an apparent reduction in LAI in the satellite retrievals, even if the actual leaf area has not decreased.

Credits: Castelli, M., Mejia-Aguilar, A., Göhner, C., & Stendardi, L. (2025). Two years of in situ measurements of biophysical variables of mountain meadows in northeastern Italy [Data set]. Zenodo. Castelli et al. (2025)

References
  1. Castelli, M., Mejia-Aguilar, A., Göhner, C., & Stendardi, L. (2025). Two years of in situ measurements of biophysical variables of mountain meadows in northeastern Italy. Zenodo. 10.5281/ZENODO.17984683