{ "cells": [ { "cell_type": "markdown", "id": "aa18db08-c8b0-455f-a9df-572ba0136cb6", "metadata": {}, "source": [ "# Processing multispectral satellite images with Python" ] }, { "cell_type": "markdown", "id": "72d27f2a-daad-425f-910e-f14bb4b408cb", "metadata": {}, "source": [ "The goal of this short tutorial is to get familiar with Sentinel-2 data (spatial / spectral resolutions, georeferencing...). You will use the [rasterio](https://rasterio.readthedocs.io/en/latest/) library to read and process the Sentinel-2 images. Rasterio is a very useful library made to access geospatial raster (=image) data, often stored in a [GeoTIFF](https://en.wikipedia.org/wiki/GeoTIFF) format (a [TIFF](https://en.wikipedia.org/wiki/TIFF) format with georeferencing information).\n", "\n", "Sentinel-2 data is stored and distributed into tiles which are 109.8 km × 109.8 km subdivisions of the [UTM grid](https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system) (see [here](https://hls.gsfc.nasa.gov/products-description/tiling-system/) for further details). In the `S2_SUBSET` folder, you will find a subset of one Sentinel-2 tile over Provence-Alpes-Côte-d’Azur, downloaded from the [GEODES](https://geodes.cnes.fr/en/homepage/) portal. There are many different portals (e.g. [Copernicus Browser](https://browser.dataspace.copernicus.eu/)) to access data of the Copernicus program, that may distribute slightly different products (pre-processed with different algorithms). To my knowledge, every portal provides different levels of products, that correspond to different steps of pre-processing. The two main levels are L1C (top-of-atmosphere reflectance data), and L2A (bottom-of-atmosphere reflectance data).\n", "\n", "In the following, we will pre-process and store the L2A Sentinel-2 data into a Numpy ndarray / PyTorch tensor, such that we can apply signal processing / machine learning algorithms. Note that the [S2-Agri Pixel-Set](https://zenodo.org/records/5815488) data set used in Tutorial 1 has probably been created with the Rasterio library." ] }, { "cell_type": "code", "execution_count": 1, "id": "0b8353b7-243d-46e1-a8e3-9a971420037c", "metadata": {}, "outputs": [], "source": [ "import os\n", "import urllib.request\n", "\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "\n", "import rasterio\n", "from rasterio.enums import Resampling\n", "\n", "import pandas as pd\n", "import numpy as np\n", "\n", "from typing import Optional, Tuple, List" ] }, { "cell_type": "markdown", "id": "33722c04-e2f7-4b57-b288-cbe8c35e6973", "metadata": {}, "source": [ "First, note that Sentinel-2 data is provided in different files: one for each spectral channel. FRE stands for Flat REflectance: this is reflectance data that has been corrected for atmospheric effects, environmental effects, and slope effects. It is also possible to have SRE products, which stands for Surface REflectance, which have not been corrected from slope effects." ] }, { "cell_type": "code", "execution_count": 2, "id": "e3f17868-bb8d-4b1a-a192-2bd600c72c19", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B2.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B3.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B4.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B5.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B6.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B7.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B8.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B8A.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B11.tif',\n", " 'SUBSET_SENTINEL2C_20250730-104911-712_L2A_T31TFJ_C_V4-0_FRE_B12.tif']" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "s2_folder = 'S2_SUBSET'\n", "img_files = os.listdir(s2_folder)\n", "img_files = sorted(img_files, key=lambda x: float(x.split('FRE_B')[-1].split('.')[0].replace('A', '.5')))\n", "img_files" ] }, { "cell_type": "markdown", "id": "6f6783f0-694c-4d7b-9b4e-2043cf81288a", "metadata": {}, "source": [ "Each channel corresponds to a detector with a specific spectral sensitivity, described by the sensor spectral function response. Let's have a look at it!" ] }, { "cell_type": "code", "execution_count": 3, "id": "8daeee00-b5a2-4ff1-a2e5-efc0d7031db7", "metadata": {}, "outputs": [], "source": [ "s2_spectral_response_function_download_link = \"https://sentiwiki.copernicus.eu/__attachments/1692737/COPE-GSEG-EOPG-TN-15-0007%20-%20Sentinel-2%20Spectral%20Response%20Functions%202024%20-%204.0.xlsx?inst-v=59ac1eb3-61c9-4626-b824-7509bfd7c905\"\n", "s2_spectral_response_function_file = \"COPE-GSEG-EOPG-TN-15-0007 - Sentinel-2 Spectral Response Functions 2024 - 4.0.xlsx\"\n", "\n", "if s2_spectral_response_function_file not in os.listdir():\n", " urllib.request.urlretrieve(s2_spectral_response_function_download_link, s2_spectral_response_function_file)" ] }, { "cell_type": "markdown", "id": "4e9190f9-fc79-46e3-b85d-66ac77e74431", "metadata": {}, "source": [ "Currently, there are 3 Sentinel-2 satellites: Sentinel-2A, Sentinel-2B, and Sentinel-2C. They have slightly different spectral function responses (SFR). Here, we will plot the Sentinel-2A SFR." ] }, { "cell_type": "code", "execution_count": 4, "id": "78ef9f3a-f007-47ff-91a4-fb491d42772c", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/romain/anaconda3/envs/ml/lib/python3.10/site-packages/openpyxl/worksheet/_reader.py:329: UserWarning: Unknown extension is not supported and will be removed\n", " warn(msg)\n" ] }, { "data": { "text/html": [ "
| \n", " | SR_WL | \n", "S2A_SR_AV_B1 | \n", "S2A_SR_AV_B2 | \n", "S2A_SR_AV_B3 | \n", "S2A_SR_AV_B4 | \n", "S2A_SR_AV_B5 | \n", "S2A_SR_AV_B6 | \n", "S2A_SR_AV_B7 | \n", "S2A_SR_AV_B8 | \n", "S2A_SR_AV_B8A | \n", "S2A_SR_AV_B9 | \n", "S2A_SR_AV_B10 | \n", "S2A_SR_AV_B11 | \n", "S2A_SR_AV_B12 | \n", "
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | \n", "300 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "
| 1 | \n", "301 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "
| 2 | \n", "302 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "
| 3 | \n", "303 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "
| 4 | \n", "304 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "0.0 | \n", "