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

Sentinel-2 Crop-Type Classification

Authors
Affiliations
Eurac Research
Eurac Research
ESA EOPF Zarr Logo

🚀 Launch in JupyterHub

Run this notebook interactively with all dependencies pre-installed

Preface

The original notebook used as a starting point for this work is a openEO platform example, available here. The example has been adapted to use the data provided by the EOPF Zarr Samples project instead of the openEO API.

Introduction

This notebook demonstrates how to perform rule-based crop-type classification using Sentinel-2 Level-2A data accessed via the EOPF STAC catalog. The objective is to extract cloud-free reflectance signals, compute monthly vegetation indices, and apply phenology-driven rules to identify dominant crop types such as maize, barley, sugar beet, potato, and soy.

The workflow is structured as follows:

  • Data access: Connect to the EOPF STAC API and load Sentinel-2 L2A Zarr cubes for reflectance (10 m, 20 m) and Scene Classification Layer (SCL) bands.

  • Preprocessing: Apply an optimized, Dask-friendly variant of the mask_scl_dilation process to filter clouds, snow, and shadows while preserving valid vegetation pixels.

  • NDVI computation: Derive NDVI from red (B04) and near-infrared (B08) bands and combine it with shortwave-infrared (B11) reflectance to form phenological features.

  • Temporal aggregation: Resample data to monthly means, interpolate gaps, and generate 12-month composites representing seasonal vegetation dynamics.

  • Rule-based classification: Use heuristic decision rules reflecting crop-specific NDVI and SWIR patterns to assign each pixel to a potential crop class.

  • Visualization: Render the resulting categorical crop map with custom colors and legend annotations for easy interpretation.

This pipeline demonstrates how clean, cloud-filtered Sentinel-2 time series can be transformed into phenology-based crop classification maps, forming a foundation for scalable, interpretable, and region-specific agricultural monitoring workflows.

Timeseries analysis of vegetation greenness

Different land cover types can be identified by their seasonal green-up and green-down patterns. NDVI time series capture these phenological dynamics:

  • Coniferous forests remain green all year round, showing little variation in NDVI.

  • Grasslands show moderate seasonal cycles with higher values in spring and summer.

  • Annual crops (e.g., sugar beet, maize, potato) exhibit strong seasonal fluctuations, with steep green-up during growth, a pronounced peak, and rapid decline after harvest.

An overview of NDVI trajectories for several crop types is shown in the plots below. These illustrate how temporal profiles can serve as fingerprints for distinguishing between vegetation classes.

Crop_Type_NDVI_Profiles

Setup

Start importing the necessary libraries

Area of Interest

The region of interest is located over the Belgium 31N tile, referenced in EPSG:32631. For the STAC query, the required input is the bounding box in EPSG:4326 (bbox_4326). Meanwhile, UTM-based slices are generated to subset the dataset, which is natively stored in UTM coordinates.

Datacube Creation

Builds the time-indexed Sentinel-2 L2A datacube for the study area corresponding to the UTM 31N tiles.

Loading...

Random Sampling of Crop Points within Datacube Bounds

This section defines utility functions to generate spatial sample points within the extent of the Sentinel-2 datacube.

  • _raster_bounds_polygon() constructs a shapely bounding box of the raster area (in EPSG:32631).

  • _random_point_in_geom() performs rejection sampling to select random points inside a given crop polygon.

  • simple_random_sampling_within_bounds() integrates these helpers to identify polygons intersecting the datacube, choose a representative one per crop, and sample buffered points within it.

The output is a set of GeoJSON feature collections, each containing a small number of randomly distributed crop-specific sample areas (e.g., maize, barley, potato, sugar beet, soy) ready for time-series extraction and analysis.

maize: 3 polygons within datacube bounds
Using polygon 1 for maize (area: 57299 m²)
potatos: 7 polygons within datacube bounds
Using polygon 1 for potatos (area: 3429 m²)
sugarbeet: 6 polygons within datacube bounds
Using polygon 0 for sugarbeet (area: 17616 m²)
barley: 7 polygons within datacube bounds
Using polygon 0 for barley (area: 26619 m²)
soy: 8 polygons within datacube bounds
Using polygon 0 for soy (area: 1302 m²)
dict_keys(['maize', 'potatos', 'sugarbeet', 'barley', 'soy'])

Point-Series Extraction & NetCDF-Safe Export (with SCL masking)

