Sentinel-3

Sentinel-3#

Let’s use EOReader to open and read some Sentinel-3 bands and see the specificities of OLCI and SLSTR constellations.

Note that Sentinel-3 processes have stopped to use SNAP since the 0.8.0 version. Some minor discrepancies might occur both in the geocoding (especially OLCI) and the reflectance values (SLSTR).
# Imports
import os

# EOReader
from eoreader.reader import Reader
from eoreader.bands import YELLOW, Oa21, RED, SWIR_2, F1

# Declare the reader (only once)
reader = Reader()
/home/docs/checkouts/readthedocs.org/user_builds/eoreader/envs/latest/lib/python3.9/site-packages/dask/dataframe/__init__.py:42: FutureWarning: 
Dask dataframe query planning is disabled because dask-expr is not installed.

You can install it with `pip install dask[dataframe]` or `conda install dask`.
This will raise in a future version.

  warnings.warn(msg, FutureWarning)
# Create logger
import logging
from sertit import logs

logger = logging.getLogger("eoreader")
logs.init_logger(logger)

Sentinel-3 OLCI#

# First of all, let's focus on Sentinel-3 OLCI data
olci_path = os.path.join(
    "/home", "prods", "S3",
    "S3A_OL_1_EFR____20191215T105023_20191215T105323_20191216T153115_0179_052_322_2160_LN1_O_NT_002.zip"
)
olci_prod = reader.open(olci_path, remove_tmp=True)
olci_prod
2025-05-05 11:15:01,523 - [WARNING] - There is no existing products in EOReader corresponding to /home/prods/S3/S3A_OL_1_EFR____20191215T105023_20191215T105323_20191216T153115_0179_052_322_2160_LN1_O_NT_002.zip.
2025-05-05 11:15:01,523 - [INFO] - Your given path may not be a satellite image. If it is, maybe the product isn't handled by EOReader. If you are sure this product is handled, it is either corrupted or you may need to go deeper in the filetree to find the correct path to give.
2025-05-05 11:15:01,524 - [DEBUG] - Please look at what folder you should give to EOReader by accessing the documentation: https://eoreader.readthedocs.io/latest/main_features.html#recognized-paths
# Get the bands information
olci_prod.bands
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[4], line 2
      1 # Get the bands information
----> 2 olci_prod.bands