This section bundles utilities to convert sampled crop locations into clean, 1-D time series and save them as NetCDF safely. It includes:

  • Attribute hygiene: _sanitize_netcdf_attrs() removes/private-attrs and non-serializable metadata so files write cleanly.

  • Grid alignment & subsetting: _align_to_datacube_grid() snaps arbitrary XY to the nearest raster cell; _subset_window() cuts a tiny window (single pixel or buffered) around the point.

  • Local resampling: _interp_like_local() cheaply interpolates ancillary cubes (SCL, B11) onto the local window.

  • Cloud/shadow filtering: apply_simple_scl_mask() masks BAD_SCL classes (shadow, clouds, cirrus, snow/ice) before index computation.

  • Band combine & reduction: _combine_and_reduce() assembles B04, B08, B11, computes NDVI, and reduces the local window to a single time series via median/mean/nearest.

  • Point catalog handling: _geojsons_to_point_df() turns per-crop GeoJSONs into a consolidated EPSG:32631 GeoDataFrame (using buffered-polygon centroids).

  • Main pipeline: extract_point_series_for_crops() tries a single-pixel extraction first, falls back to a buffered window if data are all-NaN, then writes one NetCDF per point with sanitized attributes and returns a mapping {(crop, point_idx): file_path}.

Time-Series Extraction and NetCDF Export (Compute-Intensive Step)

This step executes the per-crop time-series extraction using the previously defined extract_point_series_for_crops() function. It loops through all sampled crop points, applies cloud masking and local reduction, and saves each resulting reflectance + NDVI series as individual NetCDF files in outputs/samples_netcdf/.

⚙️ Note: This step is computationally intensive — you may choose to skip this cell if you want to reduce execution time.

Processing points:   4%|▍         | 1/25 [00:30<12:17, 30.72s/it]
[maize:0 -> seq 1] wrote outputs/samples_netcdf/maize_1.nc
Processing points:   8%|▊         | 2/25 [00:57<10:55, 28.50s/it]
[maize:1 -> seq 2] wrote outputs/samples_netcdf/maize_2.nc
Processing points:  12%|█▏        | 3/25 [01:25<10:17, 28.06s/it]
[maize:2 -> seq 3] wrote outputs/samples_netcdf/maize_3.nc
Processing points:  16%|█▌        | 4/25 [01:52<09:46, 27.93s/it]
[maize:3 -> seq 4] wrote outputs/samples_netcdf/maize_4.nc
Processing points:  20%|██        | 5/25 [02:20<09:17, 27.86s/it]
[maize:4 -> seq 5] wrote outputs/samples_netcdf/maize_5.nc
Processing points:  24%|██▍       | 6/25 [02:42<08:08, 25.69s/it]
[potatos:0 -> seq 1] wrote outputs/samples_netcdf/potatos_1.nc
Processing points:  28%|██▊       | 7/25 [03:00<07:00, 23.37s/it]
[potatos:1 -> seq 2] wrote outputs/samples_netcdf/potatos_2.nc
Processing points:  32%|███▏      | 8/25 [03:19<06:12, 21.88s/it]
[potatos:2 -> seq 3] wrote outputs/samples_netcdf/potatos_3.nc
Processing points:  36%|███▌      | 9/25 [03:37<05:29, 20.62s/it]
[potatos:3 -> seq 4] wrote outputs/samples_netcdf/potatos_4.nc
Processing points:  40%|████      | 10/25 [03:55<04:59, 19.99s/it]
[potatos:4 -> seq 5] wrote outputs/samples_netcdf/potatos_5.nc
Processing points:  44%|████▍     | 11/25 [04:22<05:06, 21.89s/it]
[sugarbeet:0 -> seq 1] wrote outputs/samples_netcdf/sugarbeet_1.nc
Processing points:  48%|████▊     | 12/25 [04:42<04:39, 21.51s/it]
[sugarbeet:1 -> seq 2] wrote outputs/samples_netcdf/sugarbeet_2.nc
Processing points:  52%|█████▏    | 13/25 [05:03<04:15, 21.27s/it]
[sugarbeet:2 -> seq 3] wrote outputs/samples_netcdf/sugarbeet_3.nc
Processing points:  56%|█████▌    | 14/25 [05:24<03:51, 21.06s/it]
[sugarbeet:3 -> seq 4] wrote outputs/samples_netcdf/sugarbeet_4.nc
Processing points:  60%|██████    | 15/25 [05:44<03:27, 20.75s/it]
[sugarbeet:4 -> seq 5] wrote outputs/samples_netcdf/sugarbeet_5.nc
Processing points:  64%|██████▍   | 16/25 [06:05<03:09, 21.03s/it]
[barley:0 -> seq 1] wrote outputs/samples_netcdf/barley_1.nc
Processing points:  68%|██████▊   | 17/25 [06:30<02:56, 22.03s/it]
[barley:1 -> seq 2] wrote outputs/samples_netcdf/barley_2.nc
Processing points:  72%|███████▏  | 18/25 [06:48<02:26, 20.96s/it]
[barley:2 -> seq 3] wrote outputs/samples_netcdf/barley_3.nc
Processing points:  76%|███████▌  | 19/25 [07:07<02:01, 20.25s/it]
[barley:3 -> seq 4] wrote outputs/samples_netcdf/barley_4.nc
Processing points:  80%|████████  | 20/25 [07:25<01:38, 19.72s/it]
[barley:4 -> seq 5] wrote outputs/samples_netcdf/barley_5.nc
Processing points:  84%|████████▍ | 21/25 [07:59<01:35, 23.84s/it]
[soy:0 -> seq 1] wrote outputs/samples_netcdf/soy_1.nc
Processing points:  88%|████████▊ | 22/25 [08:27<01:15, 25.27s/it]
[soy:1 -> seq 2] wrote outputs/samples_netcdf/soy_2.nc
Processing points:  92%|█████████▏| 23/25 [08:55<00:52, 26.07s/it]
[soy:2 -> seq 3] wrote outputs/samples_netcdf/soy_3.nc
Processing points:  96%|█████████▌| 24/25 [09:24<00:26, 26.87s/it]
[soy:3 -> seq 4] wrote outputs/samples_netcdf/soy_4.nc
Processing points: 100%|██████████| 25/25 [09:53<00:00, 23.74s/it]
[soy:4 -> seq 5] wrote outputs/samples_netcdf/soy_5.nc
CPU times: user 16min 24s, sys: 3min 20s, total: 19min 45s
Wall time: 9min 53s

NDVI Monthly Boxplot Generation for Crop Samples

This section visualizes seasonal NDVI variations across multiple crop types using the extracted time-series data.

  • _monthly_ndvi_series() computes monthly mean NDVI values (January–December) for each sample file, handling missing or sparse observations gracefully.

  • prep_ndvi_boxplot_df() aggregates all crop-wise NDVI series into a unified pandas DataFrame structured by crop, month, and sample iteration.

  • create_ndvi_boxplots() generates monthly NDVI boxplots for each crop (maize, barley, sugar beet, potato, soy), showing how vegetation activity evolves through the year.

The resulting plots—saved in outputs/ndvi_boxplots/—help compare phenological patterns between crops and validate whether NDVI dynamics align with expected growing seasons.

<Figure size 1400x600 with 1 Axes>
Saved: outputs/ndvi_boxplots/ndvi_boxplot_barley_2019.png
<Figure size 1400x600 with 1 Axes>
Saved: outputs/ndvi_boxplots/ndvi_boxplot_maize_2019.png
<Figure size 1400x600 with 1 Axes>
Saved: outputs/ndvi_boxplots/ndvi_boxplot_potatoes_2019.png
<Figure size 1400x600 with 1 Axes>
Saved: outputs/ndvi_boxplots/ndvi_boxplot_soy_2019.png
<Figure size 1400x600 with 1 Axes>
Saved: outputs/ndvi_boxplots/ndvi_boxplot_sugarbeet_2019.png

Data Access and Subsetting over a 5 × 5 km Crop Diversity Region in Belgium

This section connects to the EOPF STAC catalog and retrieves Sentinel-2 Level-2A Zarr datasets for a carefully selected 5 km × 5 km region in Belgium. The chosen area was identified because it contains all five target crop types—maize, barley, sugar beet, potato, and soy—making it an ideal test patch for classification and phenological analysis.

The workflow:

  • Defines the spatial extent and bounding box (EPSG:4326 and EPSG:32631) covering the region.

  • Connects to the EOPF STAC API to query Sentinel-2 L2A scenes between November 2018 and February 2020.

  • Loads the Scene Classification Layer (SCL), filters clear-sky observations (classes 4–6), and selects time steps with at least 10 % valid pixels.

  • Opens corresponding reflectance (B04, B08, B11) cubes and resamples them to the same grid.

  • Produces a cloud-filtered, spatially consistent datacube that will serve as the foundation for NDVI computation and crop-type classification.

Loading...

Saving and Reloading the Clear-Sky Datacube

This step saves the processed cloud-filtered Sentinel-2 datacube to disk and reloads it for subsequent analysis. This approach provides a lightweight, reusable version of the clean Sentinel-2 reflectance datacube, ready for NDVI computation, temporal aggregation, and rule-based crop classification.

Loading...

Cloud & Artifact Masking via Per-Timestamp SCL Dilation