AttributeError: 'NoneType' object has no attribute 'bands'
# Load the Yellow band and the far NIR one
# Please note that mapped band need to be called by their mapped name and the specific one with their true name
olci_bands = olci_prod.load([YELLOW, Oa21])
2023-05-31 11:47:11,601 - [DEBUG] - Loading bands ['YELLOW', 'Oa21']
2023-05-31 11:47:14,879 - [DEBUG] - Converting YELLOW to reflectance
/opt/conda/lib/python3.10/site-packages/rasterio/__init__.py:314: NotGeoreferencedWarning: The given matrix is equal to Affine.identity or its flipped counterpart. GDAL may ignore this matrix and save no geotransform without raising an error. This behavior is somewhat driver-specific.
  dataset = writer(
2023-05-31 11:47:31,933 - [DEBUG] - Geocoding YELLOW
2023-05-31 11:49:09,792 - [DEBUG] - Converting Oa21 to reflectance
2023-05-31 11:52:59,035 - [DEBUG] - Geocoding Oa21
2023-05-31 11:56:01,847 - [DEBUG] - Read YELLOW
2023-05-31 11:56:02,470 - [DEBUG] - Manage nodata for band YELLOW
2023-05-31 11:56:02,502 - [DEBUG] - Converting YELLOW to reflectance
2023-05-31 11:56:05,651 - [DEBUG] - Read Oa21
2023-05-31 11:56:05,707 - [DEBUG] - Manage nodata for band Oa21
2023-05-31 11:56:05,728 - [DEBUG] - Converting Oa21 to reflectance
# Plot a subsampled version
olci_bands[YELLOW][:, ::10, ::10].plot()
<matplotlib.collections.QuadMesh at 0x7fce10130820>
../_images/d494e5d7993a16b685ba0ce090d1a572688c1db13007a3ee1ed7cab6df6ac3b0.png
olci_bands[Oa21][:, ::10, ::10].plot()
<matplotlib.collections.QuadMesh at 0x7fce08239090>
../_images/bc6292015786467aca33fa3b0131ee8f4724768251873059ee73f0416953c001.png

Sentinel-3 SLSTR#

# Other SLSTR imports
from eoreader.keywords import SLSTR_VIEW, SLSTR_STRIPE, SLSTR_RAD_ADJUST
from eoreader.products import SlstrRadAdjustTuple, SlstrRadAdjust, SlstrView, SlstrStripe

# Then, let's focus on Sentinel-3 SLSTR data (extracted here, but a zip would work)
slstr_path = os.path.join(
    "/home", "prods", "S3",
    "S3B_SL_1_RBT____20191115T233722_20191115T234022_20191117T031722_0179_032_144_3420_LN2_O_NT_003.SEN3"
)
slstr_prod = reader.open(slstr_path, remove_tmp=True)
slstr_prod
eoreader.S3SlstrProduct 'S3B_SL_1_RBT____20191115T233722_20191115T234022_20191117T031722_0179_032_144_3420_LN2_O_NT_003'
Attributes:
	condensed_name: 20191115T233722_S3_SLSTR_RBT
	path: /home/data/DATA/PRODS/S3/S3B_SL_1_RBT____20191115T233722_20191115T234022_20191117T031722_0179_032_144_3420_LN2_O_NT_003.SEN3
	constellation: Sentinel-3 SLSTR
	sensor type: Optical
	product type: SL_1_RBT___
	default pixel size: 500.0
	default resolution: 500.0
	acquisition datetime: 2019-11-15T23:37:22.254773
	band mapping:
		GREEN: S1
		RED: S2
		NIR: S3
		NARROW_NIR: S3
		CIRRUS: S4
		SWIR_1: S5
		SWIR_2: S6
		THERMAL_IR_1: S8
		THERMAL_IR_2: S9
		S7: S7
		F1: F1
		F2: F2
	needs extraction: False
# Get the bands information
slstr_prod.bands
eoreader.SpectralBand 'F1'
Attributes:
	id: F1
	eoreader_name: F1
	common_name: 
	gsd (m): 1000.0
	asset_role: brightness_temperature
	Center wavelength (nm): 3742.0
	Bandwidth (nm): 398.0
	description: Active fire, brightness temperature, 1km
eoreader.SpectralBand 'F2'
Attributes:
	id: F2
	eoreader_name: F2
	common_name: 
	gsd (m): 1000.0
	asset_role: brightness_temperature
	Center wavelength (nm): 10854.0
	Bandwidth (nm): 776.0
	description: Active fire, brightness temperature, 1km
eoreader.SpectralBand 'S1'
Attributes:
	id: S1
	eoreader_name: GREEN
	common_name: green
	gsd (m): 500.0
	asset_role: reflectance
	Center wavelength (nm): 554.2700000000001
	Bandwidth (nm): 19.259999999999998
	description: Cloud screening, vegetation monitoring, aerosol
eoreader.SpectralBand 'S2'
Attributes:
	id: S2
	eoreader_name: RED
	common_name: red
	gsd (m): 500.0
	asset_role: reflectance
	Center wavelength (nm): 659.47
	Bandwidth (nm): 19.25
	description: NDVI, vegetation monitoring, aerosol
eoreader.SpectralBand 'S3'
Attributes:
	id: S3
	eoreader_name: NIR
	common_name: nir
	gsd (m): 500.0
	asset_role: reflectance
	Center wavelength (nm): 868.0
	Bandwidth (nm): 20.6
	description: NDVI, cloud flagging, pixel co-registration
eoreader.SpectralBand 'S3'
Attributes:
	id: S3
	eoreader_name: NARROW_NIR
	common_name: nir08
	gsd (m): 500.0
	asset_role: reflectance
	Center wavelength (nm): 868.0
	Bandwidth (nm): 20.6
	description: NDVI, cloud flagging, pixel co-registration
eoreader.SpectralBand 'S4'
Attributes:
	id: S4
	eoreader_name: CIRRUS
	common_name: cirrus
	gsd (m): 500.0
	asset_role: reflectance
	Center wavelength (nm): 1374.8
	Bandwidth (nm): 20.8
	description: Cirrus detection over land
eoreader.SpectralBand 'S5'
Attributes:
	id: S5
	eoreader_name: SWIR_1
	common_name: swir16
	gsd (m): 500.0
	asset_role: reflectance
	Center wavelength (nm): 1613.3999999999999
	Bandwidth (nm): 60.68
	description: Cloud clearing, ice, snow, vegetation monitoring
eoreader.SpectralBand 'S6'
Attributes:
	id: S6
	eoreader_name: SWIR_2
	common_name: swir22
	gsd (m): 1000.0
	asset_role: reflectance
	Center wavelength (nm): 2255.7
	Bandwidth (nm): 50.15
	description: Vegetation state and cloud clearing
eoreader.SpectralBand 'S7'
Attributes:
	id: S7
	eoreader_name: S7
	common_name: 
	gsd (m): 1000.0
	asset_role: brightness_temperature
	Center wavelength (nm): 3742.0
	Bandwidth (nm): 398.0
	description: SST, LST, Active fire, brightness temperature, 1km
eoreader.SpectralBand 'S8'
Attributes:
	id: S8
	eoreader_name: THERMAL_IR_1
	common_name: lwir11
	gsd (m): 1000.0
	asset_role: brightness_temperature
	Center wavelength (nm): 10854.0
	Bandwidth (nm): 776.0
	description: SST, LST, Active fire, brightness temperature, 1km
eoreader.SpectralBand 'S9'
Attributes:
	id: S9
	eoreader_name: THERMAL_IR_2
	common_name: lwir12
	gsd (m): 1000.0
	asset_role: brightness_temperature
	Center wavelength (nm): 12022.5
	Bandwidth (nm): 905.0
	description: SST, LST, brightness temperature, 1km
# Same remark for mapped and specific band than above
# Not that native radiance band are converted into reflectance, whereas brilliance temperature bands are not

# Load bands with nadir view and stripe B
# (for bands that have a stripe B, the other will load their unique stripe, namely A or I)
# RED: only stripe A
# SWIR_2: has strip 1, B and TDI (c)
# F1: has only stripe I
slstr_bn_bands = slstr_prod.load([RED, SWIR_2, F1], slstr_view="n", slstr_stripe="b")
slstr_bn_bands_2 = slstr_prod.load([RED, SWIR_2, F1], **{SLSTR_VIEW: SlstrView.NADIR, SLSTR_STRIPE: SlstrStripe.B})
2023-05-31 11:56:18,814 - [DEBUG] - Loading bands ['RED', 'SWIR_2', 'F1']
2023-05-31 11:56:19,164 - [DEBUG] - Converting RED to reflectance
2023-05-31 11:56:24,814 - [DEBUG] - Geocoding RED
2023-05-31 11:56:34,957 - [DEBUG] - Converting SWIR_2 to reflectance
2023-05-31 11:56:42,317 - [DEBUG] - Geocoding SWIR_2
2023-05-31 11:57:14,853 - [DEBUG] - Geocoding F1
2023-05-31 11:57:21,079 - [DEBUG] - Read RED
2023-05-31 11:57:21,131 - [DEBUG] - Manage nodata for band RED
2023-05-31 11:57:21,140 - [DEBUG] - Converting RED to reflectance
2023-05-31 11:57:21,874 - [DEBUG] - Read SWIR_2
2023-05-31 11:57:21,911 - [DEBUG] - Manage nodata for band SWIR_2
2023-05-31 11:57:21,918 - [DEBUG] - Converting SWIR_2 to reflectance
2023-05-31 11:57:22,719 - [DEBUG] - Read F1
2023-05-31 11:57:22,748 - [DEBUG] - Manage nodata for band F1
2023-05-31 11:57:22,757 - [DEBUG] - Converting F1 to reflectance
2023-05-31 11:57:24,883 - [DEBUG] - Loading bands ['RED', 'SWIR_2', 'F1']
2023-05-31 11:57:24,885 - [DEBUG] - Read RED
2023-05-31 11:57:24,916 - [DEBUG] - Manage nodata for band RED
2023-05-31 11:57:24,925 - [DEBUG] - Converting RED to reflectance
2023-05-31 11:57:25,547 - [DEBUG] - Read SWIR_2
2023-05-31 11:57:25,571 - [DEBUG] - Manage nodata for band SWIR_2
2023-05-31 11:57:25,578 - [DEBUG] - Converting SWIR_2 to reflectance
2023-05-31 11:57:26,233 - [DEBUG] - Read F1
2023-05-31 11:57:26,256 - [DEBUG] - Manage nodata for band F1
2023-05-31 11:57:26,261 - [DEBUG] - Converting F1 to reflectance
# You can use the keywords by importing them or copy their value.
# Their values can be passed as strings or as an Enum
# However, it seems safer to import the keywords and use the enum
# The result should be the same

# Please bear in mind that oblique and nadir views are not stackable !
# However, you can stack different stripes
# (but you cannot load them at once and you should collocate them to be sure, their reprojection grid may vary as their GCP vary)
# Plot a subsampled version
slstr_bn_bands[RED][:, ::10, ::10].plot()
<matplotlib.collections.QuadMesh at 0x7fce081c6830>
../_images/870be6cd550d3591229cd91153f5e33bf36de3001a792584c5eded8c8308e8b5.png
slstr_bn_bands[SWIR_2][:, ::10, ::10].plot()
<matplotlib.collections.QuadMesh at 0x7fce08185d20>
../_images/0e4531469f3df5286e1b8bfb4384212daae0cf686c9f12296750be996c3866a6.png
slstr_bn_bands[F1][:, ::10, ::10].plot()
<matplotlib.collections.QuadMesh at 0x7fcdb4447b20>
../_images/e67fca4ff5241217fd7af99ecc6632b89e0f64b1796f3134a2dce74b4ea326f0.png
# Sentinel-3 SLSTR radiance is not nominal, so EUMETSAT advises the user to make some radiance adjustments
# As stated here: https://www-cdn.eumetsat.int/files/2021-05/S3.PN-SLSTR-L1.08%20-%20i1r0%20-%20SLSTR%20L1%20PB%202.75-A%20and%201.53-B.pdf
# These coefficients have been added since the 06 version and several sets exist:

# The last one (S3.PN-SLSTR-L1.08, since 18/05/2021) which is also the default one
SlstrRadAdjust.S3_PN_SLSTR_L1_08
<SlstrRadAdjust.S3_PN_SLSTR_L1_08: SlstrRadAdjustTuple(S1_n=0.97, S2_n=0.98, S3_n=0.98, S4_n=1.0, S5_n=1.11, S6_n=1.13, S1_o=0.94, S2_o=0.95, S3_o=0.95, S4_o=1.0, S5_o=1.04, S6_o=1.07)>
# The two older sets given by EUMETSAT are the same
assert SlstrRadAdjust.S3_PN_SLSTR_L1_07 == SlstrRadAdjust.S3_PN_SLSTR_L1_06
SlstrRadAdjust.S3_PN_SLSTR_L1_07
<SlstrRadAdjust.S3_PN_SLSTR_L1_06: SlstrRadAdjustTuple(S1_n=1.0, S2_n=1.0, S3_n=1.0, S4_n=1.0, S5_n=1.12, S6_n=1.15, S1_o=1.0, S2_o=1.0, S3_o=1.0, S4_o=1.0, S5_o=1.2, S6_o=1.26)>
# Moreover, SNAP uses a different set with unknown origin (optional, in S3MPC Calibration)
SlstrRadAdjust.SNAP
<SlstrRadAdjust.SNAP: SlstrRadAdjustTuple(S1_n=1.0, S2_n=1.0, S3_n=1.0, S4_n=1.0, S5_n=1.12, S6_n=1.13, S1_o=1.0, S2_o=1.0, S3_o=1.0, S4_o=1.0, S5_o=1.15, S6_o=1.14)>
# A default set also exists, with every coefficient set to 1.0
SlstrRadAdjust.NONE
<SlstrRadAdjust.NONE: SlstrRadAdjustTuple(S1_n=1.0, S2_n=1.0, S3_n=1.0, S4_n=1.0, S5_n=1.0, S6_n=1.0, S1_o=1.0, S2_o=1.0, S3_o=1.0, S4_o=1.0, S5_o=1.0, S6_o=1.0)>
# You can use your own set by creating one.
# All the coefficients are set to 1.0 by default, so just modify the one you want
# The band keywords are {true_name}_{view_letter}
# RED is S2
user_set = SlstrRadAdjustTuple(S1_n=1.15, S2_o=1.12)

# However please bear in mind that if you want to reload the same band with a different adjustment, 
# you have to remove the temporary process folder or the previous band will be reloaded.
slstr_prod.clean_tmp()

# To apply these sets when loading a band, just add the keyword when loading it
red_pn_08 = slstr_bn_bands[RED]
slstr_red_bn = slstr_prod.load(
    RED,
    **{
        SLSTR_VIEW: SlstrView.NADIR,
        SLSTR_STRIPE: SlstrStripe.B,
        SLSTR_RAD_ADJUST: user_set
    }
)
red_user = slstr_red_bn[RED]
red_user[:, ::10, ::10].plot()
2023-05-31 11:57:35,293 - [DEBUG] - Loading bands ['RED']
2023-05-31 11:57:35,786 - [DEBUG] - Converting RED to reflectance
2023-05-31 11:57:41,670 - [DEBUG] - Geocoding RED
2023-05-31 11:57:48,599 - [DEBUG] - Read RED
2023-05-31 11:57:48,623 - [DEBUG] - Manage nodata for band RED
2023-05-31 11:57:48,629 - [DEBUG] - Converting RED to reflectance
<matplotlib.collections.QuadMesh at 0x7fcdb41f2110>
../_images/79d7816687d117ee317bf8030ddceb0a8443ed05b5fbcb8633849bc0716d105d.png
# We may need to collocate the bands if we want to work on two sets loaded apart
# Indeed, in EOReader, the bands are collocated when loaded together

# For example, if we wanted to work on the SWIR or F1 band, 
# as we first loaded them with the RED, they are collocated to this band (the first one) 
# Yet, their geodetic grid are different from the RED one (in and bn are slightly different than the an)
# So if we load on a second time only the SWIR or the F1 band, their are chances that the geocoding might be a little different
# The it is best to collocate the two bands just to be sure they will always match (and have the same size)

# To do so you could do:
from sertit import rasters

red_user = rasters.collocate(red_pn_08, other=red_user)

# Here, it is useless as we work on the master band
abs(red_pn_08 - red_user)[:, ::10, ::10].plot()
<matplotlib.collections.QuadMesh at 0x7fcdb4066710>
../_images/4d34bd79c70c0a9a064fbab9c527f6dfd2e2445df030e96f8e1d2919f297fd54.png