This section implements a direct, per-time-step version of mask_scl_dilation to aggressively remove cloud, shadow, and adjacency effects from Sentinel-2 scenes.

  • Builds circular morphological kernels (r=8 px and r=100 px) and dilates two masks: one for non-valid classes and one for cloud/shadow/snow classes.

  • Applies the combined mask to all reflectance bands (leaving SCL untouched), replacing contaminated pixels with NaN.

  • Keeps only time steps that retain any valid (non-NaN) data after masking, then concatenates them back into a cleaned time series.

⚙️ Note: The large-radius dilation (r=100) can be CPU/memory intensive; consider reducing the radius if you encounter resource limits.

Processing time steps: 100%|██████████| 35/35 [00:12<00:00,  2.73it/s]
Loading...

Saving and Reloading the Cloud-Masked Dataset

This step exports the cloud-filtered Sentinel-2 dataset produced by the mask_scl_dilation_direct() function and reloads it for verification. This ensures the cloud-masked datacube is safely persisted to disk and ready for downstream NDVI computation and temporal aggregation.

Loading...

NDVI Computation from Cloud-Masked Reflectance Data

This section derives the Normalized Difference Vegetation Index (NDVI) from the cloud-free Sentinel-2 datacube.

  • Uses the near-infrared (B08) and red (B04) reflectance bands to compute: [ \text{NDVI} = \frac{B08 - B04}{B08 + B04} ]

  • Ensures numerical stability by handling divisions where the denominator is zero and casting values to float32 precision.

  • Returns a new DataArray named NDVI, representing vegetation vigor for each pixel and time step.

This NDVI layer forms the foundation for analyzing vegetation dynamics and distinguishing different crop phenologies.

Loading...

Monthly Aggregation, Gap-Filling, and 12-Month Composites

This section converts the cloud-masked dataset into monthly features suitable for phenology analysis:

  • Resamples to month starts (1MS) and computes monthly means.

  • Reindexes to a continuous monthly timeline and linearly interpolates small gaps (limit = 1 month).

  • Clips NDVI to [0, 1] and then averages by time.month to produce a 12 month composites (Jan–Dec) for each variable.

Loading...

Monthly Band-Stack Creation for NDVI, NIR, and SWIR Features

This section restructures the 12-month climatology into a band-stacked dataset that’s easier to use for classification and visualization.

  • rename_monthly_vars() renames each month of a given variable (e.g., ndvi, b08, b11) into distinct band names like NDVI_jan, NDVI_feb, …, NDVI_dec.

  • The resulting monthly layers for NDVI, NIR (B08), and SWIR (B11) are concatenated along a new band dimension, forming a single 3D array (band, y, x).

  • This compact stack captures seasonal reflectance and vegetation dynamics across all months, serving as the feature base for rule-based or machine learning–based crop classification.

Loading...

Key Monthly Feature Selection for Rule Logic

This section extracts a targeted subset of monthly bands from the stacked feature cube to drive interpretable, rule-based classification. By focusing on months that best capture crop phenology (spring green-up, summer peak, and autumn senescence), we reduce noise and keep features meaningful.

  • NDVI features: NDVI_jan, _apr, _may, _jun, _jul, _aug, _sep, _oct, _nov — track vegetation onset, peak growth, and decline.

  • NIR (B08) features: B08_mar, _may, _jun, _oct — sensitive to canopy structure and biomass.

  • SWIR (B11) features: B11_mar, _apr, _may, _oct — informative for moisture/stress and residue.

These focused features provide clear seasonal signals that differentiate maize, barley, sugar beet, potato, and soy with simple, transparent rules.

Rule-Based Crop Classification Masks

This section defines binary masks (0/1) for each crop using seasonal NDVI, NIR, and SWIR patterns. These rules capture key phenological signatures to separate major crop types.

Encoding Crop Classes into a Composite Map

This section merges all binary crop masks into a single categorical map and scales it for visualization.

  • Each crop is assigned a unique bit weight — corn (1), barley (2), sugar beet (4), potato (8), and soy (16).

  • These masks are summed into a combined layer (total), encoding possible overlaps or unique crop pixels.

  • The helper function linear_scale_range() linearly rescales the result (0–32) to maintain consistent display range.

This produces a compact encoded crop map ready for color mapping and legend visualization.

Categorical Crop Map Visualization

This section renders the rule-based crop classification as a categorical image. Builds a label map (comb) and a colormap from col_palette, then displays the encoded array with imshow. Creates a legend from the active class keys to show crop names.

<Figure size 1000x800 with 1 Axes>